helpers.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /* tslint:disable:no-console */
  2. import chalk from 'chalk';
  3. import { execSync } from 'child_process';
  4. import spawn from 'cross-spawn';
  5. import fs from 'fs-extra';
  6. import path from 'path';
  7. import semver from 'semver';
  8. import { DbType, LogLevel } from './types';
  9. /**
  10. * If project only contains files generated by GH, it’s safe.
  11. * Also, if project contains remnant error logs from a previous
  12. * installation, lets remove them now.
  13. * We also special case IJ-based products .idea because it integrates with CRA:
  14. * https://github.com/facebook/create-react-app/pull/368#issuecomment-243446094
  15. */
  16. export function isSafeToCreateProjectIn(root: string, name: string) {
  17. // These files should be allowed to remain on a failed install,
  18. // but then silently removed during the next create.
  19. const errorLogFilePatterns = ['npm-debug.log', 'yarn-error.log', 'yarn-debug.log'];
  20. const validFiles = [
  21. '.DS_Store',
  22. 'Thumbs.db',
  23. '.git',
  24. '.gitignore',
  25. '.idea',
  26. 'README.md',
  27. 'LICENSE',
  28. '.hg',
  29. '.hgignore',
  30. '.hgcheck',
  31. '.npmignore',
  32. 'mkdocs.yml',
  33. 'docs',
  34. '.travis.yml',
  35. '.gitlab-ci.yml',
  36. '.gitattributes',
  37. ];
  38. console.log();
  39. const conflicts = fs
  40. .readdirSync(root)
  41. .filter(file => !validFiles.includes(file))
  42. // IntelliJ IDEA creates module files before CRA is launched
  43. .filter(file => !/\.iml$/.test(file))
  44. // Don't treat log files from previous installation as conflicts
  45. .filter(file => !errorLogFilePatterns.some(pattern => file.indexOf(pattern) === 0));
  46. if (conflicts.length > 0) {
  47. console.log(`The directory ${chalk.green(name)} contains files that could conflict:`);
  48. console.log();
  49. for (const file of conflicts) {
  50. console.log(` ${file}`);
  51. }
  52. console.log();
  53. console.log('Either try using a new directory name, or remove the files listed above.');
  54. return false;
  55. }
  56. // Remove any remnant files from a previous installation
  57. const currentFiles = fs.readdirSync(path.join(root));
  58. currentFiles.forEach(file => {
  59. errorLogFilePatterns.forEach(errorLogFilePattern => {
  60. // This will catch `(npm-debug|yarn-error|yarn-debug).log*` files
  61. if (file.indexOf(errorLogFilePattern) === 0) {
  62. fs.removeSync(path.join(root, file));
  63. }
  64. });
  65. });
  66. return true;
  67. }
  68. export function checkNodeVersion(requiredVersion: string) {
  69. if (!semver.satisfies(process.version, requiredVersion)) {
  70. console.error(
  71. chalk.red(
  72. 'You are running Node %s.\n' +
  73. 'Vendure requires Node %s or higher. \n' +
  74. 'Please update your version of Node.',
  75. ),
  76. process.version,
  77. requiredVersion,
  78. );
  79. process.exit(1);
  80. }
  81. }
  82. export function shouldUseYarn() {
  83. try {
  84. execSync('yarnpkg --version', { stdio: 'ignore' });
  85. return true;
  86. } catch (e) {
  87. return false;
  88. }
  89. }
  90. export function checkThatNpmCanReadCwd() {
  91. const cwd = process.cwd();
  92. let childOutput = null;
  93. try {
  94. // Note: intentionally using spawn over exec since
  95. // the problem doesn't reproduce otherwise.
  96. // `npm config list` is the only reliable way I could find
  97. // to reproduce the wrong path. Just printing process.cwd()
  98. // in a Node process was not enough.
  99. childOutput = spawn.sync('npm', ['config', 'list']).output.join('');
  100. } catch (err) {
  101. // Something went wrong spawning node.
  102. // Not great, but it means we can't do this check.
  103. // We might fail later on, but let's continue.
  104. return true;
  105. }
  106. if (typeof childOutput !== 'string') {
  107. return true;
  108. }
  109. const lines = childOutput.split('\n');
  110. // `npm config list` output includes the following line:
  111. // "; cwd = C:\path\to\current\dir" (unquoted)
  112. // I couldn't find an easier way to get it.
  113. const prefix = '; cwd = ';
  114. const line = lines.find(l => l.indexOf(prefix) === 0);
  115. if (typeof line !== 'string') {
  116. // Fail gracefully. They could remove it.
  117. return true;
  118. }
  119. const npmCWD = line.substring(prefix.length);
  120. if (npmCWD === cwd) {
  121. return true;
  122. }
  123. console.error(
  124. chalk.red(
  125. `Could not start an npm process in the right directory.\n\n` +
  126. `The current directory is: ${chalk.bold(cwd)}\n` +
  127. `However, a newly started npm process runs in: ${chalk.bold(npmCWD)}\n\n` +
  128. `This is probably caused by a misconfigured system terminal shell.`,
  129. ),
  130. );
  131. if (process.platform === 'win32') {
  132. console.error(
  133. chalk.red(`On Windows, this can usually be fixed by running:\n\n`) +
  134. ` ${chalk.cyan(
  135. 'reg',
  136. )} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
  137. ` ${chalk.cyan(
  138. 'reg',
  139. )} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
  140. chalk.red(`Try to run the above two lines in the terminal.\n`) +
  141. chalk.red(
  142. `To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`,
  143. ),
  144. );
  145. }
  146. return false;
  147. }
  148. /**
  149. * Install packages via npm or yarn.
  150. * Based on the install function from https://github.com/facebook/create-react-app
  151. */
  152. export function installPackages(
  153. root: string,
  154. useYarn: boolean,
  155. dependencies: string[],
  156. isDev: boolean,
  157. logLevel: LogLevel,
  158. ): Promise<void> {
  159. return new Promise((resolve, reject) => {
  160. let command: string;
  161. let args: string[];
  162. if (useYarn) {
  163. command = 'yarnpkg';
  164. args = ['add', '--exact', '--ignore-engines'];
  165. if (isDev) {
  166. args.push('--dev');
  167. }
  168. args = args.concat(dependencies);
  169. // Explicitly set cwd() to work around issues like
  170. // https://github.com/facebook/create-react-app/issues/3326.
  171. // Unfortunately we can only do this for Yarn because npm support for
  172. // equivalent --prefix flag doesn't help with this issue.
  173. // This is why for npm, we run checkThatNpmCanReadCwd() early instead.
  174. args.push('--cwd');
  175. args.push(root);
  176. } else {
  177. command = 'npm';
  178. args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat(dependencies);
  179. if (isDev) {
  180. args.push('--save-dev');
  181. }
  182. }
  183. if (logLevel === 'verbose') {
  184. args.push('--verbose');
  185. }
  186. const child = spawn(command, args, { stdio: logLevel === 'silent' ? 'ignore' : 'inherit' });
  187. child.on('close', code => {
  188. if (code !== 0) {
  189. reject({
  190. message:
  191. 'An error occurred when installing dependencies. Try running with `--log-level info` to diagnose.',
  192. command: `${command} ${args.join(' ')}`,
  193. });
  194. return;
  195. }
  196. resolve();
  197. });
  198. });
  199. }
  200. export function getDependencies(
  201. usingTs: boolean,
  202. dbType: DbType,
  203. ): { dependencies: string[]; devDependencies: string[] } {
  204. const dependencies = [
  205. '@vendure/core',
  206. '@vendure/email-plugin',
  207. '@vendure/asset-server-plugin',
  208. '@vendure/admin-ui-plugin',
  209. dbDriverPackage(dbType),
  210. ];
  211. const devDependencies = ['concurrently'];
  212. if (usingTs) {
  213. devDependencies.push('ts-node');
  214. }
  215. return { dependencies, devDependencies };
  216. }
  217. /**
  218. * Returns the name of the npm driver package for the
  219. * selected database.
  220. */
  221. function dbDriverPackage(dbType: DbType): string {
  222. switch (dbType) {
  223. case 'mysql':
  224. return 'mysql';
  225. case 'postgres':
  226. return 'pg';
  227. case 'sqlite':
  228. return 'sqlite3';
  229. case 'sqljs':
  230. return 'sql.js';
  231. case 'mssql':
  232. return 'mssql';
  233. case 'oracle':
  234. return 'oracledb';
  235. default:
  236. const n: never = dbType;
  237. console.error(chalk.red(`No driver package configured for type "${dbType}"`));
  238. return '';
  239. }
  240. }
  241. /**
  242. * Checks that the specified DB connection options are working (i.e. a connection can be
  243. * established) and that the named database exists.
  244. */
  245. export function checkDbConnection(options: any, root: string): Promise<true> {
  246. switch (options.type) {
  247. case 'mysql':
  248. return checkMysqlDbExists(options, root);
  249. case 'postgres':
  250. return checkPostgresDbExists(options, root);
  251. default:
  252. return Promise.resolve(true);
  253. }
  254. }
  255. async function checkMysqlDbExists(options: any, root: string): Promise<true> {
  256. const mysql = await import(path.join(root, 'node_modules/mysql'));
  257. const connectionOptions = {
  258. host: options.host,
  259. user: options.username,
  260. password: options.password,
  261. port: options.port,
  262. database: options.database,
  263. };
  264. const connection = mysql.createConnection(connectionOptions);
  265. return new Promise<boolean>((resolve, reject) => {
  266. connection.connect((err: any) => {
  267. if (err) {
  268. if (err.code === 'ER_BAD_DB_ERROR') {
  269. throwDatabaseDoesNotExist(options.database);
  270. }
  271. throwConnectionError(err);
  272. }
  273. resolve(true);
  274. });
  275. }).then(() => {
  276. return new Promise((resolve, reject) => {
  277. connection.end((err: any) => {
  278. resolve(true);
  279. });
  280. });
  281. });
  282. }
  283. async function checkPostgresDbExists(options: any, root: string): Promise<true> {
  284. const { Client } = await import(path.join(root, 'node_modules/pg'));
  285. const connectionOptions = {
  286. host: options.host,
  287. user: options.username,
  288. password: options.password,
  289. port: options.port,
  290. database: options.database,
  291. };
  292. const client = new Client(connectionOptions);
  293. try {
  294. await client.connect();
  295. } catch (e) {
  296. if (e.code === '3D000') {
  297. throwDatabaseDoesNotExist(options.database);
  298. }
  299. throwConnectionError(e);
  300. await client.end();
  301. throw e;
  302. }
  303. await client.end();
  304. return true;
  305. }
  306. function throwConnectionError(err: any) {
  307. throw new Error(
  308. `Could not connect to the database. ` +
  309. `Please check the connection settings in your Vendure config.\n[${err.message ||
  310. err.toString()}]`,
  311. );
  312. }
  313. function throwDatabaseDoesNotExist(name: string) {
  314. throw new Error(`Database "${name}" does not exist. Please create the database and then try again.`);
  315. }