UNPKG

21.3 kBJavaScriptView Raw
1'use strict';
2
3const validateProjectName = require('validate-npm-package-name');
4const chalk = require('chalk');
5const commander = require('commander');
6const fs = require('fs-extra');
7const path = require('path');
8const execSync = require('child_process').execSync;
9const spawn = require('cross-spawn');
10const semver = require('semver');
11const dns = require('dns');
12const tmp = require('tmp');
13const unpack = require('tar-pack').unpack;
14const url = require('url');
15const hyperquest = require('hyperquest');
16const envinfo = require('envinfo');
17const os = require('os');
18const packageJson = require('./package.json');
19
20// These files should be allowed to remain on a failed install,
21// but then silently removed during the next create.
22const errorLogFilePatterns = [
23 'npm-debug.log',
24 'yarn-error.log',
25 'yarn-debug.log',
26];
27
28let projectName;
29
30const program = new commander.Command(packageJson.name)
31 .version(packageJson.version)
32 .arguments('<project-directory>')
33 .usage(`${chalk.green('<project-directory>')} [options]`)
34 .action(name => {
35 projectName = name;
36 })
37 .option('--verbose', 'print additional logs')
38 .option('--info', 'print environment debug info')
39 .option(
40 '--scripts-version <alternative-package>',
41 'use a non-standard version of react-scripts'
42 )
43 .option('--use-npm')
44 .allowUnknownOption()
45 .on('--help', () => {
46 console.log(` Only ${chalk.green('<project-directory>')} is required.`);
47 console.log();
48 console.log(
49 ` A custom ${chalk.cyan('--scripts-version')} can be one of:`
50 );
51 console.log(` - a specific npm version: ${chalk.green('0.8.2')}`);
52 console.log(` - a specific npm tag: ${chalk.green('@next')}`);
53 console.log(
54 ` - a custom fork published on npm: ${chalk.green(
55 'my-react-scripts'
56 )}`
57 );
58 console.log(
59 ` - a local path relative to the current working directory: ${chalk.green(
60 'file:../my-react-scripts'
61 )}`
62 );
63 console.log(
64 ` - a .tgz archive: ${chalk.green(
65 'https://mysite.com/my-react-scripts-0.8.2.tgz'
66 )}`
67 );
68 console.log(
69 ` - a .tar.gz archive: ${chalk.green(
70 'https://mysite.com/my-react-scripts-0.8.2.tar.gz'
71 )}`
72 );
73 console.log(
74 ` It is not needed unless you specifically want to use a fork.`
75 );
76 console.log();
77 console.log(
78 ` If you have any problems, do not hesitate to file an issue:`
79 );
80 console.log(
81 ` ${chalk.cyan(
82 'https://github.com/facebook/create-react-app/issues/new'
83 )}`
84 );
85 console.log();
86 })
87 .parse(process.argv);
88
89if (program.info) {
90 console.log(chalk.bold('\nEnvironment Info:'));
91 return envinfo
92 .run(
93 {
94 System: ['OS', 'CPU'],
95 Binaries: ['Node', 'npm', 'Yarn'],
96 Browsers: ['Chrome', 'Edge', 'Internet Explorer', 'Firefox', 'Safari'],
97 npmPackages: ['react', 'react-dom', 'react-scripts'],
98 npmGlobalPackages: ['create-react-app'],
99 },
100 {
101 clipboard: true,
102 duplicates: true,
103 showNotFound: true,
104 }
105 )
106 .then(console.log)
107 .then(() => console.log(chalk.green('Copied To Clipboard!\n')));
108}
109
110if (typeof projectName === 'undefined') {
111 console.error('Please specify the project directory:');
112 console.log(
113 ` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}`
114 );
115 console.log();
116 console.log('For example:');
117 console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`);
118 console.log();
119 console.log(
120 `Run ${chalk.cyan(`${program.name()} --help`)} to see all options.`
121 );
122 process.exit(1);
123}
124
125function printValidationResults(results) {
126 if (typeof results !== 'undefined') {
127 results.forEach(error => {
128 console.error(chalk.red(` * ${error}`));
129 });
130 }
131}
132
133const hiddenProgram = new commander.Command()
134 .option(
135 '--internal-testing-template <path-to-template>',
136 '(internal usage only, DO NOT RELY ON THIS) ' +
137 'use a non-standard application template'
138 )
139 .parse(process.argv);
140
141createApp(
142 projectName,
143 program.verbose,
144 program.scriptsVersion,
145 program.useNpm,
146 hiddenProgram.internalTestingTemplate
147);
148
149function createApp(name, verbose, version, useNpm, template) {
150 const root = path.resolve(name);
151 const appName = path.basename(root);
152
153 checkAppName(appName);
154 fs.ensureDirSync(name);
155 if (!isSafeToCreateProjectIn(root, name)) {
156 process.exit(1);
157 }
158
159 console.log(`Creating a new mogul app in ${chalk.green(root)}.`);
160 console.log();
161
162 const packageJson = {
163 name: appName,
164 version: '0.1.0',
165 private: true,
166 };
167 fs.writeFileSync(
168 path.join(root, 'package.json'),
169 JSON.stringify(packageJson, null, 2) + os.EOL
170 );
171
172 const useYarn = useNpm ? false : shouldUseYarn(root);
173 const originalDirectory = process.cwd();
174 process.chdir(root);
175 if (!useYarn && !checkThatNpmCanReadCwd()) {
176 process.exit(1);
177 }
178
179 run(root, appName, version, verbose, originalDirectory, template, useYarn);
180}
181
182function isYarnAvailable() {
183 try {
184 execSync('yarnpkg --version', { stdio: 'ignore' });
185 return true;
186 } catch (e) {
187 return false;
188 }
189}
190
191function shouldUseYarn(appDir) {
192 return isYarnAvailable()
193}
194
195function install(root, useYarn, dependencies, verbose, isOnline) {
196 return new Promise((resolve, reject) => {
197 let command;
198 let args;
199 if (useYarn) {
200 command = 'yarnpkg';
201 args = ['add', '--exact'];
202 if (!isOnline) {
203 args.push('--offline');
204 }
205 [].push.apply(args, dependencies);
206
207 // Explicitly set cwd() to work around issues like
208 // https://github.com/facebook/create-react-app/issues/3326.
209 // Unfortunately we can only do this for Yarn because npm support for
210 // equivalent --prefix flag doesn't help with this issue.
211 // This is why for npm, we run checkThatNpmCanReadCwd() early instead.
212 args.push('--cwd');
213 args.push(root);
214
215 if (!isOnline) {
216 console.log(chalk.yellow('You appear to be offline.'));
217 console.log(chalk.yellow('Falling back to the local Yarn cache.'));
218 console.log();
219 }
220 } else {
221 command = 'npm';
222 args = [
223 'install',
224 '--save',
225 '--save-exact',
226 '--loglevel',
227 'error',
228 ].concat(dependencies);
229 }
230
231 if (verbose) {
232 args.push('--verbose');
233 }
234
235 const child = spawn(command, args, { stdio: 'inherit' });
236 child.on('close', code => {
237 if (code !== 0) {
238 reject({
239 command: `${command} ${args.join(' ')}`,
240 });
241 return;
242 }
243 resolve();
244 });
245 });
246}
247
248function run(
249 root,
250 appName,
251 version,
252 verbose,
253 originalDirectory,
254 template,
255 useYarn
256) {
257 const packageToInstall = getInstallPackage(version, originalDirectory);
258 const allDependencies = ['react', 'react-dom', packageToInstall];
259
260 console.log('Installing packages. This might take a couple of minutes.');
261 getPackageName(packageToInstall)
262 .then(packageName =>
263 checkIfOnline(useYarn).then(isOnline => ({
264 isOnline: isOnline,
265 packageName: packageName,
266 }))
267 )
268 .then(info => {
269 const isOnline = info.isOnline;
270 const packageName = info.packageName;
271 console.log(
272 `Installing ${chalk.cyan('react')}, ${chalk.cyan(
273 'react-dom'
274 )}, and ${chalk.cyan(packageName)}...`
275 );
276 console.log();
277
278 return install(root, useYarn, allDependencies, verbose, isOnline).then(
279 () => packageName
280 );
281 })
282 .then(packageName => {
283 checkNodeVersion(packageName);
284 setCaretRangeForRuntimeDeps(packageName);
285
286 const scriptsPath = path.resolve(
287 process.cwd(),
288 'node_modules',
289 packageName,
290 'scripts',
291 'init.js'
292 );
293 const init = require(scriptsPath);
294 init(root, appName, verbose, originalDirectory, template);
295
296 })
297 .catch(reason => {
298 console.log();
299 console.log('Aborting installation.');
300 if (reason.command) {
301 console.log(` ${chalk.cyan(reason.command)} has failed.`);
302 } else {
303 console.log(chalk.red('Unexpected error. Please report it as a bug:'));
304 console.log(reason);
305 }
306 console.log();
307
308 // On 'exit' we will delete these files from target directory.
309 const knownGeneratedFiles = ['package.json', 'node_modules'];
310 const currentFiles = fs.readdirSync(path.join(root));
311 currentFiles.forEach(file => {
312 knownGeneratedFiles.forEach(fileToMatch => {
313 // This remove all of knownGeneratedFiles.
314 if (file === fileToMatch) {
315 console.log(`Deleting generated file... ${chalk.cyan(file)}`);
316 fs.removeSync(path.join(root, file));
317 }
318 });
319 });
320 const remainingFiles = fs.readdirSync(path.join(root));
321 if (!remainingFiles.length) {
322 // Delete target folder if empty
323 console.log(
324 `Deleting ${chalk.cyan(`${appName}/`)} from ${chalk.cyan(
325 path.resolve(root, '..')
326 )}`
327 );
328 process.chdir(path.resolve(root, '..'));
329 fs.removeSync(path.join(root));
330 }
331 console.log('Done.');
332 process.exit(1);
333 });
334}
335
336function getInstallPackage(version, originalDirectory) {
337 //当前使用最新版本的, TODO 设定版本的方式
338 return "@mogul/mogul-scripts"
339// let packageToInstall = 'react-scripts';
340// const validSemver = semver.valid(version);
341// if (validSemver) {
342// packageToInstall += `@${validSemver}`;
343// } else if (version) {
344// if (version[0] === '@' && version.indexOf('/') === -1) {
345// packageToInstall += version;
346// } else if (version.match(/^file:/)) {
347// packageToInstall = `file:${path.resolve(
348// originalDirectory,
349// version.match(/^file:(.*)?$/)[1]
350// )}`;
351// } else {
352// // for tar.gz or alternative paths
353// packageToInstall = version;
354// }
355// }
356// return packageToInstall;
357}
358
359function getTemporaryDirectory() {
360 return new Promise((resolve, reject) => {
361 // Unsafe cleanup lets us recursively delete the directory if it contains
362 // contents; by default it only allows removal if it's empty
363 tmp.dir({ unsafeCleanup: true }, (err, tmpdir, callback) => {
364 if (err) {
365 reject(err);
366 } else {
367 resolve({
368 tmpdir: tmpdir,
369 cleanup: () => {
370 try {
371 callback();
372 } catch (ignored) {
373 // Callback might throw and fail, since it's a temp directory the
374 // OS will clean it up eventually...
375 }
376 },
377 });
378 }
379 });
380 });
381}
382
383function extractStream(stream, dest) {
384 return new Promise((resolve, reject) => {
385 stream.pipe(
386 unpack(dest, err => {
387 if (err) {
388 reject(err);
389 } else {
390 resolve(dest);
391 }
392 })
393 );
394 });
395}
396
397// Extract package name from tarball url or path.
398function getPackageName(installPackage) {
399 if (installPackage.match(/^.+\.(tgz|tar\.gz)$/)) {
400 return getTemporaryDirectory()
401 .then(obj => {
402 let stream;
403 if (/^http/.test(installPackage)) {
404 stream = hyperquest(installPackage);
405 } else {
406 stream = fs.createReadStream(installPackage);
407 }
408 return extractStream(stream, obj.tmpdir).then(() => obj);
409 })
410 .then(obj => {
411 const packageName = require(path.join(obj.tmpdir, 'package.json')).name;
412 obj.cleanup();
413 return packageName;
414 })
415 .catch(err => {
416 // The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz
417 // However, this function returns package name only without semver version.
418 console.log(
419 `Could not extract the package name from the archive: ${err.message}`
420 );
421 const assumedProjectName = installPackage.match(
422 /^.+\/(.+?)(?:-\d+.+)?\.(tgz|tar\.gz)$/
423 )[1];
424 console.log(
425 `Based on the filename, assuming it is "${chalk.cyan(
426 assumedProjectName
427 )}"`
428 );
429 return Promise.resolve(assumedProjectName);
430 });
431 } else if (installPackage.indexOf('git+') === 0) {
432 // Pull package name out of git urls e.g:
433 // git+https://github.com/mycompany/react-scripts.git
434 // git+ssh://github.com/mycompany/react-scripts.git#v1.2.3
435 return Promise.resolve(installPackage.match(/([^/]+)\.git(#.*)?$/)[1]);
436 } else if (installPackage.match(/.+@/)) {
437 // Do not match @scope/ when stripping off @version or @tag
438 return Promise.resolve(
439 installPackage.charAt(0) + installPackage.substr(1).split('@')[0]
440 );
441 } else if (installPackage.match(/^file:/)) {
442 const installPackagePath = installPackage.match(/^file:(.*)?$/)[1];
443 const installPackageJson = require(path.join(
444 installPackagePath,
445 'package.json'
446 ));
447 return Promise.resolve(installPackageJson.name);
448 }
449 return Promise.resolve(installPackage);
450}
451
452function checkNpmVersion() {
453 let hasMinNpm = false;
454 let npmVersion = null;
455 try {
456 npmVersion = execSync('npm --version')
457 .toString()
458 .trim();
459 hasMinNpm = semver.gte(npmVersion, '3.0.0');
460 } catch (err) {
461 // ignore
462 }
463 return {
464 hasMinNpm: hasMinNpm,
465 npmVersion: npmVersion,
466 };
467}
468
469function checkNodeVersion(packageName) {
470 const packageJsonPath = path.resolve(
471 process.cwd(),
472 'node_modules',
473 packageName,
474 'package.json'
475 );
476 const packageJson = require(packageJsonPath);
477 if (!packageJson.engines || !packageJson.engines.node) {
478 return;
479 }
480
481 if (!semver.satisfies(process.version, packageJson.engines.node)) {
482 console.error(
483 chalk.red(
484 'You are running Node %s.\n' +
485 'Create React App requires Node %s or higher. \n' +
486 'Please update your version of Node.'
487 ),
488 process.version,
489 packageJson.engines.node
490 );
491 process.exit(1);
492 }
493}
494
495function checkAppName(appName) {
496 const validationResult = validateProjectName(appName);
497 if (!validationResult.validForNewPackages) {
498 console.error(
499 `Could not create a project called ${chalk.red(
500 `"${appName}"`
501 )} because of npm naming restrictions:`
502 );
503 printValidationResults(validationResult.errors);
504 printValidationResults(validationResult.warnings);
505 process.exit(1);
506 }
507
508 // TODO: there should be a single place that holds the dependencies
509 const dependencies = ['react', 'react-dom', 'react-scripts'].sort();
510 if (dependencies.indexOf(appName) >= 0) {
511 console.error(
512 chalk.red(
513 `We cannot create a project called ${chalk.green(
514 appName
515 )} because a dependency with the same name exists.\n` +
516 `Due to the way npm works, the following names are not allowed:\n\n`
517 ) +
518 chalk.cyan(dependencies.map(depName => ` ${depName}`).join('\n')) +
519 chalk.red('\n\nPlease choose a different project name.')
520 );
521 process.exit(1);
522 }
523}
524
525function makeCaretRange(dependencies, name) {
526 const version = dependencies[name];
527
528 if (typeof version === 'undefined') {
529 console.error(chalk.red(`Missing ${name} dependency in package.json`));
530 process.exit(1);
531 }
532
533 let patchedVersion = `^${version}`;
534
535 if (!semver.validRange(patchedVersion)) {
536 console.error(
537 `Unable to patch ${name} dependency version because version ${chalk.red(
538 version
539 )} will become invalid ${chalk.red(patchedVersion)}`
540 );
541 patchedVersion = version;
542 }
543
544 dependencies[name] = patchedVersion;
545}
546
547function setCaretRangeForRuntimeDeps(packageName) {
548 const packagePath = path.join(process.cwd(), 'package.json');
549 const packageJson = require(packagePath);
550
551 if (typeof packageJson.dependencies === 'undefined') {
552 console.error(chalk.red('Missing dependencies in package.json'));
553 process.exit(1);
554 }
555
556 const packageVersion = packageJson.dependencies[packageName];
557 if (typeof packageVersion === 'undefined') {
558 console.error(chalk.red(`Unable to find ${packageName} in package.json`));
559 process.exit(1);
560 }
561
562 makeCaretRange(packageJson.dependencies, 'react');
563 makeCaretRange(packageJson.dependencies, 'react-dom');
564
565 fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + os.EOL);
566}
567
568// If project only contains files generated by GH, it’s safe.
569// Also, if project contains remnant error logs from a previous
570// installation, lets remove them now.
571// We also special case IJ-based products .idea because it integrates with CRA:
572// https://github.com/facebook/create-react-app/pull/368#issuecomment-243446094
573function isSafeToCreateProjectIn(root, name) {
574 const validFiles = [
575 '.DS_Store',
576 'Thumbs.db',
577 '.git',
578 '.gitignore',
579 '.idea',
580 'README.md',
581 'LICENSE',
582 'web.iml',
583 '.hg',
584 '.hgignore',
585 '.hgcheck',
586 '.npmignore',
587 'mkdocs.yml',
588 'docs',
589 '.travis.yml',
590 '.gitlab-ci.yml',
591 '.gitattributes',
592 ];
593 console.log();
594
595 const conflicts = fs
596 .readdirSync(root)
597 .filter(file => !validFiles.includes(file))
598 // Don't treat log files from previous installation as conflicts
599 .filter(
600 file => !errorLogFilePatterns.some(pattern => file.indexOf(pattern) === 0)
601 );
602
603 if (conflicts.length > 0) {
604 console.log(
605 `The directory ${chalk.green(name)} contains files that could conflict:`
606 );
607 console.log();
608 for (const file of conflicts) {
609 console.log(` ${file}`);
610 }
611 console.log();
612 console.log(
613 'Either try using a new directory name, or remove the files listed above.'
614 );
615
616 return false;
617 }
618
619 // Remove any remnant files from a previous installation
620 const currentFiles = fs.readdirSync(path.join(root));
621 currentFiles.forEach(file => {
622 errorLogFilePatterns.forEach(errorLogFilePattern => {
623 // This will catch `(npm-debug|yarn-error|yarn-debug).log*` files
624 if (file.indexOf(errorLogFilePattern) === 0) {
625 fs.removeSync(path.join(root, file));
626 }
627 });
628 });
629 return true;
630}
631
632function getProxy() {
633 if (process.env.https_proxy) {
634 return process.env.https_proxy;
635 } else {
636 try {
637 // Trying to read https-proxy from .npmrc
638 let httpsProxy = execSync('npm config get https-proxy')
639 .toString()
640 .trim();
641 return httpsProxy !== 'null' ? httpsProxy : undefined;
642 } catch (e) {
643 return;
644 }
645 }
646}
647function checkThatNpmCanReadCwd() {
648 const cwd = process.cwd();
649 let childOutput = null;
650 try {
651 // Note: intentionally using spawn over exec since
652 // the problem doesn't reproduce otherwise.
653 // `npm config list` is the only reliable way I could find
654 // to reproduce the wrong path. Just printing process.cwd()
655 // in a Node process was not enough.
656 childOutput = spawn.sync('npm', ['config', 'list']).output.join('');
657 } catch (err) {
658 // Something went wrong spawning node.
659 // Not great, but it means we can't do this check.
660 // We might fail later on, but let's continue.
661 return true;
662 }
663 if (typeof childOutput !== 'string') {
664 return true;
665 }
666 const lines = childOutput.split('\n');
667 // `npm config list` output includes the following line:
668 // "; cwd = C:\path\to\current\dir" (unquoted)
669 // I couldn't find an easier way to get it.
670 const prefix = '; cwd = ';
671 const line = lines.find(line => line.indexOf(prefix) === 0);
672 if (typeof line !== 'string') {
673 // Fail gracefully. They could remove it.
674 return true;
675 }
676 const npmCWD = line.substring(prefix.length);
677 if (npmCWD === cwd) {
678 return true;
679 }
680 console.error(
681 chalk.red(
682 `Could not start an npm process in the right directory.\n\n` +
683 `The current directory is: ${chalk.bold(cwd)}\n` +
684 `However, a newly started npm process runs in: ${chalk.bold(
685 npmCWD
686 )}\n\n` +
687 `This is probably caused by a misconfigured system terminal shell.`
688 )
689 );
690 if (process.platform === 'win32') {
691 console.error(
692 chalk.red(`On Windows, this can usually be fixed by running:\n\n`) +
693 ` ${chalk.cyan(
694 'reg'
695 )} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
696 ` ${chalk.cyan(
697 'reg'
698 )} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
699 chalk.red(`Try to run the above two lines in the terminal.\n`) +
700 chalk.red(
701 `To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`
702 )
703 );
704 }
705 return false;
706}
707
708function checkIfOnline(useYarn) {
709 if (!useYarn) {
710 // Don't ping the Yarn registry.
711 // We'll just assume the best case.
712 return Promise.resolve(true);
713 }
714
715 return new Promise(resolve => {
716 dns.lookup('registry.yarnpkg.com', err => {
717 let proxy;
718 if (err != null && (proxy = getProxy())) {
719 // If a proxy is defined, we likely can't resolve external hostnames.
720 // Try to resolve the proxy name as an indication of a connection.
721 dns.lookup(url.parse(proxy).hostname, proxyErr => {
722 resolve(proxyErr == null);
723 });
724 } else {
725 resolve(err == null);
726 }
727 });
728 });
729}