# 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
  • 判断 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
  • 这部分主要用到了 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
  • 这部分主要就是判断是否传入了 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
  • 没啥大用,继续

# createApp

终于进入重点了

createApp(
  projectName, // 项目名
  program.verbose, // 是否输出额外信息
  program.scriptsVersion, // 传入的脚本版本
  program.useNpm, // 是否使用npm
  hiddenProgram.internalTestingTemplate // 调试的模板路径
);
1
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
  • 获取项目名
  • checkAppName检查传入的项目名合法性
  • isSafeToCreateProjectInv 判断新建这个文件夹是否是安全的 不安全直接退出
  • 在新建的文件夹下写入 package.json 文件
  • 判断是否使用 yarn 下载
  • 如果是使用 npm,检测 npm 是否在正确目录下执行
  • 判断 node 环境,输出一些提示信息, 并采用旧版本的 react-scripts
  • 检测 npm 版本 判断 npm 版本,如果低于 3.x,使用旧版的 react-scripts 旧版本
  • 判断结束之后,跑 run 方法
  • 传入 项目路径,项目名, reactScripts 版本, 是否输入额外信息, 运行的路径, 模板(开发调试用的), 是否使用 yarn

这里简单总结一下就是:

  1. 检查安全性
  2. 判断是否使用 npm 下载
  3. 进行版本检查,低版本提示信息
  4. 执行 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
  • 在 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

# 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

# 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
  • 删减了一下 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
  • 根据是否使用 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
  • 安装依赖后会去执行 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
  • 修改 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 的依赖版本,运行脚本,并拷贝对应的模板到目录里。
  • 处理完这些之后,输出提示给用户。