UNPKG

10.5 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 (ownPackage.optionalDependencies[key]) {
191 return;
192 }
193 console.log(` Adding ${cyan(key)} to dependencies`);
194 appPackage.dependencies[key] = ownPackage.dependencies[key];
195 });
196 // Sort the deps
197 const unsortedDependencies = appPackage.dependencies;
198 appPackage.dependencies = {};
199 Object.keys(unsortedDependencies)
200 .sort()
201 .forEach(key => {
202 appPackage.dependencies[key] = unsortedDependencies[key];
203 });
204 console.log();
205
206 console.log(cyan('Updating the scripts'));
207 delete appPackage.scripts['eject'];
208 Object.keys(appPackage.scripts).forEach(key => {
209 Object.keys(ownPackage.bin).forEach(binKey => {
210 const regex = new RegExp(binKey + ' (\\w+)', 'g');
211 if (!regex.test(appPackage.scripts[key])) {
212 return;
213 }
214 appPackage.scripts[key] = appPackage.scripts[key].replace(
215 regex,
216 'node scripts/$1.js'
217 );
218 console.log(
219 ` Replacing ${cyan(`"${binKey} ${key}"`)} with ${cyan(
220 `"node scripts/${key}.js"`
221 )}`
222 );
223 });
224 });
225
226 console.log();
227 console.log(cyan('Configuring package.json'));
228 // Add Jest config
229 console.log(` Adding ${cyan('Jest')} configuration`);
230 appPackage.jest = jestConfig;
231
232 // Add Babel config
233 console.log(` Adding ${cyan('Babel')} preset`);
234 appPackage.babel = {
235 presets: ['react-app'],
236 };
237
238 // Add ESlint config
239 console.log(` Adding ${cyan('ESLint')} configuration`);
240 appPackage.eslintConfig = {
241 extends: 'react-app',
242 };
243
244 fs.writeFileSync(
245 path.join(appPath, 'package.json'),
246 JSON.stringify(appPackage, null, 2) + os.EOL
247 );
248 console.log();
249
250 if (fs.existsSync(paths.appTypeDeclarations)) {
251 try {
252 // Read app declarations file
253 let content = fs.readFileSync(paths.appTypeDeclarations, 'utf8');
254 const ownContent =
255 fs.readFileSync(paths.ownTypeDeclarations, 'utf8').trim() + os.EOL;
256
257 // Remove react-scripts reference since they're getting a copy of the types in their project
258 content =
259 content
260 // Remove react-scripts types
261 .replace(
262 /^\s*\/\/\/\s*<reference\s+types.+?"react-scripts".*\/>.*(?:\n|$)/gm,
263 ''
264 )
265 .trim() + os.EOL;
266
267 fs.writeFileSync(
268 paths.appTypeDeclarations,
269 (ownContent + os.EOL + content).trim() + os.EOL
270 );
271 } catch (e) {
272 // It's not essential that this succeeds, the TypeScript user should
273 // be able to re-create these types with ease.
274 }
275 }
276
277 // "Don't destroy what isn't ours"
278 if (ownPath.indexOf(appPath) === 0) {
279 try {
280 // remove react-scripts and react-scripts binaries from app node_modules
281 Object.keys(ownPackage.bin).forEach(binKey => {
282 fs.removeSync(path.join(appPath, 'node_modules', '.bin', binKey));
283 });
284 fs.removeSync(ownPath);
285 } catch (e) {
286 // It's not essential that this succeeds
287 }
288 }
289
290 if (fs.existsSync(paths.yarnLockFile)) {
291 const windowsCmdFilePath = path.join(
292 appPath,
293 'node_modules',
294 '.bin',
295 'react-scripts.cmd'
296 );
297 let windowsCmdFileContent;
298 if (process.platform === 'win32') {
299 // https://github.com/facebook/create-react-app/pull/3806#issuecomment-357781035
300 // Yarn is diligent about cleaning up after itself, but this causes the react-scripts.cmd file
301 // to be deleted while it is running. This trips Windows up after the eject completes.
302 // We'll read the batch file and later "write it back" to match npm behavior.
303 try {
304 windowsCmdFileContent = fs.readFileSync(windowsCmdFilePath);
305 } catch (err) {
306 // If this fails we're not worse off than if we didn't try to fix it.
307 }
308 }
309
310 console.log(cyan('Running yarn...'));
311 spawnSync('yarnpkg', ['--cwd', process.cwd()], { stdio: 'inherit' });
312
313 if (windowsCmdFileContent && !fs.existsSync(windowsCmdFilePath)) {
314 try {
315 fs.writeFileSync(windowsCmdFilePath, windowsCmdFileContent);
316 } catch (err) {
317 // If this fails we're not worse off than if we didn't try to fix it.
318 }
319 }
320 } else {
321 console.log(cyan('Running npm install...'));
322 spawnSync('npm', ['install', '--loglevel', 'error'], {
323 stdio: 'inherit',
324 });
325 }
326 console.log(green('Ejected successfully!'));
327 console.log();
328
329 if (tryGitAdd(appPath)) {
330 console.log(cyan('Staged ejected files for commit.'));
331 console.log();
332 }
333
334 console.log(
335 green('Please consider sharing why you ejected in this survey:')
336 );
337 console.log(green(' http://goo.gl/forms/Bi6CZjk1EqsdelXk1'));
338 console.log();
339 });