UNPKG

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