Last active
February 14, 2019 06:55
-
-
Save gaearon/6d1177995ab63f95c0be696ad0314967 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) 2015-present, Facebook, Inc. | |
* | |
* This source code is licensed under the MIT license found in the | |
* LICENSE file in the root directory of this source tree. | |
*/ | |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
// /!\ DO NOT MODIFY THIS FILE /!\ | |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
// | |
// create-react-app is installed globally on people's computers. This means | |
// that it is extremely difficult to have them upgrade the version and | |
// because there's only one global version installed, it is very prone to | |
// breaking changes. | |
// | |
// The only job of create-react-app is to init the repository and then | |
// forward all the commands to the local version of create-react-app. | |
// | |
// If you need to add a new command, please add it to the scripts/ folder. | |
// | |
// The only reason to modify this file is to add more warnings and | |
// troubleshooting information for the `create-react-app` command. | |
// | |
// Do not make breaking changes! We absolutely don't want to have to | |
// tell people to update their global version of create-react-app. | |
// | |
// Also be careful with new language features. | |
// This file must work on Node 6+. | |
// | |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
// /!\ DO NOT MODIFY THIS FILE /!\ | |
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
'use strict'; | |
const validateProjectName = require('validate-npm-package-name'); | |
const chalk = require('chalk'); | |
const commander = require('commander'); | |
const fs = require('fs-extra'); | |
const path = require('path'); | |
const execSync = require('child_process').execSync; | |
const spawn = require('cross-spawn'); | |
const semver = require('semver'); | |
const dns = require('dns'); | |
const tmp = require('tmp'); | |
const unpack = require('tar-pack').unpack; | |
const url = require('url'); | |
const hyperquest = require('hyperquest'); | |
const packageJson = require('./package.json'); | |
let projectName; | |
const program = new commander.Command(packageJson.name) | |
.version(packageJson.version) | |
.arguments('<project-directory>') | |
.usage(`${chalk.green('<project-directory>')} [options]`) | |
.action(name => { | |
projectName = name; | |
}) | |
.option('--verbose', 'print additional logs') | |
.option( | |
'--scripts-version <alternative-package>', | |
'use a non-standard version of react-scripts' | |
) | |
.allowUnknownOption() | |
.on('--help', () => { | |
console.log(` Only ${chalk.green('<project-directory>')} is required.`); | |
console.log(); | |
console.log( | |
` A custom ${chalk.cyan('--scripts-version')} can be one of:` | |
); | |
console.log(` - a specific npm version: ${chalk.green('0.8.2')}`); | |
console.log( | |
` - a custom fork published on npm: ${chalk.green( | |
'my-react-scripts' | |
)}` | |
); | |
console.log( | |
` - a .tgz archive: ${chalk.green( | |
'https://mysite.com/my-react-scripts-0.8.2.tgz' | |
)}` | |
); | |
console.log( | |
` It is not needed unless you specifically want to use a fork.` | |
); | |
console.log(); | |
console.log( | |
` If you have any problems, do not hesitate to file an issue:` | |
); | |
console.log( | |
` ${chalk.cyan( | |
'https://github.com/facebookincubator/create-react-app/issues/new' | |
)}` | |
); | |
console.log(); | |
}) | |
.parse(process.argv); | |
if (typeof projectName === 'undefined') { | |
console.error('Please specify the project directory:'); | |
console.log( | |
` ${chalk.cyan(program.name())} ${chalk.green('<project-directory>')}` | |
); | |
console.log(); | |
console.log('For example:'); | |
console.log(` ${chalk.cyan(program.name())} ${chalk.green('my-react-app')}`); | |
console.log(); | |
console.log( | |
`Run ${chalk.cyan(`${program.name()} --help`)} to see all options.` | |
); | |
process.exit(1); | |
} | |
function printValidationResults(results) { | |
if (typeof results !== 'undefined') { | |
results.forEach(error => { | |
console.error(chalk.red(` * ${error}`)); | |
}); | |
} | |
} | |
const hiddenProgram = new commander.Command() | |
.option( | |
'--internal-testing-template <path-to-template>', | |
'(internal usage only, DO NOT RELY ON THIS) ' + | |
'use a non-standard application template' | |
) | |
.parse(process.argv); | |
createApp( | |
projectName, | |
program.verbose, | |
program.scriptsVersion, | |
hiddenProgram.internalTestingTemplate | |
); | |
function createApp(name, verbose, version, template) { | |
const root = path.resolve(name); | |
const appName = path.basename(root); | |
checkAppName(appName); | |
fs.ensureDirSync(name); | |
if (!isSafeToCreateProjectIn(root, name)) { | |
process.exit(1); | |
} | |
console.log(`Creating a new React app in ${chalk.green(root)}.`); | |
console.log(); | |
const packageJson = { | |
name: appName, | |
version: '0.1.0', | |
private: true, | |
}; | |
fs.writeFileSync( | |
path.join(root, 'package.json'), | |
JSON.stringify(packageJson, null, 2) | |
); | |
const useYarn = shouldUseYarn(); | |
const originalDirectory = process.cwd(); | |
process.chdir(root); | |
if (!useYarn && !checkThatNpmCanReadCwd()) { | |
process.exit(1); | |
} | |
if (!semver.satisfies(process.version, '>=6.0.0')) { | |
console.log( | |
chalk.yellow( | |
`You are using Node ${process.version} so the project will be bootstrapped with an old unsupported version of tools.\n\n` + | |
`Please update to Node 6 or higher for a better, fully supported experience.\n` | |
) | |
); | |
// Fall back to latest supported react-scripts on Node 4 | |
version = '[email protected]'; | |
} | |
if (!useYarn) { | |
const npmInfo = checkNpmVersion(); | |
if (!npmInfo.hasMinNpm) { | |
if (npmInfo.npmVersion) { | |
console.log( | |
chalk.yellow( | |
`You are using npm ${npmInfo.npmVersion} so the project will be boostrapped with an old unsupported version of tools.\n\n` + | |
`Please update to npm 3 or higher for a better, fully supported experience.\n` | |
) | |
); | |
} | |
// Fall back to latest supported react-scripts for npm 3 | |
version = '[email protected]'; | |
} | |
} | |
run(root, appName, version, verbose, originalDirectory, template, useYarn); | |
} | |
function shouldUseYarn() { | |
try { | |
execSync('yarnpkg --version', { stdio: 'ignore' }); | |
return true; | |
} catch (e) { | |
return false; | |
} | |
} | |
function install(root, useYarn, dependencies, verbose, isOnline) { | |
return new Promise((resolve, reject) => { | |
let command; | |
let args; | |
if (useYarn) { | |
command = 'yarnpkg'; | |
args = ['add', '--exact']; | |
if (!isOnline) { | |
args.push('--offline'); | |
} | |
[].push.apply(args, dependencies); | |
// Explicitly set cwd() to work around issues like | |
// https://github.com/facebookincubator/create-react-app/issues/3326. | |
// Unfortunately we can only do this for Yarn because npm support for | |
// equivalent --prefix flag doesn't help with this issue. | |
// This is why for npm, we run checkThatNpmCanReadCwd() early instead. | |
args.push('--cwd') | |
args.push(root); | |
if (!isOnline) { | |
console.log(chalk.yellow('You appear to be offline.')); | |
console.log(chalk.yellow('Falling back to the local Yarn cache.')); | |
console.log(); | |
} | |
} else { | |
command = 'npm'; | |
args = [ | |
'install', | |
'--save', | |
'--save-exact', | |
'--loglevel', | |
'error', | |
].concat(dependencies); | |
} | |
if (verbose) { | |
args.push('--verbose'); | |
} | |
const child = spawn(command, args, { stdio: 'inherit' }); | |
child.on('close', code => { | |
if (code !== 0) { | |
reject({ | |
command: `${command} ${args.join(' ')}`, | |
}); | |
return; | |
} | |
resolve(); | |
}); | |
}); | |
} | |
function run( | |
root, | |
appName, | |
version, | |
verbose, | |
originalDirectory, | |
template, | |
useYarn | |
) { | |
const packageToInstall = getInstallPackage(version); | |
const allDependencies = ['react', 'react-dom', packageToInstall]; | |
console.log('Installing packages. This might take a couple of minutes.'); | |
getPackageName(packageToInstall) | |
.then(packageName => | |
checkIfOnline(useYarn).then(isOnline => ({ | |
isOnline: isOnline, | |
packageName: packageName, | |
})) | |
) | |
.then(info => { | |
const isOnline = info.isOnline; | |
const packageName = info.packageName; | |
console.log( | |
`Installing ${chalk.cyan('react')}, ${chalk.cyan( | |
'react-dom' | |
)}, and ${chalk.cyan(packageName)}...` | |
); | |
console.log(); | |
return install(root, useYarn, allDependencies, verbose, isOnline).then( | |
() => packageName | |
); | |
}) | |
.then(packageName => { | |
checkNodeVersion(packageName); | |
setCaretRangeForRuntimeDeps(packageName); | |
const scriptsPath = path.resolve( | |
process.cwd(), | |
'node_modules', | |
packageName, | |
'scripts', | |
'init.js' | |
); | |
const init = require(scriptsPath); | |
init(root, appName, verbose, originalDirectory, template); | |
if (version === '[email protected]') { | |
console.log( | |
chalk.yellow( | |
`\nNote: the project was boostrapped with an old unsupported version of tools.\n` + | |
`Please update to Node >=6 and npm >=3 to get supported tools in new projects.\n` | |
) | |
); | |
} | |
}) | |
.catch(reason => { | |
console.log(); | |
console.log('Aborting installation.'); | |
if (reason.command) { | |
console.log(` ${chalk.cyan(reason.command)} has failed.`); | |
} else { | |
console.log(chalk.red('Unexpected error. Please report it as a bug:')); | |
console.log(reason); | |
} | |
console.log(); | |
// On 'exit' we will delete these files from target directory. | |
const knownGeneratedFiles = [ | |
'package.json', | |
'npm-debug.log', | |
'yarn-error.log', | |
'yarn-debug.log', | |
'node_modules', | |
]; | |
const currentFiles = fs.readdirSync(path.join(root)); | |
currentFiles.forEach(file => { | |
knownGeneratedFiles.forEach(fileToMatch => { | |
// This will catch `(npm-debug|yarn-error|yarn-debug).log*` files | |
// and the rest of knownGeneratedFiles. | |
if ( | |
(fileToMatch.match(/.log/g) && file.indexOf(fileToMatch) === 0) || | |
file === fileToMatch | |
) { | |
console.log(`Deleting generated file... ${chalk.cyan(file)}`); | |
fs.removeSync(path.join(root, file)); | |
} | |
}); | |
}); | |
const remainingFiles = fs.readdirSync(path.join(root)); | |
if (!remainingFiles.length) { | |
// Delete target folder if empty | |
console.log( | |
`Deleting ${chalk.cyan(`${appName} /`)} from ${chalk.cyan( | |
path.resolve(root, '..') | |
)}` | |
); | |
process.chdir(path.resolve(root, '..')); | |
fs.removeSync(path.join(root)); | |
} | |
console.log('Done.'); | |
process.exit(1); | |
}); | |
} | |
function getInstallPackage(version) { | |
let packageToInstall = 'react-scripts'; | |
const validSemver = semver.valid(version); | |
if (validSemver) { | |
packageToInstall += `@${validSemver}`; | |
} else if (version) { | |
// for tar.gz or alternative paths | |
packageToInstall = version; | |
} | |
return packageToInstall; | |
} | |
function getTemporaryDirectory() { | |
return new Promise((resolve, reject) => { | |
// Unsafe cleanup lets us recursively delete the directory if it contains | |
// contents; by default it only allows removal if it's empty | |
tmp.dir({ unsafeCleanup: true }, (err, tmpdir, callback) => { | |
if (err) { | |
reject(err); | |
} else { | |
resolve({ | |
tmpdir: tmpdir, | |
cleanup: () => { | |
try { | |
callback(); | |
} catch (ignored) { | |
// Callback might throw and fail, since it's a temp directory the | |
// OS will clean it up eventually... | |
} | |
}, | |
}); | |
} | |
}); | |
}); | |
} | |
function extractStream(stream, dest) { | |
return new Promise((resolve, reject) => { | |
stream.pipe( | |
unpack(dest, err => { | |
if (err) { | |
reject(err); | |
} else { | |
resolve(dest); | |
} | |
}) | |
); | |
}); | |
} | |
// Extract package name from tarball url or path. | |
function getPackageName(installPackage) { | |
if (installPackage.indexOf('.tgz') > -1) { | |
return getTemporaryDirectory() | |
.then(obj => { | |
let stream; | |
if (/^http/.test(installPackage)) { | |
stream = hyperquest(installPackage); | |
} else { | |
stream = fs.createReadStream(installPackage); | |
} | |
return extractStream(stream, obj.tmpdir).then(() => obj); | |
}) | |
.then(obj => { | |
const packageName = require(path.join(obj.tmpdir, 'package.json')).name; | |
obj.cleanup(); | |
return packageName; | |
}) | |
.catch(err => { | |
// The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz | |
// However, this function returns package name only without semver version. | |
console.log( | |
`Could not extract the package name from the archive: ${err.message}` | |
); | |
const assumedProjectName = installPackage.match( | |
/^.+\/(.+?)(?:-\d+.+)?\.tgz$/ | |
)[1]; | |
console.log( | |
`Based on the filename, assuming it is "${chalk.cyan( | |
assumedProjectName | |
)}"` | |
); | |
return Promise.resolve(assumedProjectName); | |
}); | |
} else if (installPackage.indexOf('git+') === 0) { | |
// Pull package name out of git urls e.g: | |
// git+https://github.com/mycompany/react-scripts.git | |
// git+ssh://github.com/mycompany/react-scripts.git#v1.2.3 | |
return Promise.resolve(installPackage.match(/([^/]+)\.git(#.*)?$/)[1]); | |
} else if (installPackage.match(/.+@/)) { | |
// Do not match @scope/ when stripping off @version or @tag | |
return Promise.resolve( | |
installPackage.charAt(0) + installPackage.substr(1).split('@')[0] | |
); | |
} | |
return Promise.resolve(installPackage); | |
} | |
function checkNpmVersion() { | |
let hasMinNpm = false; | |
let npmVersion = null; | |
try { | |
npmVersion = execSync('npm --version') | |
.toString() | |
.trim(); | |
hasMinNpm = semver.gte(npmVersion, '3.0.0'); | |
} catch (err) { | |
// ignore | |
} | |
return { | |
hasMinNpm: hasMinNpm, | |
npmVersion: npmVersion, | |
}; | |
} | |
function checkNodeVersion(packageName) { | |
const packageJsonPath = path.resolve( | |
process.cwd(), | |
'node_modules', | |
packageName, | |
'package.json' | |
); | |
const packageJson = require(packageJsonPath); | |
if (!packageJson.engines || !packageJson.engines.node) { | |
return; | |
} | |
if (!semver.satisfies(process.version, packageJson.engines.node)) { | |
console.error( | |
chalk.red( | |
'You are running Node %s.\n' + | |
'Create React App requires Node %s or higher. \n' + | |
'Please update your version of Node.' | |
), | |
process.version, | |
packageJson.engines.node | |
); | |
process.exit(1); | |
} | |
} | |
function checkAppName(appName) { | |
const validationResult = validateProjectName(appName); | |
if (!validationResult.validForNewPackages) { | |
console.error( | |
`Could not create a project called ${chalk.red( | |
`"${appName}"` | |
)} because of npm naming restrictions:` | |
); | |
printValidationResults(validationResult.errors); | |
printValidationResults(validationResult.warnings); | |
process.exit(1); | |
} | |
// TODO: there should be a single place that holds the dependencies | |
const dependencies = ['react', 'react-dom', 'react-scripts'].sort(); | |
if (dependencies.indexOf(appName) >= 0) { | |
console.error( | |
chalk.red( | |
`We cannot create a project called ${chalk.green( | |
appName | |
)} because a dependency with the same name exists.\n` + | |
`Due to the way npm works, the following names are not allowed:\n\n` | |
) + | |
chalk.cyan(dependencies.map(depName => ` ${depName}`).join('\n')) + | |
chalk.red('\n\nPlease choose a different project name.') | |
); | |
process.exit(1); | |
} | |
} | |
function makeCaretRange(dependencies, name) { | |
const version = dependencies[name]; | |
if (typeof version === 'undefined') { | |
console.error(chalk.red(`Missing ${name} dependency in package.json`)); | |
process.exit(1); | |
} | |
let patchedVersion = `^${version}`; | |
if (!semver.validRange(patchedVersion)) { | |
console.error( | |
`Unable to patch ${name} dependency version because version ${chalk.red( | |
version | |
)} will become invalid ${chalk.red(patchedVersion)}` | |
); | |
patchedVersion = version; | |
} | |
dependencies[name] = patchedVersion; | |
} | |
function setCaretRangeForRuntimeDeps(packageName) { | |
const packagePath = path.join(process.cwd(), 'package.json'); | |
const packageJson = require(packagePath); | |
if (typeof packageJson.dependencies === 'undefined') { | |
console.error(chalk.red('Missing dependencies in package.json')); | |
process.exit(1); | |
} | |
const packageVersion = packageJson.dependencies[packageName]; | |
if (typeof packageVersion === 'undefined') { | |
console.error(chalk.red(`Unable to find ${packageName} in package.json`)); | |
process.exit(1); | |
} | |
makeCaretRange(packageJson.dependencies, 'react'); | |
makeCaretRange(packageJson.dependencies, 'react-dom'); | |
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2)); | |
} | |
// If project only contains files generated by GH, it’s safe. | |
// We also special case IJ-based products .idea because it integrates with CRA: | |
// https://github.com/facebookincubator/create-react-app/pull/368#issuecomment-243446094 | |
function isSafeToCreateProjectIn(root, name) { | |
const validFiles = [ | |
'.DS_Store', | |
'Thumbs.db', | |
'.git', | |
'.gitignore', | |
'.idea', | |
'README.md', | |
'LICENSE', | |
'web.iml', | |
'.hg', | |
'.hgignore', | |
'.hgcheck', | |
]; | |
console.log(); | |
const conflicts = fs | |
.readdirSync(root) | |
.filter(file => !validFiles.includes(file)); | |
if (conflicts.length < 1) { | |
return true; | |
} | |
console.log( | |
`The directory ${chalk.green(name)} contains files that could conflict:` | |
); | |
console.log(); | |
for (const file of conflicts) { | |
console.log(` ${file}`); | |
} | |
console.log(); | |
console.log( | |
'Either try using a new directory name, or remove the files listed above.' | |
); | |
return false; | |
} | |
function checkThatNpmCanReadCwd() { | |
const cwd = process.cwd(); | |
let childOutput = null; | |
try { | |
// Note: intentionally using spawn over exec since | |
// the problem doesn't reproduce otherwise. | |
// `npm config list` is the only reliably way I could find | |
// to reproduce the wrong path. Just printing process.cwd() | |
// in a Node process was not enough. | |
childOutput = spawn.sync('npm', ['config', 'list']).output.join(''); | |
} catch (err) { | |
// Something went wrong spawning node. | |
// Not great, but it means we can't do this check. | |
// We might fail later on, but let's continue. | |
return true; | |
} | |
if (typeof childOutput !== 'string') { | |
return true; | |
} | |
const lines = childOutput.split('\n'); | |
// `npm config list` output includes the following line: | |
// "; cwd = C:\path\to\current\dir" (unquoted) | |
// I couldn't find an easier way to get it. | |
const prefix = '; cwd = '; | |
const line = lines.find(line => line.indexOf(prefix) === 0); | |
if (typeof line !== 'string') { | |
// Fail gracefully. They could remove it. | |
return true; | |
} | |
const npmCWD = line.substring(prefix.length); | |
if (npmCWD === cwd) { | |
return true; | |
} | |
console.error( | |
chalk.red( | |
`Could not start an npm process in the right directory.\n\n` + | |
`The current directory is: ${chalk.bold(cwd)}\n` + | |
`However, a newly started npm process runs in: ${chalk.bold(npmCWD)}\n\n` + | |
`This is probably caused by a miconfigured system terminal shell.` | |
) | |
); | |
if (process.platform === 'win32') { | |
console.error( | |
chalk.red(`On Windows, this can usually be fixed by running:\n\n`) + | |
` ${chalk.cyan('reg')} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` + | |
` ${chalk.cyan('reg')} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` + | |
chalk.red(`Try to run the above two lines in the terminal.\n`) + | |
chalk.red(`To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/`) | |
); | |
} | |
return false; | |
} | |
function checkIfOnline(useYarn) { | |
if (!useYarn) { | |
// Don't ping the Yarn registry. | |
// We'll just assume the best case. | |
return Promise.resolve(true); | |
} | |
return new Promise(resolve => { | |
dns.lookup('registry.yarnpkg.com', err => { | |
if (err != null && process.env.https_proxy) { | |
// If a proxy is defined, we likely can't resolve external hostnames. | |
// Try to resolve the proxy name as an indication of a connection. | |
dns.lookup(url.parse(process.env.https_proxy).hostname, proxyErr => { | |
resolve(proxyErr == null); | |
}); | |
} else { | |
resolve(err == null); | |
} | |
}); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment