var currentNodeVersion = process.versions.node;
var semver = currentNodeVersion.split('.');
var major = semver[0];
if (major < 8) {
console.error(
'You are running Node ' +
currentNodeVersion +
'.\n' +
'Create React App requires Node 8 or higher. \n' +
'Please update your version of Node.'
);
process.exit(1);
}
require('./createReactApp');
在create-react-app/index.js
中,首先获取了进程中的node
版本号,并通过截取获得主版本号,如果主版本号小于8,则提示用户应该升级node
版本并退出进程,主要的逻辑还是在createReactApp
中。
createReactApp
主要的三个核心函数:createApp
作环境和命令行参数的处理run
根据参数获取需要安装的依赖文件install
安装依赖// 为终端输出的文字添加颜色,增加区别
const chalk = require('chalk');
// 轻量的node命令行工具 可以处理用户终端输入的命令行参数
const commander = require('commander');
const dns = require('dns');
// 可以输出当前设备的硬件及软件信息
const envinfo = require('envinfo');
const execSync = require('child_process').execSync;
// 文件的处理的模块
const fs = require('fs-extra');
const hyperquest = require('hyperquest');
// 提供终端进行交互的功能
const inquirer = require('inquirer');
const os = require('os');
const path = require('path');
// 规范化版本工具库
const semver = require('semver');
// 跨平台的spawn解决方案
const spawn = require('cross-spawn');
// 临时文件工具库
const tmp = require('tmp');
const unpack = require('tar-pack').unpack;
const url = require('url');
// 验证包名是否被占用的工具
const validateProjectName = require('validate-npm-package-name');
const packageJson = require('./package.json');
createReactApp.js
中首先引入项目所需要的模块,以下几个模块是脚手架工具非常常用的工具
chalk
commander
semver
spawn
inquirer
validateProjectName
// 获取命令行参数
const program = new commander.Command(packageJson.name)
.version(packageJson.version)
.arguments('<project-directory>') // create-react-app 唯一参数
.usage(`${chalk.green('<project-directory>')} [options]`) // 用法介绍
.action(name => {
projectName = name; // create-react-app my-react 这里name就是my-react
})
.option('--verbose', 'print additional logs')
.option('--info', 'print environment debug info')
.option(
'--scripts-version <alternative-package>',
'use a non-standard version of react-scripts' // 使用不标准版本的react-scripts
)
.option(
'--template <path-to-template>',
'specify a template for the created project'
)
.option('--use-npm') // 使用npm
.option('--use-pnp') // 使用pnp
// TODO: Remove this in next major release.
.option( // 使用typescript 将在下个主版本移除
'--typescript',
'(this option will be removed in favour of templates in the next major release of create-react-app)'
)
.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 specific npm tag: ${chalk.green('@next')}`);
console.log(
` - a custom fork published on npm: ${chalk.green(
'my-react-scripts'
)}`
);
console.log(
` - a local path relative to the current working directory: ${chalk.green(
'file:../my-react-scripts'
)}`
);
console.log(
` - a .tgz archive: ${chalk.green(
'https://mysite.com/my-react-scripts-0.8.2.tgz'
)}`
);
console.log(
` - a .tar.gz archive: ${chalk.green(
'https://mysite.com/my-react-scripts-0.8.2.tar.gz'
)}`
);
console.log(
` It is not needed unless you specifically want to use a fork.`
);
console.log();
console.log(` A custom ${chalk.cyan('--template')} can be one of:`);
console.log(
` - a custom fork published on npm: ${chalk.green(
'cra-template-typescript'
)}`
);
console.log(
` - a local path relative to the current working directory: ${chalk.green(
'file:../my-custom-template'
)}`
);
console.log(
` - a .tgz archive: ${chalk.green(
'https://mysite.com/my-custom-template-0.8.2.tgz'
)}`
);
console.log(
` - a .tar.gz archive: ${chalk.green(
'https://mysite.com/my-custom-template-0.8.2.tar.gz'
)}`
);
console.log();
console.log(
` If you have any problems, do not hesitate to file an issue:`
);
console.log(
` ${chalk.cyan(
'https://github.com/facebook/create-react-app/issues/new'
)}`
);
console.log();
})
.parse(process.argv); // 解析命令行参数
首先生成crate-react-app
脚手架支持的命令行参数,当我们在终端输入create-react-app --help
则会提示这个命令的帮助提示信息和可选参数的使用。最终commander
为我们处理并解析命令行中的参数。
if (program.info) {
console.log(chalk.bold('\nEnvironment Info:'));
console.log(
`\n current version of ${packageJson.name}: ${packageJson.version}`
);
console.log(` running from ${__dirname}`);
return envinfo
.run(
{
System: ['OS', 'CPU'],
Binaries: ['Node', 'npm', 'Yarn'],
Browsers: ['Chrome', 'Edge', 'Internet Explorer', 'Firefox', 'Safari'],
npmPackages: ['react', 'react-dom', 'react-scripts'],
npmGlobalPackages: ['create-react-app'],
},
{
duplicates: true,
showNotFound: true,
}
)
.then(console.log);
}
当命令行参数中存在--info
如create-react-app --info
时,脚手架通过envinfo
输出当前的系统信息和软件版本信息提供参考,最终结束进程。
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);
}
接着判断项目名称是否存在,projectName
在commander action
中被赋值,如果不存在则提示用户提供项目名称和示例并退出进程。
function createApp(
name,
verbose,
version,
template,
useNpm,
usePnp,
useTypeScript
) {
// 判断当前进程的node版本是否大于8.10.0
const unsupportedNodeVersion = !semver.satisfies(process.version, '>=8.10.0');
// 当当前node版本小于8.10.0时不能使用typescript并退出进程
if (unsupportedNodeVersion && useTypeScript) {
console.error(
chalk.red(
`You are using Node ${process.version} with the TypeScript template. Node 8.10 or higher is required to use TypeScript.\n`
)
);
process.exit(1);
} else if (unsupportedNodeVersion) {
// 当当前node版本小于8.10.0时提示用户建议升级node,同时指定当前react-scripts的版本
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 8.10 or higher for a better, fully supported experience.\n`
)
);
// Fall back to latest supported react-scripts on Node 4
version = 'react-scripts@0.9.x';
}
const root = path.resolve(name); // 获取要创建项目的绝对路径
const appName = path.basename(root); // 获取项目名称
// 检查用户输入的项目名称
checkAppName(appName);
// 根据项目名称创建新目录
fs.ensureDirSync(name);
// 当有不安全的文件进程退出
if (!isSafeToCreateProjectIn(root, name)) {
process.exit(1);
}
console.log();
console.log(`Creating a new React app in ${chalk.green(root)}.`);
console.log();
const packageJson = {
name: appName,
version: '0.1.0',
private: true,
};
// 在新目录中写入package.json
fs.writeFileSync(
path.join(root, 'package.json'),
JSON.stringify(packageJson, null, 2) + os.EOL
);
// 是否使用yarn安装,在shouldUseYarn中判断是否安装了yarn
const useYarn = useNpm ? false : shouldUseYarn();
// 记录当前的文件目录
const originalDirectory = process.cwd();
process.chdir(root);
if (!useYarn && !checkThatNpmCanReadCwd()) {
process.exit(1);
}
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 bootstrapped with an old unsupported version of tools.\n\n` +
`Please update to npm 5 or higher for a better, fully supported experience.\n`
)
);
}
// Fall back to latest supported react-scripts for npm 3
version = 'react-scripts@0.9.x';
}
} else if (usePnp) {
const yarnInfo = checkYarnVersion();
if (!yarnInfo.hasMinYarnPnp) {
if (yarnInfo.yarnVersion) {
console.log(
chalk.yellow(
`You are using Yarn ${yarnInfo.yarnVersion} together with the --use-pnp flag, but Plug'n'Play is only supported starting from the 1.12 release.\n\n` +
`Please update to Yarn 1.12 or higher for a better, fully supported experience.\n`
)
);
}
// 1.11 had an issue with webpack-dev-middleware, so better not use PnP with it (never reached stable, but still)
usePnp = false;
}
}
if (useTypeScript) {
console.log(
chalk.yellow(
'The --typescript option has been deprecated and will be removed in a future release.'
)
);
console.log(
chalk.yellow(
`In future, please use ${chalk.cyan('--template typescript')}.`
)
);
console.log();
if (!template) {
template = 'typescript';
}
}
if (useYarn) {
let yarnUsesDefaultRegistry = true;
// 检测用户是否安装了yarn
try {
yarnUsesDefaultRegistry =
execSync('yarnpkg config get registry')
.toString()
.trim() === 'https://registry.yarnpkg.com';
} catch (e) {
// ignore
}
if (yarnUsesDefaultRegistry) {
fs.copySync(
require.resolve('./yarn.lock.cached'),
path.join(root, 'yarn.lock')
);
}
}
run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp
);
}
function run(
root,
appName,
version,
verbose,
originalDirectory,
template,
useYarn,
usePnp
) {
Promise.all([
// 获取安装包
getInstallPackage(version, originalDirectory),
getTemplateInstallPackage(template, originalDirectory),
]).then(([packageToInstall, templateToInstall]) => {
const allDependencies = ['react', 'react-dom', packageToInstall];
console.log('Installing packages. This might take a couple of minutes.');
Promise.all([
getPackageInfo(packageToInstall),
getPackageInfo(templateToInstall),
])
.then(([packageInfo, templateInfo]) =>
checkIfOnline(useYarn).then(isOnline => ({
isOnline,
packageInfo,
templateInfo,
}))
)
.then(({ isOnline, packageInfo, templateInfo }) => {
let packageVersion = semver.coerce(packageInfo.version);
const templatesVersionMinimum = '3.3.0';
// Assume compatibility if we can't test the version.
if (!semver.valid(packageVersion)) {
packageVersion = templatesVersionMinimum;
}
// Only support templates when used alongside new react-scripts versions.
const supportsTemplates = semver.gte(
packageVersion,
templatesVersionMinimum
);
if (supportsTemplates) {
allDependencies.push(templateToInstall);
} else if (template) {
console.log('');
console.log(
`The ${chalk.cyan(packageInfo.name)} version you're using ${
packageInfo.name === 'react-scripts' ? 'is not' : 'may not be'
} compatible with the ${chalk.cyan('--template')} option.`
);
console.log('');
}
// TODO: Remove with next major release.
if (!supportsTemplates && (template || '').includes('typescript')) {
allDependencies.push(
'@types/node',
'@types/react',
'@types/react-dom',
'@types/jest',
'typescript'
);
}
console.log(
`Installing ${chalk.cyan('react')}, ${chalk.cyan(
'react-dom'
)}, and ${chalk.cyan(packageInfo.name)}${
supportsTemplates ? ` with ${chalk.cyan(templateInfo.name)}` : ''
}...`
);
console.log();
return install(
root,
useYarn,
usePnp,
allDependencies,
verbose,
isOnline
).then(() => ({
packageInfo,
supportsTemplates,
templateInfo,
}));
})
.then(async ({ packageInfo, supportsTemplates, templateInfo }) => {
const packageName = packageInfo.name;
const templateName = supportsTemplates ? templateInfo.name : undefined;
checkNodeVersion(packageName);
setCaretRangeForRuntimeDeps(packageName);
const pnpPath = path.resolve(process.cwd(), '.pnp.js');
const nodeArgs = fs.existsSync(pnpPath) ? ['--require', pnpPath] : [];
await executeNodeScript(
{
cwd: process.cwd(),
args: nodeArgs,
},
[root, appName, verbose, originalDirectory, templateName],
`
var init = require('${packageName}/scripts/init.js');
init.apply(null, JSON.parse(process.argv[1]));
`
);
if (version === 'react-scripts@0.9.x') {
console.log(
chalk.yellow(
`\nNote: the project was bootstrapped with an old unsupported version of tools.\n` +
`Please update to Node >=8.10 and npm >=5 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',
'yarn.lock',
'node_modules',
];
const currentFiles = fs.readdirSync(path.join(root));
currentFiles.forEach(file => {
knownGeneratedFiles.forEach(fileToMatch => {
// This removes all knownGeneratedFiles.
if (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);
});
});
}
run
函数首先获取react-scripts
和模板的信息,接着将所需要的依赖放入allDependencies
中,最终执行install
函数
install
主要根据命令行参数中指定的包管理工具(默认yarn)来安装模块。