init.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  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. // This was deprecated in CRA v5.
  116. if (templateJson.dependencies || templateJson.scripts) {
  117. console.log();
  118. console.log(
  119. chalk.red(
  120. 'Root-level `dependencies` and `scripts` keys in `template.json` were deprecated for Create React App 5.\n' +
  121. 'This template needs to be updated to use the new `package` key.'
  122. )
  123. );
  124. console.log('For more information, visit https://cra.link/templates');
  125. }
  126. // Keys to ignore in templatePackage
  127. const templatePackageBlacklist = [
  128. 'name',
  129. 'version',
  130. 'description',
  131. 'keywords',
  132. 'bugs',
  133. 'license',
  134. 'author',
  135. 'contributors',
  136. 'files',
  137. 'browser',
  138. 'bin',
  139. 'man',
  140. 'directories',
  141. 'repository',
  142. 'peerDependencies',
  143. 'bundledDependencies',
  144. 'optionalDependencies',
  145. 'engineStrict',
  146. 'os',
  147. 'cpu',
  148. 'preferGlobal',
  149. 'private',
  150. 'publishConfig',
  151. ];
  152. // Keys from templatePackage that will be merged with appPackage
  153. const templatePackageToMerge = ['dependencies', 'scripts'];
  154. // Keys from templatePackage that will be added to appPackage,
  155. // replacing any existing entries.
  156. const templatePackageToReplace = Object.keys(templatePackage).filter(key => {
  157. return (
  158. !templatePackageBlacklist.includes(key) &&
  159. !templatePackageToMerge.includes(key)
  160. );
  161. });
  162. // Copy over some of the devDependencies
  163. appPackage.dependencies = appPackage.dependencies || {};
  164. // Setup the script rules
  165. const templateScripts = templatePackage.scripts || {};
  166. appPackage.scripts = Object.assign(
  167. {
  168. start: 'react-scripts start',
  169. build: 'react-scripts build',
  170. test: 'react-scripts test',
  171. eject: 'react-scripts eject',
  172. },
  173. templateScripts
  174. );
  175. // Update scripts for Yarn users
  176. if (useYarn) {
  177. appPackage.scripts = Object.entries(appPackage.scripts).reduce(
  178. (acc, [key, value]) => ({
  179. ...acc,
  180. [key]: value.replace(/(npm run |npm )/, 'yarn '),
  181. }),
  182. {}
  183. );
  184. }
  185. // Setup the eslint config
  186. appPackage.eslintConfig = {
  187. extends: 'react-app',
  188. };
  189. // Setup the browsers list
  190. appPackage.browserslist = defaultBrowsers;
  191. // Add templatePackage keys/values to appPackage, replacing existing entries
  192. templatePackageToReplace.forEach(key => {
  193. appPackage[key] = templatePackage[key];
  194. });
  195. fs.writeFileSync(
  196. path.join(appPath, 'package.json'),
  197. JSON.stringify(appPackage, null, 2) + os.EOL
  198. );
  199. const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
  200. if (readmeExists) {
  201. fs.renameSync(
  202. path.join(appPath, 'README.md'),
  203. path.join(appPath, 'README.old.md')
  204. );
  205. }
  206. // Copy the files for the user
  207. const templateDir = path.join(templatePath, 'template');
  208. if (fs.existsSync(templateDir)) {
  209. fs.copySync(templateDir, appPath);
  210. } else {
  211. console.error(
  212. `Could not locate supplied template: ${chalk.green(templateDir)}`
  213. );
  214. return;
  215. }
  216. // modifies README.md commands based on user used package manager.
  217. if (useYarn) {
  218. try {
  219. const readme = fs.readFileSync(path.join(appPath, 'README.md'), 'utf8');
  220. fs.writeFileSync(
  221. path.join(appPath, 'README.md'),
  222. readme.replace(/(npm run |npm )/g, 'yarn '),
  223. 'utf8'
  224. );
  225. } catch (err) {
  226. // Silencing the error. As it fall backs to using default npm commands.
  227. }
  228. }
  229. const gitignoreExists = fs.existsSync(path.join(appPath, '.gitignore'));
  230. if (gitignoreExists) {
  231. // Append if there's already a `.gitignore` file there
  232. const data = fs.readFileSync(path.join(appPath, 'gitignore'));
  233. fs.appendFileSync(path.join(appPath, '.gitignore'), data);
  234. fs.unlinkSync(path.join(appPath, 'gitignore'));
  235. } else {
  236. // Rename gitignore after the fact to prevent npm from renaming it to .npmignore
  237. // See: https://github.com/npm/npm/issues/1862
  238. fs.moveSync(
  239. path.join(appPath, 'gitignore'),
  240. path.join(appPath, '.gitignore'),
  241. []
  242. );
  243. }
  244. // Initialize git repo
  245. let initializedGit = false;
  246. if (tryGitInit()) {
  247. initializedGit = true;
  248. console.log();
  249. console.log('Initialized a git repository.');
  250. }
  251. let command;
  252. let remove;
  253. let args;
  254. if (useYarn) {
  255. command = 'yarnpkg';
  256. remove = 'remove';
  257. args = ['add'];
  258. } else {
  259. command = 'npm';
  260. remove = 'uninstall';
  261. args = [
  262. 'install',
  263. '--no-audit', // https://github.com/facebook/create-react-app/issues/11174
  264. '--save',
  265. verbose && '--verbose',
  266. ].filter(e => e);
  267. }
  268. // Install additional template dependencies, if present.
  269. const dependenciesToInstall = Object.entries({
  270. ...templatePackage.dependencies,
  271. ...templatePackage.devDependencies,
  272. });
  273. if (dependenciesToInstall.length) {
  274. args = args.concat(
  275. dependenciesToInstall.map(([dependency, version]) => {
  276. return `${dependency}@${version}`;
  277. })
  278. );
  279. }
  280. // Install react and react-dom for backward compatibility with old CRA cli
  281. // which doesn't install react and react-dom along with react-scripts
  282. if (!isReactInstalled(appPackage)) {
  283. args = args.concat(['react', 'react-dom']);
  284. }
  285. // Install template dependencies, and react and react-dom if missing.
  286. if ((!isReactInstalled(appPackage) || templateName) && args.length > 1) {
  287. console.log();
  288. console.log(`Installing template dependencies using ${command}...`);
  289. const proc = spawn.sync(command, args, { stdio: 'inherit' });
  290. if (proc.status !== 0) {
  291. console.error(`\`${command} ${args.join(' ')}\` failed`);
  292. return;
  293. }
  294. }
  295. if (args.find(arg => arg.includes('typescript'))) {
  296. console.log();
  297. verifyTypeScriptSetup();
  298. }
  299. // Remove template
  300. console.log(`Removing template package using ${command}...`);
  301. console.log();
  302. const proc = spawn.sync(command, [remove, templateName], {
  303. stdio: 'inherit',
  304. });
  305. if (proc.status !== 0) {
  306. console.error(`\`${command} ${args.join(' ')}\` failed`);
  307. return;
  308. }
  309. // Create git commit if git repo was initialized
  310. if (initializedGit && tryGitCommit(appPath)) {
  311. console.log();
  312. console.log('Created git commit.');
  313. }
  314. // Display the most elegant way to cd.
  315. // This needs to handle an undefined originalDirectory for
  316. // backward compatibility with old global-cli's.
  317. let cdpath;
  318. if (originalDirectory && path.join(originalDirectory, appName) === appPath) {
  319. cdpath = appName;
  320. } else {
  321. cdpath = appPath;
  322. }
  323. // Change displayed command to yarn instead of yarnpkg
  324. const displayedCommand = useYarn ? 'yarn' : 'npm';
  325. console.log();
  326. console.log(`Success! Created ${appName} at ${appPath}`);
  327. console.log('Inside that directory, you can run several commands:');
  328. console.log();
  329. console.log(chalk.cyan(` ${displayedCommand} start`));
  330. console.log(' Starts the development server.');
  331. console.log();
  332. console.log(
  333. chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}build`)
  334. );
  335. console.log(' Bundles the app into static files for production.');
  336. console.log();
  337. console.log(chalk.cyan(` ${displayedCommand} test`));
  338. console.log(' Starts the test runner.');
  339. console.log();
  340. console.log(
  341. chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}eject`)
  342. );
  343. console.log(
  344. ' Removes this tool and copies build dependencies, configuration files'
  345. );
  346. console.log(
  347. ' and scripts into the app directory. If you do this, you can’t go back!'
  348. );
  349. console.log();
  350. console.log('We suggest that you begin by typing:');
  351. console.log();
  352. console.log(chalk.cyan(' cd'), cdpath);
  353. console.log(` ${chalk.cyan(`${displayedCommand} start`)}`);
  354. if (readmeExists) {
  355. console.log();
  356. console.log(
  357. chalk.yellow(
  358. 'You had a `README.md` file, we renamed it to `README.old.md`'
  359. )
  360. );
  361. }
  362. console.log();
  363. console.log('Happy hacking!');
  364. };
  365. function isReactInstalled(appPackage) {
  366. const dependencies = appPackage.dependencies || {};
  367. return (
  368. typeof dependencies.react !== 'undefined' &&
  369. typeof dependencies['react-dom'] !== 'undefined'
  370. );
  371. }