# create-react-app 源码分析
# 判断 node 版本
首先进入入口文件
index.js
"use strict";
var chalk = require("chalk");
// 判断node版本,如果太低就返回错误
var currentNodeVersion = process.versions.node;
var semver = currentNodeVersion.split(".");
var major = semver[0];
if (major < 4) {
console.error(
chalk.red(
"You are running Node " +
currentNodeVersion +
".\n" +
"Create React App requires Node 4 or higher. \n" +
"Please update your version of Node."
)
);
process.exit(1);
}
require("./createReactApp");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 判断 node 版本,如果版本太低就返回错误
# commander 命令
顺着入口文件往下进入到主文件 createReactApp.js
最上面的引入先不管,往下看,断点调试第一个运行的部分
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("--info", "print environment debug info")
.option(
"--scripts-version <alternative-package>",
"use a non-standard version of react-scripts"
)
.option("--use-npm")
.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(
` - 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(
` 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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- 这部分主要用到了
commander
这个包 commander
主要用来和用户进行交互,包括版本号,及用户输入的参数- 用<> 包着 project-directory 表示 project-directory 为必填项
# 判断是否传入了 projectName
继续跟着断点向下调试
if (typeof projectName === "undefined") {
if (program.info) {
envinfo.print({
packages: [
"react",
"react-dom",
"react-scripts",
"redux",
"react-redux",
"antd",
"babel-plugin-import",
"react-app-rewired"
],
noNativeIDE: true,
duplicates: true
});
process.exit(0);
}
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- 这部分主要就是判断是否传入了 projectName
- 如果传入了 --info 就会判断环境中的版本
envinfo
用来输出当前环境系统的一些系统信息,比如系统版本,npm 等等还有 react,react-dom,react-scripts 这些包的版本- 最后控制退出终止程序
# 隐藏的 commander 参数
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);
1
2
3
4
5
6
7
2
3
4
5
6
7
- 没啥大用,继续
# createApp
终于进入重点了
createApp(
projectName, // 项目名
program.verbose, // 是否输出额外信息
program.scriptsVersion, // 传入的脚本版本
program.useNpm, // 是否使用npm
hiddenProgram.internalTestingTemplate // 调试的模板路径
);
1
2
3
4
5
6
7
2
3
4
5
6
7
- 调用 createApp,传入了一堆参数
接下来就来好好瞅瞅 createApp 究竟干了啥
function createApp(name, verbose, version, useNpm, 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();
// 在新建的文件夹下写入 package.json 文件
const packageJson = {
name: appName,
version: "0.1.0",
private: true
};
fs.writeFileSync(
path.join(root, "package.json"),
JSON.stringify(packageJson, null, 2)
);
// 判断是否使用yarn下载
const useYarn = useNpm ? false : shouldUseYarn();
const originalDirectory = process.cwd();
process.chdir(root);
// 如果是使用npm,检测npm是否在正确目录下执行
if (!useYarn && !checkThatNpmCanReadCwd()) {
process.exit(1);
}
// 判断node环境,输出一些提示信息, 并采用旧版本的 react-scripts
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 = "react-scripts@0.9.x";
}
if (!useYarn) {
// 检测npm版本 判断npm版本,如果低于3.x,使用旧版的 react-scripts旧版本
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 = "react-scripts@0.9.x";
}
}
// 项目路径,项目名, reactScripts版本, 是否输入额外信息, 运行的路径, 模板(开发调试用的), 是否使用yarn
run(root, appName, version, verbose, originalDirectory, template, useYarn);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
- 获取项目名
checkAppName
检查传入的项目名合法性isSafeToCreateProjectInv
判断新建这个文件夹是否是安全的 不安全直接退出- 在新建的文件夹下写入 package.json 文件
- 判断是否使用 yarn 下载
- 如果是使用 npm,检测 npm 是否在正确目录下执行
- 判断 node 环境,输出一些提示信息, 并采用旧版本的 react-scripts
- 检测 npm 版本 判断 npm 版本,如果低于 3.x,使用旧版的 react-scripts 旧版本
- 判断结束之后,跑 run 方法
- 传入 项目路径,项目名, reactScripts 版本, 是否输入额外信息, 运行的路径, 模板(开发调试用的), 是否使用 yarn
这里简单总结一下就是:
- 检查安全性
- 判断是否使用 npm 下载
- 进行版本检查,低版本提示信息
- 执行 run,把上面过滤好的参数传入
下面是用到的几个小函数
# checkAppName 检查项目名合法性
function checkAppName(appName) {
const validationResult = validateProjectName(appName);
if (!validationResult.validForNewPackages) {
// 判断是否符合npm规范如果不符合,输出提示并结束任务
}
// TODO: there should be a single place that holds the dependencies
const dependencies = [
"react",
"react-dom",
"react-scripts",
"redux",
"react-redux",
"antd",
"babel-plugin-import",
"react-app-rewired"
].sort();
if (dependencies.indexOf(appName) >= 0) {
// 判断是否重名,如果重名则输出提示并结束任务
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 在 createApp 中,判断传入的项目的合法性
# shouldUseYarn 判断是否有装 yarn
function shouldUseYarn() {
try {
execSync("yarnpkg --version", { stdio: "ignore" });
return true;
} catch (e) {
return false;
}
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# checkThatNpmCanReadCwd 判断是否有装 npm
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 reliable 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 misconfigured 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# run
function run(...) {
// 这里获取要安装的package,默认情况下是 `react-scripts`。 也可能是根据传参去拿对应的包
const packageToInstall = getInstallPackage(version, originalDirectory);
// 需要安装所有的依赖, react, react-dom, react-script
const allDependencies = ['react', 'react-dom', packageToInstall];
...
// 获取包名,支持 taz|tar格式、git仓库、版本号、文件路径等等
getPackageName(packageToInstall)
.then(packageName =>
// 如果是yarn,判断是否在线模式(对应的就是离线模式),处理完判断就返回给下一个then处理
checkIfOnline(useYarn).then(isOnline => ({
isOnline: isOnline,
packageName: packageName,
}))
)
.then(info => {
const isOnline = info.isOnline;
const packageName = info.packageName;
/** 开始核心的安装部分 传入`安装路径`,`是否使用yarn`,`所有依赖`,`是否输出额外信息`,`在线状态` **/
/** 这里主要的操作是 根据传入的参数,开始跑 npm || yarn 安装react react-dom等依赖 **/
/** 这里如果网络不好,可能会挂 **/
return install(root, useYarn, allDependencies, verbose, isOnline).then(
() => packageName
);
})
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- 删减了一下 run 方法
- 先根据传入的版本 version 和原始目录 originalDirectory 去获取要安装的某个 package
- 默认的 version 为空,获取到的 packageToInstall 值是 react-scripts, 然后将 packageToInstall 拼接到 allDependencies 意为所有需要安装的依赖
- 然后如果当前是采用 yarn 安装方式的话,就判断是否处于离线状态
- 扔给 install 去下载
# install
function install(root, useYarn, dependencies, verbose, isOnline) {
// 主要根据参数拼装命令行,然后用node去跑安装脚本 如 `npm install react react-dom --save` 或者 `yarn add react react-dom`
return new Promise((resolve, reject) => {
let command;
let args;
// 开始拼装 yarn 命令行
if (useYarn) {
command = "yarnpkg";
args = ["add", "--exact"]; // 使用确切版本模式
// 判断是否是离线状态 加个状态
if (!isOnline) {
args.push("--offline");
}
[].push.apply(args, dependencies);
// 将cwd设置为我们要安装的目录路径
args.push("--cwd");
args.push(root);
// 如果是离线的话输出一些提示信息
} else {
// npm 安装模式,与yarn同理
command = "npm";
args = [
"install",
"--save",
"--save-exact",
"--loglevel",
"error"
].concat(dependencies);
}
// 如果有传verbose, 则加该参数 输出额外的信息
if (verbose) {
args.push("--verbose");
}
// 用 cross-spawn 跨平台执行命令行
const child = spawn(command, args, { stdio: "inherit" });
// 关闭的处理
child.on("close", code => {
if (code !== 0) {
return reject({ command: `${command} ${args.join(" ")}` });
}
resolve();
});
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
- 根据是否使用 yarn 分成两种处理方法, 根据传入的 dependencies 去拼接需要安装的依赖
- 处理做完之后里面除了安装的依赖和 package.json 外没有任何东西。所以接下来的操作是生成一些 webpack 的配置和一个简单的可启动 demo
run(...) {
...
getPackageName(packageToInstall)
.then(...)
.then(info => install(...).then(()=> packageName))
/** install 安装完之后的逻辑 **/
/** 从这里开始拷贝模板逻辑 **/
.then(packageName => {
// 安装完 react, react-dom, react-scripts 之后检查当前环境运行的node版本是否符合要求
checkNodeVersion(packageName);
// 该项package.json里react, react-dom的版本范围,eg: 16.0.0 => ^16.0.0
setCaretRangeForRuntimeDeps(packageName);
// 加载script脚本,并执行init方法
const scriptsPath = path.resolve(
process.cwd(),
'node_modules',
packageName,
'scripts',
'init.js'
);
const init = require(scriptsPath);
// init 方法主要执行的操作是
// 写入package.json 一些脚本。eg: script: {start: 'react-scripts start'}
// 改写README.MD
// 把预设的模版拷贝到项目下
// 输出成功与后续操作的信息
init(root, appName, verbose, originalDirectory, template);
if (version === 'react-scripts@0.9.x') {
// 如果是旧版本的 react-scripts 输出提示
}
})
.catch(reason => {
// 出错的话,把安装了的文件全删了 并输出一些日志信息等
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- 安装依赖后会去执行 init 方法
目标文件夹/node_modules/react-scripts/script/init.js
module.exports = function(
appPath,
appName,
verbose,
originalDirectory,
template
) {
const ownPackageName = require(path.join(__dirname, "..", "package.json"))
.name;
const ownPath = path.join(appPath, "node_modules", ownPackageName);
const appPackage = require(path.join(appPath, "package.json"));
const useYarn = fs.existsSync(path.join(appPath, "yarn.lock"));
// 1. 把启动脚本写入目标 package.json
appPackage.scripts = {
start: "react-scripts start",
build: "react-scripts build",
test: "react-scripts test --env=jsdom",
eject: "react-scripts eject"
};
fs.writeFileSync(
path.join(appPath, "package.json"),
JSON.stringify(appPackage, null, 2)
);
// 2. 改写README.MD,把一些帮助信息写进去
const readmeExists = fs.existsSync(path.join(appPath, "README.md"));
if (readmeExists) {
fs.renameSync(
path.join(appPath, "README.md"),
path.join(appPath, "README.old.md")
);
}
// 3. 把预设的模版拷贝到项目下,主要有 public, src/[APP.css, APP.js, index.js,....], .gitignore
const templatePath = template
? path.resolve(originalDirectory, template)
: path.join(ownPath, "template");
if (fs.existsSync(templatePath)) {
fs.copySync(templatePath, appPath);
} else {
return;
}
fs.move(
path.join(appPath, "gitignore"),
path.join(appPath, ".gitignore"),
[],
err => {
/* 错误处理 */
}
);
// 这里再次进行命令行的拼接,如果后面发现没有安装react和react-dom,重新安装一次
let command;
let args;
if (useYarn) {
command = "yarnpkg";
args = ["add"];
} else {
command = "npm";
args = ["install", "--save", verbose && "--verbose"].filter(e => e);
}
args.push("react", "react-dom");
const templateDependenciesPath = path.join(
appPath,
".template.dependencies.json"
);
if (fs.existsSync(templateDependenciesPath)) {
const templateDependencies = require(templateDependenciesPath).dependencies;
args = args.concat(
Object.keys(templateDependencies).map(key => {
return `${key}@${templateDependencies[key]}`;
})
);
fs.unlinkSync(templateDependenciesPath);
}
if (!isReactInstalled(appPackage) || template) {
const proc = spawn.sync(command, args, { stdio: "inherit" });
if (proc.status !== 0) {
console.error(`\`${command} ${args.join(" ")}\` failed`);
return;
}
}
// 5. 输出成功的日志
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
- 修改 package.json,写入一些启动脚本,比如 script: {start: 'react-scripts start'},用来启动开发项目
- 改写 README.MD,把一些帮助信息写进去
- 把预设的模版拷贝到项目下,主要有 public, src/[APP.css, APP.js, index.js,....], .gitignore
- 对旧版的 node 做一些兼容的处理,这里补一句,在选择 react-scripts 时就有根据 node 版本去判断选择比较老的 @0.9.x 版。
- 如果完成输出对应的信息,如果失败,做一些输出日志等操作。
# 总结
- 判断node版本如果小于4就退出,否则执行 createReactApp.js 文件
- createReactApp.js先做一些命令行的处理响应处理,然后判断是否有传入 projectName 没有就提示并退出
- 根据传入的 projectName 创建目录,并创建package.json。
- 判断是否有特殊要求指定安装某个版本的react-scripts,然后用cross-spawn去处理跨平台的命令行问题,用yarn或npm安装react, react-dom, react-scripts。
- 安装完之后跑 react-scripts/script/init.js 修改 package.json 的依赖版本,运行脚本,并拷贝对应的模板到目录里。
- 处理完这些之后,输出提示给用户。
← 手写 Koa lottie原理探索 →