helpers.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. /* eslint-disable no-console */
  2. import { execSync } from 'child_process';
  3. import spawn from 'cross-spawn';
  4. import fs from 'fs-extra';
  5. import path from 'path';
  6. import pc from 'picocolors';
  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. 'migration.ts',
  39. 'node_modules',
  40. 'package.json',
  41. 'package-lock.json',
  42. 'src',
  43. 'static',
  44. 'tsconfig.json',
  45. 'yarn.lock',
  46. ];
  47. console.log();
  48. const conflicts = fs
  49. .readdirSync(root)
  50. .filter(file => !validFiles.includes(file))
  51. // IntelliJ IDEA creates module files before CRA is launched
  52. .filter(file => !/\.iml$/.test(file))
  53. // Don't treat log files from previous installation as conflicts
  54. .filter(file => !errorLogFilePatterns.some(pattern => file.indexOf(pattern) === 0));
  55. if (conflicts.length > 0) {
  56. console.log(`The directory ${pc.green(name)} contains files that could conflict:`);
  57. console.log();
  58. for (const file of conflicts) {
  59. console.log(` ${file}`);
  60. }
  61. console.log();
  62. console.log('Either try using a new directory name, or remove the files listed above.');
  63. return false;
  64. }
  65. // Remove any remnant files from a previous installation
  66. const currentFiles = fs.readdirSync(path.join(root));
  67. currentFiles.forEach(file => {
  68. errorLogFilePatterns.forEach(errorLogFilePattern => {
  69. // This will catch `(npm-debug|yarn-error|yarn-debug).log*` files
  70. if (file.indexOf(errorLogFilePattern) === 0) {
  71. fs.removeSync(path.join(root, file));
  72. }
  73. });
  74. });
  75. return true;
  76. }
  77. export function scaffoldAlreadyExists(root: string, name: string): boolean {
  78. const scaffoldFiles = ['migration.ts', 'package.json', 'tsconfig.json', 'README.md'];
  79. const files = fs.readdirSync(root);
  80. return scaffoldFiles.every(scaffoldFile => files.includes(scaffoldFile));
  81. }
  82. export function checkNodeVersion(requiredVersion: string) {
  83. if (!semver.satisfies(process.version, requiredVersion)) {
  84. console.error(
  85. pc.red(
  86. 'You are running Node %s.\n' +
  87. 'Vendure requires Node %s or higher. \n' +
  88. 'Please update your version of Node.',
  89. ),
  90. process.version,
  91. requiredVersion,
  92. );
  93. process.exit(1);
  94. }
  95. }
  96. export function yarnIsAvailable() {
  97. try {
  98. const yarnVersion = execSync('yarnpkg --version');
  99. if (semver.major(yarnVersion.toString()) > 1) {
  100. return true;
  101. } else {
  102. return false;
  103. }
  104. } catch (e: any) {
  105. return false;
  106. }
  107. }
  108. // Bun support should not be exposed yet, see
  109. // https://github.com/oven-sh/bun/issues/4947
  110. // https://github.com/lovell/sharp/issues/3511
  111. export function bunIsAvailable() {
  112. try {
  113. execSync('bun --version', { stdio: 'ignore' });
  114. return true;
  115. } catch (e: any) {
  116. return false;
  117. }
  118. }
  119. export function checkThatNpmCanReadCwd() {
  120. const cwd = process.cwd();
  121. let childOutput = null;
  122. try {
  123. // Note: intentionally using spawn over exec since
  124. // the problem doesn't reproduce otherwise.
  125. // `npm config list` is the only reliable way I could find
  126. // to reproduce the wrong path. Just printing process.cwd()
  127. // in a Node process was not enough.
  128. childOutput = spawn.sync('npm', ['config', 'list']).output.join('');
  129. } catch (err: any) {
  130. // Something went wrong spawning node.
  131. // Not great, but it means we can't do this check.
  132. // We might fail later on, but let's continue.
  133. return true;
  134. }
  135. if (typeof childOutput !== 'string') {
  136. return true;
  137. }
  138. const lines = childOutput.split('\n');
  139. // `npm config list` output includes the following line:
  140. // "; cwd = C:\path\to\current\dir" (unquoted)
  141. // I couldn't find an easier way to get it.
  142. const prefix = '; cwd = ';
  143. const line = lines.find(l => l.indexOf(prefix) === 0);
  144. if (typeof line !== 'string') {
  145. // Fail gracefully. They could remove it.
  146. return true;
  147. }
  148. const npmCWD = line.substring(prefix.length);
  149. if (npmCWD === cwd) {
  150. return true;
  151. }
  152. console.error(
  153. pc.red(
  154. 'Could not start an npm process in the right directory.\n\n' +
  155. `The current directory is: ${pc.bold(cwd)}\n` +
  156. `However, a newly started npm process runs in: ${pc.bold(npmCWD)}\n\n` +
  157. 'This is probably caused by a misconfigured system terminal shell.',
  158. ),
  159. );
  160. if (process.platform === 'win32') {
  161. console.error(
  162. pc.red('On Windows, this can usually be fixed by running:\n\n') +
  163. ` ${pc.cyan('reg')} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
  164. ` ${pc.cyan(
  165. 'reg',
  166. )} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
  167. pc.red('Try to run the above two lines in the terminal.\n') +
  168. pc.red(
  169. 'To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/',
  170. ),
  171. );
  172. }
  173. return false;
  174. }
  175. /**
  176. * Install packages via npm or yarn.
  177. * Based on the install function from https://github.com/facebook/create-react-app
  178. */
  179. export function installPackages(
  180. root: string,
  181. useYarn: boolean,
  182. dependencies: string[],
  183. isDev: boolean,
  184. logLevel: CliLogLevel,
  185. isCi: boolean = false,
  186. ): Promise<void> {
  187. return new Promise((resolve, reject) => {
  188. let command: string;
  189. let args: string[];
  190. if (useYarn) {
  191. command = 'yarnpkg';
  192. args = ['add', '--exact', '--ignore-engines'];
  193. if (isDev) {
  194. args.push('--dev');
  195. }
  196. if (isCi) {
  197. // In CI, publish to Verdaccio
  198. // See https://github.com/yarnpkg/yarn/issues/6029
  199. args.push('--registry http://localhost:4873/');
  200. // Increase network timeout
  201. // See https://github.com/yarnpkg/yarn/issues/4890#issuecomment-358179301
  202. args.push('--network-timeout 300000');
  203. }
  204. args = args.concat(dependencies);
  205. // Explicitly set cwd() to work around issues like
  206. // https://github.com/facebook/create-react-app/issues/3326.
  207. // Unfortunately we can only do this for Yarn because npm support for
  208. // equivalent --prefix flag doesn't help with this issue.
  209. // This is why for npm, we run checkThatNpmCanReadCwd() early instead.
  210. args.push('--cwd');
  211. args.push(root);
  212. } else {
  213. command = 'npm';
  214. args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat(dependencies);
  215. if (isDev) {
  216. args.push('--save-dev');
  217. }
  218. }
  219. if (logLevel === 'verbose') {
  220. args.push('--verbose');
  221. }
  222. const child = spawn(command, args, { stdio: logLevel === 'silent' ? 'ignore' : 'inherit' });
  223. child.on('close', code => {
  224. if (code !== 0) {
  225. let message = 'An error occurred when installing dependencies.';
  226. if (logLevel === 'silent') {
  227. message += ' Try running with `--log-level info` or `--log-level verbose` to diagnose.';
  228. }
  229. reject({
  230. message,
  231. command: `${command} ${args.join(' ')}`,
  232. });
  233. return;
  234. }
  235. resolve();
  236. });
  237. });
  238. }
  239. export function getDependencies(
  240. dbType: DbType,
  241. vendurePkgVersion = '',
  242. ): { dependencies: string[]; devDependencies: string[] } {
  243. const dependencies = [
  244. `@vendure/core${vendurePkgVersion}`,
  245. `@vendure/email-plugin${vendurePkgVersion}`,
  246. `@vendure/asset-server-plugin${vendurePkgVersion}`,
  247. `@vendure/admin-ui-plugin${vendurePkgVersion}`,
  248. 'dotenv',
  249. dbDriverPackage(dbType),
  250. ];
  251. const devDependencies = [
  252. `@vendure/cli${vendurePkgVersion}`,
  253. 'concurrently',
  254. `typescript@${TYPESCRIPT_VERSION}`,
  255. ];
  256. return { dependencies, devDependencies };
  257. }
  258. /**
  259. * Returns the name of the npm driver package for the
  260. * selected database.
  261. */
  262. function dbDriverPackage(dbType: DbType): string {
  263. switch (dbType) {
  264. case 'mysql':
  265. case 'mariadb':
  266. return 'mysql';
  267. case 'postgres':
  268. return 'pg';
  269. case 'sqlite':
  270. return 'better-sqlite3';
  271. case 'sqljs':
  272. return 'sql.js';
  273. case 'mssql':
  274. return 'mssql';
  275. case 'oracle':
  276. return 'oracledb';
  277. default:
  278. const n: never = dbType;
  279. console.error(pc.red(`No driver package configured for type "${dbType as string}"`));
  280. return '';
  281. }
  282. }
  283. /**
  284. * Checks that the specified DB connection options are working (i.e. a connection can be
  285. * established) and that the named database exists.
  286. */
  287. export function checkDbConnection(options: any, root: string): Promise<true> {
  288. switch (options.type) {
  289. case 'mysql':
  290. return checkMysqlDbExists(options, root);
  291. case 'postgres':
  292. return checkPostgresDbExists(options, root);
  293. default:
  294. return Promise.resolve(true);
  295. }
  296. }
  297. async function checkMysqlDbExists(options: any, root: string): Promise<true> {
  298. const mysql = await import(path.join(root, 'node_modules/mysql'));
  299. const connectionOptions = {
  300. host: options.host,
  301. user: options.username,
  302. password: options.password,
  303. port: options.port,
  304. database: options.database,
  305. };
  306. const connection = mysql.createConnection(connectionOptions);
  307. return new Promise<boolean>((resolve, reject) => {
  308. connection.connect((err: any) => {
  309. if (err) {
  310. if (err.code === 'ER_BAD_DB_ERROR') {
  311. throwDatabaseDoesNotExist(options.database);
  312. }
  313. throwConnectionError(err);
  314. }
  315. resolve(true);
  316. });
  317. }).then(() => {
  318. return new Promise((resolve, reject) => {
  319. connection.end((err: any) => {
  320. resolve(true);
  321. });
  322. });
  323. });
  324. }
  325. async function checkPostgresDbExists(options: any, root: string): Promise<true> {
  326. const { Client } = await import(path.join(root, 'node_modules/pg'));
  327. const connectionOptions = {
  328. host: options.host,
  329. user: options.username,
  330. password: options.password,
  331. port: options.port,
  332. database: options.database,
  333. schema: options.schema,
  334. ssl: options.ssl,
  335. };
  336. const client = new Client(connectionOptions);
  337. try {
  338. await client.connect();
  339. const schema = await client.query(
  340. `SELECT schema_name FROM information_schema.schemata WHERE schema_name = '${
  341. options.schema as string
  342. }'`,
  343. );
  344. if (schema.rows.length === 0) {
  345. throw new Error('NO_SCHEMA');
  346. }
  347. } catch (e: any) {
  348. if (e.code === '3D000') {
  349. throwDatabaseDoesNotExist(options.database);
  350. } else if (e.message === 'NO_SCHEMA') {
  351. throwDatabaseSchemaDoesNotExist(options.database, options.schema);
  352. } else if (e.code === '28000') {
  353. throwSSLConnectionError(e, options.ssl);
  354. }
  355. throwConnectionError(e);
  356. await client.end();
  357. throw e;
  358. }
  359. await client.end();
  360. return true;
  361. }
  362. function throwConnectionError(err: any) {
  363. throw new Error(
  364. 'Could not connect to the database. ' +
  365. `Please check the connection settings in your Vendure config.\n[${
  366. (err.message || err.toString()) as string
  367. }]`,
  368. );
  369. }
  370. function throwSSLConnectionError(err: any, sslEnabled?: any) {
  371. throw new Error(
  372. 'Could not connect to the database. ' +
  373. (sslEnabled === undefined
  374. ? 'Is your server requiring an SSL connection?'
  375. : 'Are you sure your server supports SSL?') +
  376. `Please check the connection settings in your Vendure config.\n[${
  377. (err.message || err.toString()) as string
  378. }]`,
  379. );
  380. }
  381. function throwDatabaseDoesNotExist(name: string) {
  382. throw new Error(`Database "${name}" does not exist. Please create the database and then try again.`);
  383. }
  384. function throwDatabaseSchemaDoesNotExist(dbName: string, schemaName: string) {
  385. throw new Error(
  386. `Schema "${dbName}.${schemaName}" does not exist. Please create the schema "${schemaName}" and then try again.`,
  387. );
  388. }
  389. export function isServerPortInUse(): Promise<boolean> {
  390. // eslint-disable-next-line @typescript-eslint/no-var-requires
  391. const tcpPortUsed = require('tcp-port-used');
  392. try {
  393. return tcpPortUsed.check(SERVER_PORT);
  394. } catch (e: any) {
  395. console.log(pc.yellow(`Warning: could not determine whether port ${SERVER_PORT} is available`));
  396. return Promise.resolve(false);
  397. }
  398. }