helpers.ts 11 KB

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