helpers.ts 12 KB

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