eject.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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 prompts = require('prompts');
  18. const execSync = require('child_process').execSync;
  19. const chalk = require('react-dev-utils/chalk');
  20. const paths = require('../config/paths');
  21. const createJestConfig = require('./utils/createJestConfig');
  22. const spawnSync = require('react-dev-utils/crossSpawn').sync;
  23. const os = require('os');
  24. const green = chalk.green;
  25. const cyan = chalk.cyan;
  26. function getGitStatus() {
  27. try {
  28. let stdout = execSync(`git status --porcelain`, {
  29. stdio: ['pipe', 'pipe', 'ignore'],
  30. }).toString();
  31. return stdout.trim();
  32. } catch (e) {
  33. return '';
  34. }
  35. }
  36. function tryGitAdd(appPath) {
  37. try {
  38. spawnSync(
  39. 'git',
  40. ['add', path.join(appPath, 'config'), path.join(appPath, 'scripts')],
  41. {
  42. stdio: 'inherit',
  43. }
  44. );
  45. return true;
  46. } catch (e) {
  47. return false;
  48. }
  49. }
  50. console.log(
  51. chalk.cyan.bold(
  52. 'NOTE: Create React App 2+ supports TypeScript, Sass, CSS Modules and more without ejecting: ' +
  53. 'https://reactjs.org/blog/2018/10/01/create-react-app-v2.html'
  54. )
  55. );
  56. console.log();
  57. prompts({
  58. type: 'confirm',
  59. name: 'shouldEject',
  60. message: 'Are you sure you want to eject? This action is permanent.',
  61. initial: false,
  62. }).then(answer => {
  63. if (!answer.shouldEject) {
  64. console.log(cyan('Close one! Eject aborted.'));
  65. return;
  66. }
  67. const gitStatus = getGitStatus();
  68. if (gitStatus) {
  69. console.error(
  70. chalk.red(
  71. 'This git repository has untracked files or uncommitted changes:'
  72. ) +
  73. '\n\n' +
  74. gitStatus
  75. .split('\n')
  76. .map(line => line.match(/ .*/g)[0].trim())
  77. .join('\n') +
  78. '\n\n' +
  79. chalk.red(
  80. 'Remove untracked files, stash or commit any changes, and try again.'
  81. )
  82. );
  83. process.exit(1);
  84. }
  85. console.log('Ejecting...');
  86. const ownPath = paths.ownPath;
  87. const appPath = paths.appPath;
  88. function verifyAbsent(file) {
  89. if (fs.existsSync(path.join(appPath, file))) {
  90. console.error(
  91. `\`${file}\` already exists in your app folder. We cannot ` +
  92. 'continue as you would lose all the changes in that file or directory. ' +
  93. 'Please move or delete it (maybe make a copy for backup) and run this ' +
  94. 'command again.'
  95. );
  96. process.exit(1);
  97. }
  98. }
  99. const folders = ['config', 'config/jest', 'scripts'];
  100. // Make shallow array of files paths
  101. const files = folders.reduce((files, folder) => {
  102. return files.concat(
  103. fs
  104. .readdirSync(path.join(ownPath, folder))
  105. // set full path
  106. .map(file => path.join(ownPath, folder, file))
  107. // omit dirs from file list
  108. .filter(file => fs.lstatSync(file).isFile())
  109. );
  110. }, []);
  111. // Ensure that the app folder is clean and we won't override any files
  112. folders.forEach(verifyAbsent);
  113. files.forEach(verifyAbsent);
  114. // Prepare Jest config early in case it throws
  115. const jestConfig = createJestConfig(
  116. filePath => path.posix.join('<rootDir>', filePath),
  117. null,
  118. true
  119. );
  120. console.log();
  121. console.log(cyan(`Copying files into ${appPath}`));
  122. folders.forEach(folder => {
  123. fs.mkdirSync(path.join(appPath, folder));
  124. });
  125. files.forEach(file => {
  126. let content = fs.readFileSync(file, 'utf8');
  127. // Skip flagged files
  128. if (content.match(/\/\/ @remove-file-on-eject/)) {
  129. return;
  130. }
  131. content =
  132. content
  133. // Remove dead code from .js files on eject
  134. .replace(
  135. /\/\/ @remove-on-eject-begin([\s\S]*?)\/\/ @remove-on-eject-end/gm,
  136. ''
  137. )
  138. // Remove dead code from .applescript files on eject
  139. .replace(
  140. /-- @remove-on-eject-begin([\s\S]*?)-- @remove-on-eject-end/gm,
  141. ''
  142. )
  143. .trim() + '\n';
  144. console.log(` Adding ${cyan(file.replace(ownPath, ''))} to the project`);
  145. fs.writeFileSync(file.replace(ownPath, appPath), content);
  146. });
  147. console.log();
  148. const ownPackage = require(path.join(ownPath, 'package.json'));
  149. const appPackage = require(path.join(appPath, 'package.json'));
  150. console.log(cyan('Updating the dependencies'));
  151. const ownPackageName = ownPackage.name;
  152. if (appPackage.devDependencies) {
  153. // We used to put react-scripts in devDependencies
  154. if (appPackage.devDependencies[ownPackageName]) {
  155. console.log(` Removing ${cyan(ownPackageName)} from devDependencies`);
  156. delete appPackage.devDependencies[ownPackageName];
  157. }
  158. }
  159. appPackage.dependencies = appPackage.dependencies || {};
  160. if (appPackage.dependencies[ownPackageName]) {
  161. console.log(` Removing ${cyan(ownPackageName)} from dependencies`);
  162. delete appPackage.dependencies[ownPackageName];
  163. }
  164. Object.keys(ownPackage.dependencies).forEach(key => {
  165. // For some reason optionalDependencies end up in dependencies after install
  166. if (
  167. ownPackage.optionalDependencies &&
  168. ownPackage.optionalDependencies[key]
  169. ) {
  170. return;
  171. }
  172. console.log(` Adding ${cyan(key)} to dependencies`);
  173. appPackage.dependencies[key] = ownPackage.dependencies[key];
  174. });
  175. // Sort the deps
  176. const unsortedDependencies = appPackage.dependencies;
  177. appPackage.dependencies = {};
  178. Object.keys(unsortedDependencies)
  179. .sort()
  180. .forEach(key => {
  181. appPackage.dependencies[key] = unsortedDependencies[key];
  182. });
  183. console.log();
  184. console.log(cyan('Updating the scripts'));
  185. delete appPackage.scripts['eject'];
  186. Object.keys(appPackage.scripts).forEach(key => {
  187. Object.keys(ownPackage.bin).forEach(binKey => {
  188. const regex = new RegExp(binKey + ' (\\w+)', 'g');
  189. if (!regex.test(appPackage.scripts[key])) {
  190. return;
  191. }
  192. appPackage.scripts[key] = appPackage.scripts[key].replace(
  193. regex,
  194. 'node scripts/$1.js'
  195. );
  196. console.log(
  197. ` Replacing ${cyan(`"${binKey} ${key}"`)} with ${cyan(
  198. `"node scripts/${key}.js"`
  199. )}`
  200. );
  201. });
  202. });
  203. console.log();
  204. console.log(cyan('Configuring package.json'));
  205. // Add Jest config
  206. console.log(` Adding ${cyan('Jest')} configuration`);
  207. appPackage.jest = jestConfig;
  208. // Add Babel config
  209. console.log(` Adding ${cyan('Babel')} preset`);
  210. appPackage.babel = {
  211. presets: ['react-app'],
  212. };
  213. // Add ESlint config
  214. if (!appPackage.eslintConfig) {
  215. console.log(` Adding ${cyan('ESLint')} configuration`);
  216. appPackage.eslintConfig = {
  217. extends: 'react-app',
  218. };
  219. }
  220. fs.writeFileSync(
  221. path.join(appPath, 'package.json'),
  222. JSON.stringify(appPackage, null, 2) + os.EOL
  223. );
  224. console.log();
  225. if (fs.existsSync(paths.appTypeDeclarations)) {
  226. try {
  227. // Read app declarations file
  228. let content = fs.readFileSync(paths.appTypeDeclarations, 'utf8');
  229. const ownContent =
  230. fs.readFileSync(paths.ownTypeDeclarations, 'utf8').trim() + os.EOL;
  231. // Remove react-scripts reference since they're getting a copy of the types in their project
  232. content =
  233. content
  234. // Remove react-scripts types
  235. .replace(
  236. /^\s*\/\/\/\s*<reference\s+types.+?"react-scripts".*\/>.*(?:\n|$)/gm,
  237. ''
  238. )
  239. .trim() + os.EOL;
  240. fs.writeFileSync(
  241. paths.appTypeDeclarations,
  242. (ownContent + os.EOL + content).trim() + os.EOL
  243. );
  244. } catch (e) {
  245. // It's not essential that this succeeds, the TypeScript user should
  246. // be able to re-create these types with ease.
  247. }
  248. }
  249. // "Don't destroy what isn't ours"
  250. if (ownPath.indexOf(appPath) === 0) {
  251. try {
  252. // remove react-scripts and react-scripts binaries from app node_modules
  253. Object.keys(ownPackage.bin).forEach(binKey => {
  254. fs.removeSync(path.join(appPath, 'node_modules', '.bin', binKey));
  255. });
  256. fs.removeSync(ownPath);
  257. } catch (e) {
  258. // It's not essential that this succeeds
  259. }
  260. }
  261. if (fs.existsSync(paths.yarnLockFile)) {
  262. const windowsCmdFilePath = path.join(
  263. appPath,
  264. 'node_modules',
  265. '.bin',
  266. 'react-scripts.cmd'
  267. );
  268. let windowsCmdFileContent;
  269. if (process.platform === 'win32') {
  270. // https://github.com/facebook/create-react-app/pull/3806#issuecomment-357781035
  271. // Yarn is diligent about cleaning up after itself, but this causes the react-scripts.cmd file
  272. // to be deleted while it is running. This trips Windows up after the eject completes.
  273. // We'll read the batch file and later "write it back" to match npm behavior.
  274. try {
  275. windowsCmdFileContent = fs.readFileSync(windowsCmdFilePath);
  276. } catch (err) {
  277. // If this fails we're not worse off than if we didn't try to fix it.
  278. }
  279. }
  280. console.log(cyan('Running yarn...'));
  281. spawnSync('yarnpkg', ['--cwd', process.cwd()], { stdio: 'inherit' });
  282. if (windowsCmdFileContent && !fs.existsSync(windowsCmdFilePath)) {
  283. try {
  284. fs.writeFileSync(windowsCmdFilePath, windowsCmdFileContent);
  285. } catch (err) {
  286. // If this fails we're not worse off than if we didn't try to fix it.
  287. }
  288. }
  289. } else {
  290. console.log(cyan('Running npm install...'));
  291. spawnSync('npm', ['install', '--loglevel', 'error'], {
  292. stdio: 'inherit',
  293. });
  294. }
  295. console.log(green('Ejected successfully!'));
  296. console.log();
  297. if (tryGitAdd(appPath)) {
  298. console.log(cyan('Staged ejected files for commit.'));
  299. console.log();
  300. }
  301. console.log(green('Please consider sharing why you ejected in this survey:'));
  302. console.log(green(' http://goo.gl/forms/Bi6CZjk1EqsdelXk1'));
  303. console.log();
  304. });