helpers.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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 { CliLogLevel, DbType } 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: CliLogLevel,
  158. isCi: boolean = false,
  159. ): Promise<void> {
  160. return new Promise((resolve, reject) => {
  161. let command: string;
  162. let args: string[];
  163. if (useYarn) {
  164. command = 'yarnpkg';
  165. args = ['add', '--exact', '--ignore-engines'];
  166. if (isDev) {
  167. args.push('--dev');
  168. }
  169. if (isCi) {
  170. // In CI, publish to Verdaccio
  171. // See https://github.com/yarnpkg/yarn/issues/6029
  172. args.push('--registry http://localhost:4873/');
  173. // Increase network timeout
  174. // See https://github.com/yarnpkg/yarn/issues/4890#issuecomment-358179301
  175. args.push('--network-timeout 300000');
  176. }
  177. args = args.concat(dependencies);
  178. // Explicitly set cwd() to work around issues like
  179. // https://github.com/facebook/create-react-app/issues/3326.
  180. // Unfortunately we can only do this for Yarn because npm support for
  181. // equivalent --prefix flag doesn't help with this issue.
  182. // This is why for npm, we run checkThatNpmCanReadCwd() early instead.
  183. args.push('--cwd');
  184. args.push(root);
  185. } else {
  186. command = 'npm';
  187. args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat(dependencies);
  188. if (isDev) {
  189. args.push('--save-dev');
  190. }
  191. }
  192. if (logLevel === 'verbose') {
  193. args.push('--verbose');
  194. }
  195. const child = spawn(command, args, { stdio: logLevel === 'silent' ? 'ignore' : 'inherit' });
  196. child.on('close', code => {
  197. if (code !== 0) {
  198. reject({
  199. message:
  200. 'An error occurred when installing dependencies. Try running with `--log-level info` to diagnose.',
  201. command: `${command} ${args.join(' ')}`,
  202. });
  203. return;
  204. }
  205. resolve();
  206. });
  207. });
  208. }
  209. export function getDependencies(
  210. usingTs: boolean,
  211. dbType: DbType,
  212. vendurePkgVersion = '',
  213. ): { dependencies: string[]; devDependencies: string[] } {
  214. const dependencies = [
  215. `@vendure/core${vendurePkgVersion}`,
  216. `@vendure/email-plugin${vendurePkgVersion}`,
  217. `@vendure/asset-server-plugin${vendurePkgVersion}`,
  218. `@vendure/admin-ui-plugin${vendurePkgVersion}`,
  219. dbDriverPackage(dbType),
  220. ];
  221. const devDependencies = ['concurrently'];
  222. if (usingTs) {
  223. devDependencies.push('ts-node');
  224. }
  225. return { dependencies, devDependencies };
  226. }
  227. /**
  228. * Returns the name of the npm driver package for the
  229. * selected database.
  230. */
  231. function dbDriverPackage(dbType: DbType): string {
  232. switch (dbType) {
  233. case 'mysql':
  234. return 'mysql';
  235. case 'postgres':
  236. return 'pg';
  237. case 'sqlite':
  238. return 'sqlite3';
  239. case 'sqljs':
  240. return 'sql.js';
  241. case 'mssql':
  242. return 'mssql';
  243. case 'oracle':
  244. return 'oracledb';
  245. default:
  246. const n: never = dbType;
  247. console.error(chalk.red(`No driver package configured for type "${dbType}"`));
  248. return '';
  249. }
  250. }
  251. /**
  252. * Checks that the specified DB connection options are working (i.e. a connection can be
  253. * established) and that the named database exists.
  254. */
  255. export function checkDbConnection(options: any, root: string): Promise<true> {
  256. switch (options.type) {
  257. case 'mysql':
  258. return checkMysqlDbExists(options, root);
  259. case 'postgres':
  260. return checkPostgresDbExists(options, root);
  261. default:
  262. return Promise.resolve(true);
  263. }
  264. }
  265. async function checkMysqlDbExists(options: any, root: string): Promise<true> {
  266. const mysql = await import(path.join(root, 'node_modules/mysql'));
  267. const connectionOptions = {
  268. host: options.host,
  269. user: options.username,
  270. password: options.password,
  271. port: options.port,
  272. database: options.database,
  273. };
  274. const connection = mysql.createConnection(connectionOptions);
  275. return new Promise<boolean>((resolve, reject) => {
  276. connection.connect((err: any) => {
  277. if (err) {
  278. if (err.code === 'ER_BAD_DB_ERROR') {
  279. throwDatabaseDoesNotExist(options.database);
  280. }
  281. throwConnectionError(err);
  282. }
  283. resolve(true);
  284. });
  285. }).then(() => {
  286. return new Promise((resolve, reject) => {
  287. connection.end((err: any) => {
  288. resolve(true);
  289. });
  290. });
  291. });
  292. }
  293. async function checkPostgresDbExists(options: any, root: string): Promise<true> {
  294. const { Client } = await import(path.join(root, 'node_modules/pg'));
  295. const connectionOptions = {
  296. host: options.host,
  297. user: options.username,
  298. password: options.password,
  299. port: options.port,
  300. database: options.database,
  301. };
  302. const client = new Client(connectionOptions);
  303. try {
  304. await client.connect();
  305. } catch (e) {
  306. if (e.code === '3D000') {
  307. throwDatabaseDoesNotExist(options.database);
  308. }
  309. throwConnectionError(e);
  310. await client.end();
  311. throw e;
  312. }
  313. await client.end();
  314. return true;
  315. }
  316. function throwConnectionError(err: any) {
  317. throw new Error(
  318. `Could not connect to the database. ` +
  319. `Please check the connection settings in your Vendure config.\n[${err.message ||
  320. err.toString()}]`,
  321. );
  322. }
  323. function throwDatabaseDoesNotExist(name: string) {
  324. throw new Error(`Database "${name}" does not exist. Please create the database and then try again.`);
  325. }