UNPKG

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