UNPKG

11.8 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 chalk = require('react-dev-utils/chalk');
20const execSync = require('child_process').execSync;
21const spawn = require('react-dev-utils/crossSpawn');
22const { defaultBrowsers } = require('react-dev-utils/browsersHelper');
23const os = require('os');
24const verifyTypeScriptSetup = require('./utils/verifyTypeScriptSetup');
25
26function isInGitRepository() {
27 try {
28 execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
29 return true;
30 } catch (e) {
31 return false;
32 }
33}
34
35function isInMercurialRepository() {
36 try {
37 execSync('hg --cwd . root', { stdio: 'ignore' });
38 return true;
39 } catch (e) {
40 return false;
41 }
42}
43
44function tryGitInit() {
45 try {
46 execSync('git --version', { stdio: 'ignore' });
47 if (isInGitRepository() || isInMercurialRepository()) {
48 return false;
49 }
50
51 execSync('git init', { stdio: 'ignore' });
52 return true;
53 } catch (e) {
54 console.warn('Git repo not initialized', e);
55 return false;
56 }
57}
58
59function tryGitCommit(appPath) {
60 try {
61 execSync('git add -A', { stdio: 'ignore' });
62 execSync('git commit -m "Initialize project using Create React App"', {
63 stdio: 'ignore',
64 });
65 return true;
66 } catch (e) {
67 // We couldn't commit in already initialized git repo,
68 // maybe the commit author config is not set.
69 // In the future, we might supply our own committer
70 // like Ember CLI does, but for now, let's just
71 // remove the Git files to avoid a half-done state.
72 console.warn('Git commit not created', e);
73 console.warn('Removing .git directory...');
74 try {
75 // unlinkSync() doesn't work on directories.
76 fs.removeSync(path.join(appPath, '.git'));
77 } catch (removeErr) {
78 // Ignore.
79 }
80 return false;
81 }
82}
83
84module.exports = function (
85 appPath,
86 appName,
87 verbose,
88 originalDirectory,
89 templateName
90) {
91 const appPackage = require(path.join(appPath, 'package.json'));
92 const useYarn = fs.existsSync(path.join(appPath, 'yarn.lock'));
93
94 if (!templateName) {
95 console.log('');
96 console.error(
97 `A template was not provided. This is likely because you're using an outdated version of ${chalk.cyan(
98 'create-react-app'
99 )}.`
100 );
101 console.error(
102 `Please note that global installs of ${chalk.cyan(
103 'create-react-app'
104 )} are no longer supported.`
105 );
106 console.error(
107 `You can fix this by running ${chalk.cyan(
108 'npm uninstall -g create-react-app'
109 )} or ${chalk.cyan(
110 'yarn global remove create-react-app'
111 )} before using ${chalk.cyan('create-react-app')} again.`
112 );
113 return;
114 }
115
116 const templatePath = path.dirname(
117 require.resolve(`${templateName}/package.json`, { paths: [appPath] })
118 );
119
120 const templateJsonPath = path.join(templatePath, 'template.json');
121
122 let templateJson = {};
123 if (fs.existsSync(templateJsonPath)) {
124 templateJson = require(templateJsonPath);
125 }
126
127 const templatePackage = templateJson.package || {};
128
129 // TODO: Deprecate support for root-level `dependencies` and `scripts` in v5.
130 // These should now be set under the `package` key.
131 if (templateJson.dependencies || templateJson.scripts) {
132 console.log();
133 console.log(
134 chalk.yellow(
135 'Root-level `dependencies` and `scripts` keys in `template.json` are deprecated.\n' +
136 'This template should be updated to use the new `package` key.'
137 )
138 );
139 console.log('For more information, visit https://cra.link/templates');
140 }
141 if (templateJson.dependencies) {
142 templatePackage.dependencies = templateJson.dependencies;
143 }
144 if (templateJson.scripts) {
145 templatePackage.scripts = templateJson.scripts;
146 }
147
148 // Keys to ignore in templatePackage
149 const templatePackageBlacklist = [
150 'name',
151 'version',
152 'description',
153 'keywords',
154 'bugs',
155 'license',
156 'author',
157 'contributors',
158 'files',
159 'browser',
160 'bin',
161 'man',
162 'directories',
163 'repository',
164 'peerDependencies',
165 'bundledDependencies',
166 'optionalDependencies',
167 'engineStrict',
168 'os',
169 'cpu',
170 'preferGlobal',
171 'private',
172 'publishConfig',
173 ];
174
175 // Keys from templatePackage that will be merged with appPackage
176 const templatePackageToMerge = ['dependencies', 'scripts'];
177
178 // Keys from templatePackage that will be added to appPackage,
179 // replacing any existing entries.
180 const templatePackageToReplace = Object.keys(templatePackage).filter(key => {
181 return (
182 !templatePackageBlacklist.includes(key) &&
183 !templatePackageToMerge.includes(key)
184 );
185 });
186
187 // Copy over some of the devDependencies
188 appPackage.dependencies = appPackage.dependencies || {};
189
190 // Setup the script rules
191 const templateScripts = templatePackage.scripts || {};
192 appPackage.scripts = Object.assign(
193 {
194 start: 'react-scripts start',
195 build: 'react-scripts build',
196 test: 'react-scripts test',
197 eject: 'react-scripts eject',
198 },
199 templateScripts
200 );
201
202 // Update scripts for Yarn users
203 if (useYarn) {
204 appPackage.scripts = Object.entries(appPackage.scripts).reduce(
205 (acc, [key, value]) => ({
206 ...acc,
207 [key]: value.replace(/(npm run |npm )/, 'yarn '),
208 }),
209 {}
210 );
211 }
212
213 // Setup the eslint config
214 appPackage.eslintConfig = {
215 extends: 'react-app',
216 };
217
218 // Setup the browsers list
219 appPackage.browserslist = defaultBrowsers;
220
221 // Add templatePackage keys/values to appPackage, replacing existing entries
222 templatePackageToReplace.forEach(key => {
223 appPackage[key] = templatePackage[key];
224 });
225
226 fs.writeFileSync(
227 path.join(appPath, 'package.json'),
228 JSON.stringify(appPackage, null, 2) + os.EOL
229 );
230
231 const readmeExists = fs.existsSync(path.join(appPath, 'README.md'));
232 if (readmeExists) {
233 fs.renameSync(
234 path.join(appPath, 'README.md'),
235 path.join(appPath, 'README.old.md')
236 );
237 }
238
239 // Copy the files for the user
240 const templateDir = path.join(templatePath, 'template');
241 if (fs.existsSync(templateDir)) {
242 fs.copySync(templateDir, appPath);
243 } else {
244 console.error(
245 `Could not locate supplied template: ${chalk.green(templateDir)}`
246 );
247 return;
248 }
249
250 // modifies README.md commands based on user used package manager.
251 if (useYarn) {
252 try {
253 const readme = fs.readFileSync(path.join(appPath, 'README.md'), 'utf8');
254 fs.writeFileSync(
255 path.join(appPath, 'README.md'),
256 readme.replace(/(npm run |npm )/g, 'yarn '),
257 'utf8'
258 );
259 } catch (err) {
260 // Silencing the error. As it fall backs to using default npm commands.
261 }
262 }
263
264 const gitignoreExists = fs.existsSync(path.join(appPath, '.gitignore'));
265 if (gitignoreExists) {
266 // Append if there's already a `.gitignore` file there
267 const data = fs.readFileSync(path.join(appPath, 'gitignore'));
268 fs.appendFileSync(path.join(appPath, '.gitignore'), data);
269 fs.unlinkSync(path.join(appPath, 'gitignore'));
270 } else {
271 // Rename gitignore after the fact to prevent npm from renaming it to .npmignore
272 // See: https://github.com/npm/npm/issues/1862
273 fs.moveSync(
274 path.join(appPath, 'gitignore'),
275 path.join(appPath, '.gitignore'),
276 []
277 );
278 }
279
280 // Initialize git repo
281 let initializedGit = false;
282
283 if (tryGitInit()) {
284 initializedGit = true;
285 console.log();
286 console.log('Initialized a git repository.');
287 }
288
289 let command;
290 let remove;
291 let args;
292
293 if (useYarn) {
294 command = 'yarnpkg';
295 remove = 'remove';
296 args = ['add'];
297 } else {
298 command = 'npm';
299 remove = 'uninstall';
300 args = ['install', '--save', verbose && '--verbose'].filter(e => e);
301 }
302
303 // Install additional template dependencies, if present.
304 const dependenciesToInstall = Object.entries({
305 ...templatePackage.dependencies,
306 ...templatePackage.devDependencies,
307 });
308 if (dependenciesToInstall.length) {
309 args = args.concat(
310 dependenciesToInstall.map(([dependency, version]) => {
311 return `${dependency}@${version}`;
312 })
313 );
314 }
315
316 // Install react and react-dom for backward compatibility with old CRA cli
317 // which doesn't install react and react-dom along with react-scripts
318 if (!isReactInstalled(appPackage)) {
319 args = args.concat(['react', 'react-dom']);
320 }
321
322 // Install template dependencies, and react and react-dom if missing.
323 if ((!isReactInstalled(appPackage) || templateName) && args.length > 1) {
324 console.log();
325 console.log(`Installing template dependencies using ${command}...`);
326
327 const proc = spawn.sync(command, args, { stdio: 'inherit' });
328 if (proc.status !== 0) {
329 console.error(`\`${command} ${args.join(' ')}\` failed`);
330 return;
331 }
332 }
333
334 if (args.find(arg => arg.includes('typescript'))) {
335 console.log();
336 verifyTypeScriptSetup();
337 }
338
339 // Remove template
340 console.log(`Removing template package using ${command}...`);
341 console.log();
342
343 const proc = spawn.sync(command, [remove, templateName], {
344 stdio: 'inherit',
345 });
346 if (proc.status !== 0) {
347 console.error(`\`${command} ${args.join(' ')}\` failed`);
348 return;
349 }
350
351 // Create git commit if git repo was initialized
352 if (initializedGit && tryGitCommit(appPath)) {
353 console.log();
354 console.log('Created git commit.');
355 }
356
357 // Display the most elegant way to cd.
358 // This needs to handle an undefined originalDirectory for
359 // backward compatibility with old global-cli's.
360 let cdpath;
361 if (originalDirectory && path.join(originalDirectory, appName) === appPath) {
362 cdpath = appName;
363 } else {
364 cdpath = appPath;
365 }
366
367 // Change displayed command to yarn instead of yarnpkg
368 const displayedCommand = useYarn ? 'yarn' : 'npm';
369
370 console.log();
371 console.log(`Success! Created ${appName} at ${appPath}`);
372 console.log('Inside that directory, you can run several commands:');
373 console.log();
374 console.log(chalk.cyan(` ${displayedCommand} start`));
375 console.log(' Starts the development server.');
376 console.log();
377 console.log(
378 chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}build`)
379 );
380 console.log(' Bundles the app into static files for production.');
381 console.log();
382 console.log(chalk.cyan(` ${displayedCommand} test`));
383 console.log(' Starts the test runner.');
384 console.log();
385 console.log(
386 chalk.cyan(` ${displayedCommand} ${useYarn ? '' : 'run '}eject`)
387 );
388 console.log(
389 ' Removes this tool and copies build dependencies, configuration files'
390 );
391 console.log(
392 ' and scripts into the app directory. If you do this, you can’t go back!'
393 );
394 console.log();
395 console.log('We suggest that you begin by typing:');
396 console.log();
397 console.log(chalk.cyan(' cd'), cdpath);
398 console.log(` ${chalk.cyan(`${displayedCommand} start`)}`);
399 if (readmeExists) {
400 console.log();
401 console.log(
402 chalk.yellow(
403 'You had a `README.md` file, we renamed it to `README.old.md`'
404 )
405 );
406 }
407 console.log();
408 console.log('Happy hacking!');
409};
410
411function isReactInstalled(appPackage) {
412 const dependencies = appPackage.dependencies || {};
413
414 return (
415 typeof dependencies.react !== 'undefined' &&
416 typeof dependencies['react-dom'] !== 'undefined'
417 );
418}