【React】create-react-app源码分析

咸亦
2023-12-01
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);
}

当命令行参数中存在--infocreate-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);
}

接着判断项目名称是否存在,projectNamecommander 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
  );
}
YES
NO
YES
NO
YES
NO
createApp
进程node版本是否小于8.10.0并是否使用typescript
退出进程
包名是否被占用
根据项目名称生成目录
判断新目录中是否有不合规的文件
在新目录中写入package.json
判断当前的包管理工具
执行run函数

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)来安装模块。

 类似资料: