init.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. // @remove-file-on-eject
  2. /**
  3. * Copyright (c) 2015-present, Facebook, Inc.
  4. *
  5. * This source code is licensed under the MIT license found in the
  6. * LICENSE file in the root directory of this source tree.
  7. */
  8. 'use strict';
  9. // Makes the script crash on unhandled rejections instead of silently
  10. // ignoring them. In the future, promise rejections that are not handled will
  11. // terminate the Node.js process with a non-zero exit code.
  12. process.on('unhandledRejection', err => {
  13. throw err;
  14. });
  15. const fs = require('fs-extra');
  16. const path = require('path');
  17. const chalk = require('react-dev-utils/chalk');
  18. const execSync = require('child_process').execSync;
  19. const spawn = require('react-dev-utils/crossSpawn');
  20. const { defaultBrowsers } = require('react-dev-utils/browsersHelper');
  21. const os = require('os');
  22. const verifyTypeScriptSetup = require('./utils/verifyTypeScriptSetup');
  23. function isInGitRepository() {
  24. try {
  25. execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
  26. return true;
  27. } catch (e) {
  28. return false;
  29. }
  30. }
  31. function isInMercurialRepository() {
  32. try {
  33. execSync('hg --cwd . root', { stdio: 'ignore' });
  34. return true;
  35. } catch (e) {
  36. return false;
  37. }
  38. }
  39. function tryGitInit() {
  40. try {
  41. execSync('git --version', { stdio: 'ignore' });
  42. if (isInGitRepository() || isInMercurialRepository()) {
  43. return false;
  44. }
  45. execSync('git init', { stdio: 'ignore' });
  46. return true;
  47. } catch (e) {
  48. console.warn('Git repo not initialized', e);
  49. return false;
  50. }
  51. }
  52. function tryGitCommit(appPath) {
  53. try {
  54. execSync('git add -A', { stdio: 'ignore' });
  55. execSync('git commit -m "Initialize project using Create React App"', {
  56. stdio: 'ignore',
  57. });
  58. return true;
  59. } catch (e) {
  60. // We couldn't commit in already initialized git repo,
  61. // maybe the commit author config is not set.
  62. // In the future, we might supply our own committer
  63. // like Ember CLI does, but for now, let's just
  64. // remove the Git files to avoid a half-done state.
  65. console.warn('Git commit not created', e);
  66. console.warn('Removing .git directory...');
  67. try {
  68. // unlinkSync() doesn't work on directories.
  69. fs.removeSync(path.join(appPath, '.git'));
  70. } catch (removeErr) {
  71. // Ignore.
  72. }
  73. return false;
  74. }
  75. }
  76. module.exports = function (
  77. appPath,
  78. appName,
  79. verbose,
  80. originalDirectory,
  81. templateName
  82. ) {
  83. const appPackage = require(path.join(appPath, 'package.json'));
  84. const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
  85. if (!templateName) {
  86. console.log('');
  87. console.error(
  88. `A template was not provided. This is likely because you're using an outdated version of ${chalk.cyan(
  89. 'create-react-app'
  90. )}.`
  91. );
  92. console.error(
  93. `Please note that global installs of ${chalk.cyan(
  94. 'create-react-app'
  95. )} are no longer supported.`
  96. );
  97. console.error(
  98. `You can fix this by running ${chalk.cyan(
  99. 'npm uninstall -g create-react-app'
  100. )} or ${chalk.cyan(
  101. 'yarn global remove create-react-app'
  102. )} before using ${chalk.cyan('create-react-app')} again.`
  103. );
  104. return;
  105. }
  106. const templatePath = path.dirname(
  107. require.resolve(`${templateName}/package.json`, { paths: [appPath] })
  108. );
  109. const templateJsonPath = path.join(templatePath, 'template.json');
  110. let templateJson = {};
  111. if (fs.existsSync(templateJsonPath)) {
  112. templateJson = require(templateJsonPath);
  113. }
  114. const templatePackage = templateJson.package || {};
  115. // TODO: Deprecate support for root-level `dependencies` and `scripts` in v5.
  116. // These should now be set under the `package` key.
  117. if (templateJson.dependencies || templateJson.scripts) {
  118. console.log();
  119. console.log(
  120. chalk.yellow(
  121. 'Root-level `dependencies` and `scripts` keys in `template.json` are deprecated.\n' +
  122. 'This template should be updated to use the new `package` key.'
  123. )
  124. );
  125. console.log('For more information, visit https://cra.link/templates');
  126. }
  127. if (templateJson.dependencies) {
  128. templatePackage.dependencies = templateJson.dependencies;
  129. }
  130. if (templateJson.scripts) {
  131. templatePackage.scripts = templateJson.scripts;
  132. }
  133. // Keys to ignore in templatePackage
  134. const templatePackageBlacklist = [
  135. 'name',
  136. 'version',
  137. 'description',
  138. 'keywords',
  139. 'bugs',
  140. 'license',
  141. 'author',
  142. 'contributors',
  143. 'files',
  144. 'browser',
  145. 'bin',
  146. 'man',
  147. 'directories',
  148. 'repository',
  149. 'peerDependencies',
  150. 'bundledDependencies',
  151. 'optionalDependencies',
  152. 'engineStrict',
  153. 'os',
  154. 'cpu',
  155. 'preferGlobal',
  156. 'private',
  157. 'publishConfig',
  158. ];
  159. // Keys from templatePackage that will be merged with appPackage
  160. const templatePackageToMerge = ['dependencies', 'scripts'];
  161. // Keys from templatePackage that will be added to appPackage,
  162. // replacing any existing entries.
  163. const templatePackageToReplace = Object.keys(templatePackage).filter(key => {
  164. return (
  165. !templatePackageBlacklist.includes(key) &&
  166. !templatePackageToMerge.includes(key)
  167. );
  168. });
  169. // Copy over some of the devDependencies
  170. appPackage.dependencies = appPackage.dependencies || {};
  171. // Setup the script rules
  172. const templateScripts = templatePackage.scripts || {};
  173. appPackage.scripts = Object.assign(
  174. {
  175. start: 'react-scripts start',
  176. build: 'react-scripts build',
  177. test: 'react-scripts test',
  178. eject: 'react-scripts eject',
  179. },
  180. templateScripts
  181. );
  182. // Update scripts for Yarn users
  183. if (useYarn) {
  184. appPackage.scripts = Object.entries(appPackage.scripts).reduce(
  185. (acc, [key, value]) => ({
  186. ...acc,
  187. [key]: value.replace(/(npm run |npm )/, 'yarn '),
  188. }),
  189. {}
  190. );
  191. }
  192. // Setup the eslint config
  193. appPackage.eslintConfig = {
  194. extends: 'react-app',
  195. };
  196. // Setup the browsers list
  197. appPackage.browserslist = defaultBrowsers;
  198. // Add templatePackage keys/values to appPackage, replacing existing entries
  199. templatePackageToReplace.forEach(key => {
  200. appPackage[key] = templatePackage[key];
  201. });
  202. fs.writeFileSync(
  203. path.join(appPath, 'package.json'),
  204. JSON.stringify(appPackage, null, 2) + os.EOL
  205. );
  206. const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
  207. if (readmeExists) {
  208. fs.renameSync(
  209. path.join(appPath, 'README.md'),
  210. path.join(appPath, 'README.old.md')
  211. );
  212. }
  213. // Copy the files for the user
  214. const templateDir = path.join(templatePath, 'template');
  215. if (fs.existsSync(templateDir)) {
  216. fs.copySync(templateDir, appPath);
  217. } else {
  218. console.error(
  219. `Could not locate supplied template: ${chalk.green(templateDir)}`
  220. );
  221. return;
  222. }
  223. // modifies README.md commands based on user used package manager.
  224. if (useYarn) {
  225. try {
  226. const readme = fs.readFileSync(path.join(appPath, 'README.md'), 'utf8');
  227. fs.writeFileSync(
  228. path.join(appPath, 'README.md'),
  229. readme.replace(/(npm run |npm )/g, 'yarn '),
  230. 'utf8'
  231. );
  232. } catch (err) {
  233. // Silencing the error. As it fall backs to using default npm commands.
  234. }
  235. }
  236. const gitignoreExists = fs.existsSync(path.join(appPath, '.gitignore'));
  237. if (gitignoreExists) {
  238. // Append if there's already a `.gitignore` file there
  239. const data = fs.readFileSync(path.join(appPath, 'gitignore'));
  240. fs.appendFileSync(path.join(appPath, '.gitignore'), data);
  241. fs.unlinkSync(path.join(appPath, 'gitignore'));
  242. } else {
  243. // Rename gitignore after the fact to prevent npm from renaming it to .npmignore
  244. // See: https://github.com/npm/npm/issues/1862
  245. fs.moveSync(
  246. path.join(appPath, 'gitignore'),
  247. path.join(appPath, '.gitignore'),
  248. []
  249. );
  250. }
  251. // Initialize git repo
  252. let initializedGit = false;
  253. if (tryGitInit()) {
  254. initializedGit = true;
  255. console.log();
  256. console.log('Initialized a git repository.');
  257. }
  258. let command;
  259. let remove;
  260. let args;
  261. if (useYarn) {
  262. command = 'yarnpkg';
  263. remove = 'remove';
  264. args = ['add'];
  265. } else {
  266. command = 'npm';
  267. remove = 'uninstall';
  268. args = ['install', '--save', verbose && '--verbose'].filter(e => e);
  269. }
  270. // Install additional template dependencies, if present.
  271. const dependenciesToInstall = Object.entries({
  272. ...templatePackage.dependencies,
  273. ...templatePackage.devDependencies,
  274. });
  275. if (dependenciesToInstall.length) {
  276. args = args.concat(
  277. dependenciesToInstall.map(([dependency, version]) => {
  278. return `${dependency}@${version}`;
  279. })
  280. );
  281. }
  282. // Install react and react-dom for backward compatibility with old CRA cli
  283. // which doesn't install react and react-dom along with react-scripts
  284. if (!isReactInstalled(appPackage)) {
  285. args = args.concat(['react', 'react-dom']);
  286. }
  287. // Install template dependencies, and react and react-dom if missing.
  288. if ((!isReactInstalled(appPackage) || templateName) && args.length > 1) {
  289. console.log();
  290. console.log(`Installing template dependencies using ${command}...`);
  291. const proc = spawn.sync(command, args, { stdio: 'inherit' });
  292. if (proc.status !== 0) {
  293. console.error(`\`${command} ${args.join(' ')}\` failed`);
  294. return;
  295. }
  296. }
  297. if (args.find(arg => arg.includes('typescript'))) {
  298. console.log();
  299. verifyTypeScriptSetup();
  300. }
  301. // Remove template
  302. console.log(`Removing template package using ${command}...`);
  303. console.log();
  304. const proc = spawn.sync(command, [remove, templateName], {
  305. stdio: 'inherit',
  306. });
  307. if (proc.status !== 0) {
  308. console.error(`\`${command} ${args.join(' ')}\` failed`);
  309. return;
  310. }
  311. // Create git commit if git repo was initialized
  312. if (initializedGit && tryGitCommit(appPath)) {
  313. console.log();
  314. console.log('Created git commit.');
  315. }
  316. // Display the most elegant way to cd.
  317. // This needs to handle an undefined originalDirectory for
  318. // backward compatibility with old global-cli's.
  319. let cdpath;
  320. if (originalDirectory && path.join(originalDirectory, appName) === appPath) {
  321. cdpath = appName;
  322. } else {
  323. cdpath = appPath;
  324. }
  325. // Change displayed command to yarn instead of yarnpkg
  326. const displayedCommand = useYarn ? 'yarn' : 'npm';
  327. console.log();
  328. console.log(`Success! Created ${appName} at ${appPath}`);
  329. console.log('Inside that directory, you can run several commands:');
  330. console.log();
  331. console.log(chalk.cyan(` ${displayedCommand} start`));
  332. console.log(' Starts the development server.');
  333. console.log();
  334. console.log(
  335. chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}build`)
  336. );
  337. console.log(' Bundles the app into static files for production.');
  338. console.log();
  339. console.log(chalk.cyan(` ${displayedCommand} test`));
  340. console.log(' Starts the test runner.');
  341. console.log();
  342. console.log(
  343. chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}eject`)
  344. );
  345. console.log(
  346. ' Removes this tool and copies build dependencies, configuration files'
  347. );
  348. console.log(
  349. ' and scripts into the app directory. If you do this, you can’t go back!'
  350. );
  351. console.log();
  352. console.log('We suggest that you begin by typing:');
  353. console.log();
  354. console.log(chalk.cyan(' cd'), cdpath);
  355. console.log(` ${chalk.cyan(`${displayedCommand} start`)}`);
  356. if (readmeExists) {
  357. console.log();
  358. console.log(
  359. chalk.yellow(
  360. 'You had a `README.md` file, we renamed it to `README.old.md`'
  361. )
  362. );
  363. }
  364. console.log();
  365. console.log('Happy hacking!');
  366. };
  367. function isReactInstalled(appPackage) {
  368. const dependencies = appPackage.dependencies || {};
  369. return (
  370. typeof dependencies.react !== 'undefined' &&
  371. typeof dependencies['react-dom'] !== 'undefined'
  372. );
  373. }