create-react-app 脚手架-部分源码分析

前置知识

monorepo管理

  • Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)
  • monorepo 最主要的好处是统一的工作流和代码共享
  • Lerna是一个管理多个 npm 模块的工具,优化维护多包的工作流,解决多个包互相依赖,且发布需要手动维护多个包的问题
  • yarn

1609568943612

monorepo优势

  • Monorepo最主要的好处是统一的工作流代码共享

  • 目前大多数开源库都使用Monorepo进行管理,如react、vue-next、create-react-app

monorepo劣势

  • 体积庞大。babel仓库下存放了所有相关代码,clone到本地也需要耗费不少时间。
  • 不适合用于公司项目。各个业务线仓库代码基本都是独立的,如果堆放到一起,理解和维护成本将会相当大

Lerna

安装
npm i lerna -g
初始化
lerna init

1

package.json

package.json

{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}
lerna.json

lerna.json

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}
yarn workspace
  • yarn workspace允许我们使用 monorepo 的形式来管理项目
  • 在安装 node_modules 的时候它不会安装到每个子项目的 node_modules 里面,而是直接安装到根目录下面,这样每个子项目都可以读取到根目录的 node_modules
  • 整个项目只有根目录下面会有一份 yarn.lock 文件。子项目也会被 linknode_modules 里面,这样就允许我们就可以直接用 import 导入对应的项目
  • yarn.lock文件是自动生成的,也完全Yarn来处理.yarn.lock锁定你安装的每个依赖项的版本,这可以确保你不会意外获得不良依赖
开启workspace

package.json

{
  "name": "root",
  "private": true, // 私有的,用来管理整个项目,不会被发布到npm
+  "workspaces": [
+    "packages/*"
+  ],
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}
创建子项目
lerna create create-react-app3
lerna create react-scripts3
lerna create cra-template3

1

添加依赖

设置加速镜像

yarn config get registry
yarn config set registry http://registry.npm.taobao.org/
yarn config set registry http://registry.npmjs.org/
作用命令
查看工作空间信息yarn workspaces info
给根空间添加依赖yarn add chalk cross-spawn fs-extra --ignore-workspace-root-check
给某个项目添加依赖yarn workspace create-react-app3 add commander
删除所有的 node_moduleslerna clean 等于 yarn workspaces run clean
安装和linkyarn install 等于 lerna bootstrap --npm-client yarn --use-workspaces
重新获取所有的 node_modulesyarn install --force
查看缓存目录yarn cache dir
清除本地缓存yarn cache clean

commander

  • chalk可以在终端显示颜色
  • commander是一个完整的node.js命令行解决方案
  • version方法可以设置版本,其默认选项为-V--version
  • 通过.arguments可以为最顶层命令指定参数,对子命令而言,参数都包括在.command调用之中了。尖括号(例如)意味着必选,而方括号(例如[optional])则代表可选
  • 通过usage选项可以修改帮助信息的首行提示
const chalk = require('chalk');
const {Command} = require('commander');
console.log('process.argv',process.argv);
new Command('create-react-app')
    .version('1.0.0')
    .arguments('<must1> <must2> [optional]')
    .usage(`${chalk.green('<must1> <must2>')} [optional]`)
    .action((must1,must2,optional,...args) => {
       console.log(must1,must2,optional,args);
    })
    .parse(process.argv);

cross-spawn

  • cross-spawn是node的spawnspawnSync的跨平台解决方案
  • inherit表示将相应的stdio流传给父进程或从父进程传入
const spawn = require('cross-spawn');
const child = spawn('node', ['script.js','one','two','three'], { stdio: 'inherit' });
child.on('close',()=>{
    console.log('child is done!');
});
const result = spawn.sync('node', ['script.js','one','two','three'], { stdio: 'inherit' });
console.log(result);

dotenv

  • 使用dotenv,只需要将程序的环境变量配置写在.env文件中

.env

MONGODB_HOST=localhost
MONGODB_PORT=27017
MONGODB_DB=test
MONGODB_URI=mongodb://${MONGODB_HOST}:${MONGODB_PORT}/${MONGODB_DB}
const dotenvFile = '.env';
require('dotenv-expand')(
    require('dotenv').config({
        path: dotenvFile,
    })
);
console.log(process.env.MONGODB_HOST);
console.log(process.env.MONGODB_PORT);
console.log(process.env.MONGODB_DB);
console.log(process.env.MONGODB_URI);

NODE_PATH

  • NODE_PATH 就是NODE中用来寻找模块所提供的路径注册环境变量
let fs = require('fs');
let path = require('path');
const appDirectory = fs.realpathSync(process.cwd());
//NODE_PATH folder1;folder2
process.env.NODE_PATH = [
    './a',
    path.resolve(appDirectory, 'b'),
    './c',
].join(path.delimiter);
process.env.NODE_PATH = (process.env.NODE_PATH || '')
  .split(path.delimiter)//windows ; mac :
  .filter(folder => folder && !path.isAbsolute(folder))//不是绝对路径的删除
  .map(folder => path.resolve(appDirectory, folder))//只能是相对目录
  .join(path.delimiter);//再连接在一起
console.log(process.env.NODE_PATH);

semver-regex

  • 匹配semver版本的正则表达式
const semverRegex = require('semver-regex');
console.log(semverRegex().test('v1.0.0'));
//=> true
console.log(semverRegex().test('1.2.3-alpha.10.beta.0+build.unicorn.rainbow'));
//=> true
console.log(semverRegex().exec('unicorn 1.0.0 rainbow')[0]);
//=> '1.0.0'
console.log('unicorn 1.0.0 and rainbow 2.1.3'.match(semverRegex()));
//=> ['1.0.0', '2.1.3']
const semver = require('semver')
console.log(semver.valid('1.2.3')); // '1.2.3'
console.log(semver.valid('a.b.c')); // null
console.log(semver.clean('  =v1.2.3   ')); // '1.2.3'
console.log(semver.satisfies('1.2.3', '1.x || >=2.5.0 || 5.0.0 - 7.2.3')); // true
console.log(semver.gt('1.2.3', '9.8.7')); // false
console.log(semver.lt('1.2.3', '9.8.7')); // true
console.log(semver.valid(semver.coerce('v2'))); // '2.0.0'
console.log(semver.valid(semver.coerce('42.6.7.9.3-alpha'))); // '42.6.7'

globby

  • globby是基于 glob,并进一步得到了增强
var globby = require('globby');
(async () => {
    const paths = await globby(['images','photos'],{
      expandDirectories: true
    });
    console.log(paths);
})();

globalThis

  • globalThis提供统一的机制来访问全局对象
    • Web 浏览器 window
    • Node.js global
    • Web Worker self
// 浏览器环境
console.log(globalThis);    // => Window {...}
// node.js 环境
console.log(globalThis);    // => Object [global] {...}
// web worker 环境
console.log(globalThis);    // => DedicatedWorkerGlobalScope {...}

bfj

const bfj = require('bfj');

bfj.read('big.json')
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.log(error)
  });

pnp

yarn install

  • 1.将依赖包的版本区间解析为某个具体的版本号
  • 2.下载对应版本依赖的 tar 包到本地离线镜像
  • 3.将依赖从离线镜像解压到本地缓存
  • 4.将依赖从缓存拷贝到当前目录的 node_modules 目录

Pnp

  • PnP工作原理是作为把依赖从缓存拷贝到 node_modules 的替代方案

使用

启用
#create-react-app 已经集成了对 PnP 的支持。只需在创建项目时添加 --use-pnp 参数
npx create-react-app react-project --use-pnp 
# 在已有项目中开启 PnP
yarn --pnp
yarn add uuid
package.json
  • 只要 installConfig.pnp 的值是一个真值且当前版本的 Yarn 支持,PnP 特性就会被启用
{
  "installConfig": {
    "pnp": true
  }
}
uuid.js

uuid.js

let uuid = require('uuid');
console.log(uuid.v4());
运行
  • 由于在开启了 PnP 的项目中不再有 node_modules 目录,所有的依赖引用都必须由 .pnp.js 中的 resolver 处理
  • 因此不论是执行 script 还是用 node 直接执行一个 JS 文件,都必须经由 Yarn 处理
yarn run build
yarn node uuid.js

npx安装依赖

npm 5.2.0之后提供了npx命令,当调用npx <command>,而你的$PATH没有<command> 时,将会自动安装这个名字的包,并执行它。执行完后,这个安装下来的包会被清理掉。 使用npx执行cra,能够保证你执行的脚手架始终是最新的版本。关于npx具体的文档可以参考这里blog.npmjs.org/post/162869…

This feature is ideal for things like generators, too. Tools like yeoman or create-react-app only ever get called once in a blue moon. By the time you run them again, they’ll already be far out of date, so you end up having to run an install every time you want to use them anyway.

目录结构

create-react-app是以learn组织的monorepo。是一个多仓库的项目,主要代码都在packages下的各个包中

  • https://github.com/facebook/create-react-app

  • https://github.com/facebook/create-react-app/releases

  • 目前react-react-app脚手架最新版本是5.0.1

  • Create React App 5.0.1 is a maintenance release that improves compatibility with React 18. We’ve also updated our templates to use createRoot and relaxed our check for older versions of Create React App.

  • packages下有11个目录

    babel-plugin-named-asset-import
    babel-preset-react-app
    confusing-browser-globals
    cra-template
    cra-template-typescript
    create-react-app
    eslint-config-react-app
    react-app-polyfill
    react-dev-utils
    react-error-overlay
    react-scripts
    
  • babel-plugin-named-asset-import

    • 允许将资源文件通过自定义的命名导入
  • babel-preset-react-app

    • react相关babel preset预设,用于将 JavaScript(包括 JSX 语法和最新的 JavaScript 特性)转换为在大多数浏览器中兼容的 JavaScript
  • confusing-browser-globals

    • 列出了在浏览器环境中容易混淆或产生误导的全局变量,eslint的一个插件,限制全局变量的使用
  • cra-template

    • 默认模板,它提供了一个基础的 React 应用结构。该模板包含了一些基本的文件和配置
  • cra-template-typescript

    • 它提供了一个基础的 TypeScript React 应用结构,并包含了必要的 TypeScript 配置
  • create-react-app

    • 提供了create-react-app命令及选项
    • 检测环境:如node版本、cra版本、yarn还是npm、是否断网、是否开启pnp,以及yarn是否支持pnp
    • 获取模板
    • 下载依赖(react、react-dom以及模板)
    • 执行react-scripts下的init方法
  • eslint-config-react-app

    • 含了一系列的 ESLint 规则和插件,用于确保代码质量和一致性。该配置是为 React 项目量身定制的
  • react-app-polyfill

    • 包含了一些用于支持较旧浏览器(如 IE11)的 polyfills。这些 polyfills 可以确保 React 应用能够在不支持某些现代 JavaScript 特性的浏览器中正常运行
  • react-dev-utils

    • 各种webpack插件、工具函数
  • react-error-overlay

    • 显示友好的错误信息和调试提示
  • react-scripts

    • 一组脚本和配置,包含了用于启动、构建和测试 React 应用的所有必要工具和配置
    • 提供init方法:读取template包并生成项目;修改package.json;删除template依赖
    • 提供项目内几个命令:start、build、test、eject

工作流程

  • 运行create-react-app命令后的执行流程大致如下

image-20240620162846482

  • 这个过程中包含了许多细节

    • 当node版本过低时将会降低react-scripts版本以尽可能成功

    • 支持yarn和npm,可选pnp特性,并且检查了用户环境是否支持这些命令和特性

    • 当用户离线时使用缓存安装

    • 检查项目名是否和package包名重复

    • 如果本地文件与生成的文件有冲突时进行了小心的处理

image-20240620163203817

核心

image-20240620154934921

create-react-app

image-20240620170111005

index.js

  • 判断当前运行node版本,小于V14,抛错,退出;否则继续向下执行,进入init

    image-20240620164507358

createReactApp.js

init

image-20240620165224025

  • 首先是解析用户输入的内容,代码如下:

  • const program = new commander.Command(packageJson.name)…option()…parse(process.argv)

  • 注意create-react-app命令并不是commander提供的,commander起到的作用是解析用户输入的内容并给出反馈。

  • 这一步通过用户输入获取几个变量

    • projectName

    • verbose:print additional logs

    • scriptsVersion:use a non-standard version of react-scripts

    • template:specify a template for the created project

    • usePnp

  • 另外后续通过 (process.env.npm_config_user_agent || ‘’).indexOf(‘yarn’)获取到了另一个重要变量useYarn;

  • 接下来就是检查cra的版本checkForLatestVersion,

checkForLatestVersion

cra最新版本号

image-20240620165251455

  • 获取cra最新版本号用到2个方法

    • // 正常获取时
      https.get('https://registry.npmjs.org/-/package/create-react-app/dist-tags'
      
    • // 出错时
      execSync('npm view create-react-app version').toString().trim();
      

接下来就进入createApp方法

createApp

创建react app

  • 首先检测Node版本是否支持

  • 然后做输入路径检查

    • const root = path.resolve(name);
      const appName = path.basename(root);
      
  • 调用checkAppName方法

  • 继续执行fs.ensureDirSync(fs为三方库fs-extra)进行异步创建目录

  • 判断isSafeToCreateProjectIn(root,name)冲突文件 conflicts.length为0,移除log文件,返回true

  • 写入package.json

  • process.chdir()方法是过程模块的内置应用程序编程接口,用于更改当前工作目录

  • 通过checkThatNpmCanReadCwd检查Npm是否可以读取Cwd

  • 如果不使用yarn,使用的是npm,需要检查npm的版本,执行的npmInfo

  • 执行run

checkAppName
  • 通过validate-npm-package-name进行检查,另外还检查了是否和'react', 'react-dom', 'react-scripts'重名

    • image-20240620170653920

    • image-20240620170715998

    • image-20240620170736010

isSafeToCreateProjectIn

判断isSafeToCreateProjectIn(root,name)冲突文件 conflicts.length为0,移除log文件,返回true

image-20240620171112147

checkThatNpmCanReadCwd

image-20240620172145525

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('');
        }

        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],
          `
        const 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 >=14 and npm >=6 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', '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);
      });
  });
}
getPackageInfo
  • 确定要安装的包

  • 确定要安装的react-scripts的版本,也支持其他的react-scripts

  • 确定template的包及版本,可以是cra-template,也支持其他的template

  • 安装的包还包括react、react-dom

checkIfOnline
  • 检查网络
install
  • 确定安装命令和参数,分别有
yarnpkg add --exact --offline --enable-pnp --cwd --verbose ...
npm install --no-audit --save --save-exact --loglevel error --verbose
  • 这些参数都是根据上面的流程确定,以spawn(command, args, { stdio: 'inherit' });执行
setCaretRangeForRuntimeDeps
  • 为指定的运行时依赖设置caret范围
executeNodeScript
  • 执行了react-scripts包中的init方法

cra-template

模板目录

├───template/
│   ├───public/
│   │   ├───favicon.ico
│   │   ├───index.html
│   │   ├───logo192.png
│   │   ├───logo512.png
│   │   ├───manifest.json
│   │   └───robots.txt
│   ├───src/
│   │   ├───App.css
│   │   ├───App.js
│   │   ├───App.test.js
│   │   ├───index.css
│   │   ├───index.js
│   │   ├───logo.svg
│   │   ├───reportWebVitals.js
│   │   └───setupTests.js
│   ├───gitignore
│   └───README.md
├───package.json
├───README.md
└───template.json

template目录下便是项目待copy的文件,而template.json则是这个template需要继续安装的依赖。 这些操作全都由react-scripts完成

react-scripts

scripts\init.js

  • 加载cra-template中的package.json,读取dependencies scripts

  • 更新package.json中的scripts

    image-20240620175531783

  • 更新package.json中的eslintConfig和browserslist

image-20240620175633957

  • 写入package.json

  • 拷贝模版项目到新建项目目录下

image-20240620175849612

  • 判断是否存在.gitignore

image-20240620175932515

  • 初始化git repo

image-20240621093251888

  • 确定执行 yarn or npm

image-20240621094736729

  • 判断是否安装react

image-20240621095123080

  • 安装依赖,通过const proc = spawn.sync(command, [remove, templateName], {stdio: 'inherit',});开启子进程执行安装命令

    image-20240621094838589

  • 删除template

    image-20240621095410116

  • git commit

image-20240621095511053

  • 显示最优雅的 cd 方式

image-20240621095640730

  • 打印成功信息的提示

image-20240621095725204

scripts\start.js

要初始化 webpack 配置,通过 webpack-dev-server 本地启动一个node服务

const fs = require('fs');
const chalk = require('react-dev-utils/chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
  choosePort,
  createCompiler,
  prepareProxy,
  prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
 
// 判断nodejs是否在终端运行
const isInteractive = process.stdout.isTTY;
 
// 校验入口文件
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
  process.exit(1);
}
 
// 设置默认的端口和HOST
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
 
/**
 * checkBrowsers内部使用browserslist,会从can-i-use数据库判断css、js支持的版本
 * 会先校验package.json文件里面有没有browserslist字段
 * 1.如果有直接返回promise
 * 2.没有的话会在终端询问是否要添加browserslist
*/
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
  .then(() => {
    /**
     * detect-port-alt校验当前端口是否被占用
     * 如果被占用,提示是否使用另外的端口
    */
    return choosePort(HOST, DEFAULT_PORT);
  })
  .then(port => {
    // 返回当前端口
    if (port == null) {
      // We have not found a port.
      return;
    }
 
    /**
     * configFactory有以下功能
     * 初始化webpack配置
     * 1.定义入口文件、输出文件
     * 2.定义规则:处理图片、字体、css、jsx
     * 3.使用插件
     *  - HtmlWebpackPlugin 为html自动插入输出的js
     *  - MiniCssExtractPlugin css压缩插件
     *  - WebpackManifestPlugin 生成manifest.json
     *  - ESLintPlugin 配置一些eslint规则
    */
    const config = configFactory('development');
    const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
    const appName = require(paths.appPackageJson).name;
 
    // 判断是否使用ts
    const useTypeScript = fs.existsSync(paths.appTsConfig);
    // 通过协议、域名、端口组合成完成的地址字符串
    const urls = prepareUrls(
      protocol,
      HOST,
      port,
      paths.publicUrlOrPath.slice(0, -1)
    );
    // 内部通过调用webpack(config) 生成一个compiler实例
    const compiler = createCompiler({
      appName,
      config,
      urls,
      useYarn,
      useTypeScript,
      webpack,
    });
    // 获取package.json文件中的proxy字段
    const proxySetting = require(paths.appPackageJson).proxy;
    // 配置一些代理相关的信息
    const proxyConfig = prepareProxy(
      proxySetting,
      paths.appPublic,
      paths.publicUrlOrPath
    );
    // 配置WebpackDevServer参数
    // https://github.com/webpack/webpack-dev-server
    const serverConfig = {
      ...createDevServerConfig(proxyConfig, urls.lanUrlForConfig),
      host: HOST,
      port,
    };
 
    // 创建本地服务器
    const devServer = new WebpackDevServer(serverConfig, compiler);
    // 服务启动后的回调
    devServer.startCallback(() => {
      if (isInteractive) {
        clearConsole();
      }
 
      if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
        console.log(
          chalk.yellow(
            `Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
          )
        );
      }
 
      console.log(chalk.cyan('Starting the development server...\n'));
      openBrowser(urls.localUrlForBrowser);
    });
  })

config/webpack配置

image-20240621100338833

react-scripts\config\webpack.config.js

// @remove-on-eject-begin
/**
 * 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.
 */
// @remove-on-eject-end
'use strict';

const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const ESLintPlugin = require('eslint-webpack-plugin');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin =
  process.env.TSC_COMPILE_ON_ERROR === 'true'
    ? require('react-dev-utils/ForkTsCheckerWarningWebpackPlugin')
    : require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
// @remove-on-eject-begin
const getCacheIdentifier = require('react-dev-utils/getCacheIdentifier');
// @remove-on-eject-end
const createEnvironmentHash = require('./webpack/persistentCache/createEnvironmentHash');

// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';

const reactRefreshRuntimeEntry = require.resolve('react-refresh/runtime');
const reactRefreshWebpackPluginRuntimeEntry = require.resolve(
  '@pmmmwh/react-refresh-webpack-plugin'
);
const babelRuntimeEntry = require.resolve('babel-preset-react-app');
const babelRuntimeEntryHelpers = require.resolve(
  '@babel/runtime/helpers/esm/assertThisInitialized',
  { paths: [babelRuntimeEntry] }
);
const babelRuntimeRegenerator = require.resolve('@babel/runtime/regenerator', {
  paths: [babelRuntimeEntry],
});

// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';

const emitErrorsAsWarnings = process.env.ESLINT_NO_DEV_ERRORS === 'true';
const disableESLintPlugin = process.env.DISABLE_ESLINT_PLUGIN === 'true';

const imageInlineSizeLimit = parseInt(
  process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);

// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);

// Check if Tailwind config exists
const useTailwind = fs.existsSync(
  path.join(paths.appPath, 'tailwind.config.js')
);

// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc;

// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;

const hasJsxRuntime = (() => {
  if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
    return false;
  }

  try {
    require.resolve('react/jsx-runtime');
    return true;
  } catch (e) {
    return false;
  }
})();

// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function (webpackEnv) {
  const isEnvDevelopment = webpackEnv === 'development';
  const isEnvProduction = webpackEnv === 'production';

  // Variable used for enabling profiling in Production
  // passed into alias object. Uses a flag if passed into the build command
  const isEnvProductionProfile =
    isEnvProduction && process.argv.includes('--profile');

  // We will provide `paths.publicUrlOrPath` to our app
  // as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
  // Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
  // Get environment variables to inject into our app.
  const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));

  const shouldUseReactRefresh = env.raw.FAST_REFRESH;

  // common function to get style loaders
  const getStyleLoaders = (cssOptions, preProcessor) => {
    const loaders = [
      isEnvDevelopment && require.resolve('style-loader'),
      isEnvProduction && {
        loader: MiniCssExtractPlugin.loader,
        // css is located in `static/css`, use '../../' to locate index.html folder
        // in production `paths.publicUrlOrPath` can be a relative path
        options: paths.publicUrlOrPath.startsWith('.')
          ? { publicPath: '../../' }
          : {},
      },
      {
        loader: require.resolve('css-loader'),
        options: cssOptions,
      },
      {
        // Options for PostCSS as we reference these options twice
        // Adds vendor prefixing based on your specified browser support in
        // package.json
        loader: require.resolve('postcss-loader'),
        options: {
          postcssOptions: {
            // Necessary for external CSS imports to work
            // https://github.com/facebook/create-react-app/issues/2677
            ident: 'postcss',
            config: false,
            plugins: !useTailwind
              ? [
                  'postcss-flexbugs-fixes',
                  [
                    'postcss-preset-env',
                    {
                      autoprefixer: {
                        flexbox: 'no-2009',
                      },
                      stage: 3,
                    },
                  ],
                  // Adds PostCSS Normalize as the reset css with default options,
                  // so that it honors browserslist config in package.json
                  // which in turn let's users customize the target behavior as per their needs.
                  'postcss-normalize',
                ]
              : [
                  'tailwindcss',
                  'postcss-flexbugs-fixes',
                  [
                    'postcss-preset-env',
                    {
                      autoprefixer: {
                        flexbox: 'no-2009',
                      },
                      stage: 3,
                    },
                  ],
                ],
          },
          sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
        },
      },
    ].filter(Boolean);
    if (preProcessor) {
      loaders.push(
        {
          loader: require.resolve('resolve-url-loader'),
          options: {
            sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
            root: paths.appSrc,
          },
        },
        {
          loader: require.resolve(preProcessor),
          options: {
            sourceMap: true,
          },
        }
      );
    }
    return loaders;
  };

  return {
    target: ['browserslist'],
    // Webpack noise constrained to errors and warnings
    stats: 'errors-warnings',
    mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
    // Stop compilation early in production
    bail: isEnvProduction,
    devtool: isEnvProduction
      ? shouldUseSourceMap
        ? 'source-map'
        : false
      : isEnvDevelopment && 'cheap-module-source-map',
    // These are the "entry points" to our application.
    // This means they will be the "root" imports that are included in JS bundle.
    entry: paths.appIndexJs,
    output: {
      // The build folder.
      path: paths.appBuild,
      // Add /* filename */ comments to generated require()s in the output.
      pathinfo: isEnvDevelopment,
      // There will be one main bundle, and one file per asynchronous chunk.
      // In development, it does not produce real files.
      filename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].js'
        : isEnvDevelopment && 'static/js/bundle.js',
      // There are also additional JS chunk files if you use code splitting.
      chunkFilename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].chunk.js'
        : isEnvDevelopment && 'static/js/[name].chunk.js',
      assetModuleFilename: 'static/media/[name].[hash][ext]',
      // webpack uses `publicPath` to determine where the app is being served from.
      // It requires a trailing slash, or the file assets will get an incorrect path.
      // We inferred the "public path" (such as / or /my-project) from homepage.
      publicPath: paths.publicUrlOrPath,
      // Point sourcemap entries to original disk location (format as URL on Windows)
      devtoolModuleFilenameTemplate: isEnvProduction
        ? info =>
            path
              .relative(paths.appSrc, info.absoluteResourcePath)
              .replace(/\\/g, '/')
        : isEnvDevelopment &&
          (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
    },
    cache: {
      type: 'filesystem',
      version: createEnvironmentHash(env.raw),
      cacheDirectory: paths.appWebpackCache,
      store: 'pack',
      buildDependencies: {
        defaultWebpack: ['webpack/lib/'],
        config: [__filename],
        tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
          fs.existsSync(f)
        ),
      },
    },
    infrastructureLogging: {
      level: 'none',
    },
    optimization: {
      minimize: isEnvProduction,
      minimizer: [
        // This is only used in production mode
        new TerserPlugin({
          terserOptions: {
            parse: {
              // We want terser to parse ecma 8 code. However, we don't want it
              // to apply any minification steps that turns valid ecma 5 code
              // into invalid ecma 5 code. This is why the 'compress' and 'output'
              // sections only apply transformations that are ecma 5 safe
              // https://github.com/facebook/create-react-app/pull/4234
              ecma: 8,
            },
            compress: {
              ecma: 5,
              warnings: false,
              // Disabled because of an issue with Uglify breaking seemingly valid code:
              // https://github.com/facebook/create-react-app/issues/2376
              // Pending further investigation:
              // https://github.com/mishoo/UglifyJS2/issues/2011
              comparisons: false,
              // Disabled because of an issue with Terser breaking valid code:
              // https://github.com/facebook/create-react-app/issues/5250
              // Pending further investigation:
              // https://github.com/terser-js/terser/issues/120
              inline: 2,
            },
            mangle: {
              safari10: true,
            },
            // Added for profiling in devtools
            keep_classnames: isEnvProductionProfile,
            keep_fnames: isEnvProductionProfile,
            output: {
              ecma: 5,
              comments: false,
              // Turned on because emoji and regex is not minified properly using default
              // https://github.com/facebook/create-react-app/issues/2488
              ascii_only: true,
            },
          },
        }),
        // This is only used in production mode
        new CssMinimizerPlugin(),
      ],
    },
    resolve: {
      // This allows you to set a fallback for where webpack should look for modules.
      // We placed these paths second because we want `node_modules` to "win"
      // if there are any conflicts. This matches Node resolution mechanism.
      // https://github.com/facebook/create-react-app/issues/253
      modules: ['node_modules', paths.appNodeModules].concat(
        modules.additionalModulePaths || []
      ),
      // These are the reasonable defaults supported by the Node ecosystem.
      // We also include JSX as a common component filename extension to support
      // some tools, although we do not recommend using it, see:
      // https://github.com/facebook/create-react-app/issues/290
      // `web` extension prefixes have been added for better support
      // for React Native Web.
      extensions: paths.moduleFileExtensions
        .map(ext => `.${ext}`)
        .filter(ext => useTypeScript || !ext.includes('ts')),
      alias: {
        // Support React Native Web
        // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
        'react-native': 'react-native-web',
        // Allows for better profiling with ReactDevTools
        ...(isEnvProductionProfile && {
          'react-dom$': 'react-dom/profiling',
          'scheduler/tracing': 'scheduler/tracing-profiling',
        }),
        ...(modules.webpackAliases || {}),
      },
      plugins: [
        // Prevents users from importing files from outside of src/ (or node_modules/).
        // This often causes confusion because we only process files within src/ with babel.
        // To fix this, we prevent you from importing files out of src/ -- if you'd like to,
        // please link the files into your node_modules/ and let module-resolution kick in.
        // Make sure your source files are compiled, as they will not be processed in any way.
        new ModuleScopePlugin(paths.appSrc, [
          paths.appPackageJson,
          reactRefreshRuntimeEntry,
          reactRefreshWebpackPluginRuntimeEntry,
          babelRuntimeEntry,
          babelRuntimeEntryHelpers,
          babelRuntimeRegenerator,
        ]),
      ],
    },
    module: {
      strictExportPresence: true,
      rules: [
        // Handle node_modules packages that contain sourcemaps
        shouldUseSourceMap && {
          enforce: 'pre',
          exclude: /@babel(?:\/|\\{1,2})runtime/,
          test: /\.(js|mjs|jsx|ts|tsx|css)$/,
          loader: require.resolve('source-map-loader'),
        },
        {
          // "oneOf" will traverse all following loaders until one will
          // match the requirements. When no loader matches it will fall
          // back to the "file" loader at the end of the loader list.
          oneOf: [
            // TODO: Merge this config once `image/avif` is in the mime-db
            // https://github.com/jshttp/mime-db
            {
              test: [/\.avif$/],
              type: 'asset',
              mimetype: 'image/avif',
              parser: {
                dataUrlCondition: {
                  maxSize: imageInlineSizeLimit,
                },
              },
            },
            // "url" loader works like "file" loader except that it embeds assets
            // smaller than specified limit in bytes as data URLs to avoid requests.
            // A missing `test` is equivalent to a match.
            {
              test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
              type: 'asset',
              parser: {
                dataUrlCondition: {
                  maxSize: imageInlineSizeLimit,
                },
              },
            },
            {
              test: /\.svg$/,
              use: [
                {
                  loader: require.resolve('@svgr/webpack'),
                  options: {
                    prettier: false,
                    svgo: false,
                    svgoConfig: {
                      plugins: [{ removeViewBox: false }],
                    },
                    titleProp: true,
                    ref: true,
                  },
                },
                {
                  loader: require.resolve('file-loader'),
                  options: {
                    name: 'static/media/[name].[hash].[ext]',
                  },
                },
              ],
              issuer: {
                and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
              },
            },
            // Process application JS with Babel.
            // The preset includes JSX, Flow, TypeScript, and some ESnext features.
            {
              test: /\.(js|mjs|jsx|ts|tsx)$/,
              include: paths.appSrc,
              loader: require.resolve('babel-loader'),
              options: {
                customize: require.resolve(
                  'babel-preset-react-app/webpack-overrides'
                ),
                presets: [
                  [
                    require.resolve('babel-preset-react-app'),
                    {
                      runtime: hasJsxRuntime ? 'automatic' : 'classic',
                    },
                  ],
                ],
                // @remove-on-eject-begin
                babelrc: false,
                configFile: false,
                // Make sure we have a unique cache identifier, erring on the
                // side of caution.
                // We remove this when the user ejects because the default
                // is sane and uses Babel options. Instead of options, we use
                // the react-scripts and babel-preset-react-app versions.
                cacheIdentifier: getCacheIdentifier(
                  isEnvProduction
                    ? 'production'
                    : isEnvDevelopment && 'development',
                  [
                    'babel-plugin-named-asset-import',
                    'babel-preset-react-app',
                    'react-dev-utils',
                    'react-scripts',
                  ]
                ),
                // @remove-on-eject-end
                plugins: [
                  isEnvDevelopment &&
                    shouldUseReactRefresh &&
                    require.resolve('react-refresh/babel'),
                ].filter(Boolean),
                // This is a feature of `babel-loader` for webpack (not Babel itself).
                // It enables caching results in ./node_modules/.cache/babel-loader/
                // directory for faster rebuilds.
                cacheDirectory: true,
                // See #6846 for context on why cacheCompression is disabled
                cacheCompression: false,
                compact: isEnvProduction,
              },
            },
            // Process any JS outside of the app with Babel.
            // Unlike the application JS, we only compile the standard ES features.
            {
              test: /\.(js|mjs)$/,
              exclude: /@babel(?:\/|\\{1,2})runtime/,
              loader: require.resolve('babel-loader'),
              options: {
                babelrc: false,
                configFile: false,
                compact: false,
                presets: [
                  [
                    require.resolve('babel-preset-react-app/dependencies'),
                    { helpers: true },
                  ],
                ],
                cacheDirectory: true,
                // See #6846 for context on why cacheCompression is disabled
                cacheCompression: false,
                // @remove-on-eject-begin
                cacheIdentifier: getCacheIdentifier(
                  isEnvProduction
                    ? 'production'
                    : isEnvDevelopment && 'development',
                  [
                    'babel-plugin-named-asset-import',
                    'babel-preset-react-app',
                    'react-dev-utils',
                    'react-scripts',
                  ]
                ),
                // @remove-on-eject-end
                // Babel sourcemaps are needed for debugging into node_modules
                // code.  Without the options below, debuggers like VSCode
                // show incorrect code and set breakpoints on the wrong lines.
                sourceMaps: shouldUseSourceMap,
                inputSourceMap: shouldUseSourceMap,
              },
            },
            // "postcss" loader applies autoprefixer to our CSS.
            // "css" loader resolves paths in CSS and adds assets as dependencies.
            // "style" loader turns CSS into JS modules that inject <style> tags.
            // In production, we use MiniCSSExtractPlugin to extract that CSS
            // to a file, but in development "style" loader enables hot editing
            // of CSS.
            // By default we support CSS Modules with the extension .module.css
            {
              test: cssRegex,
              exclude: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction
                  ? shouldUseSourceMap
                  : isEnvDevelopment,
                modules: {
                  mode: 'icss',
                },
              }),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true,
            },
            // Adds support for CSS Modules (https://github.com/css-modules/css-modules)
            // using the extension .module.css
            {
              test: cssModuleRegex,
              use: getStyleLoaders({
                importLoaders: 1,
                sourceMap: isEnvProduction
                  ? shouldUseSourceMap
                  : isEnvDevelopment,
                modules: {
                  mode: 'local',
                  getLocalIdent: getCSSModuleLocalIdent,
                },
              }),
            },
            // Opt-in support for SASS (using .scss or .sass extensions).
            // By default we support SASS Modules with the
            // extensions .module.scss or .module.sass
            {
              test: sassRegex,
              exclude: sassModuleRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 3,
                  sourceMap: isEnvProduction
                    ? shouldUseSourceMap
                    : isEnvDevelopment,
                  modules: {
                    mode: 'icss',
                  },
                },
                'sass-loader'
              ),
              // Don't consider CSS imports dead code even if the
              // containing package claims to have no side effects.
              // Remove this when webpack adds a warning or an error for this.
              // See https://github.com/webpack/webpack/issues/6571
              sideEffects: true,
            },
            // Adds support for CSS Modules, but using SASS
            // using the extension .module.scss or .module.sass
            {
              test: sassModuleRegex,
              use: getStyleLoaders(
                {
                  importLoaders: 3,
                  sourceMap: isEnvProduction
                    ? shouldUseSourceMap
                    : isEnvDevelopment,
                  modules: {
                    mode: 'local',
                    getLocalIdent: getCSSModuleLocalIdent,
                  },
                },
                'sass-loader'
              ),
            },
            // "file" loader makes sure those assets get served by WebpackDevServer.
            // When you `import` an asset, you get its (virtual) filename.
            // In production, they would get copied to the `build` folder.
            // This loader doesn't use a "test" so it will catch all modules
            // that fall through the other loaders.
            {
              // Exclude `js` files to keep "css" loader working as it injects
              // its runtime that would otherwise be processed through "file" loader.
              // Also exclude `html` and `json` extensions so they get processed
              // by webpacks internal loaders.
              exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
              type: 'asset/resource',
            },
            // ** STOP ** Are you adding a new loader?
            // Make sure to add the new loader(s) before the "file" loader.
          ],
        },
      ].filter(Boolean),
    },
    plugins: [
      // Generates an `index.html` file with the <script> injected.
      new HtmlWebpackPlugin(
        Object.assign(
          {},
          {
            inject: true,
            template: paths.appHtml,
          },
          isEnvProduction
            ? {
                minify: {
                  removeComments: true,
                  collapseWhitespace: true,
                  removeRedundantAttributes: true,
                  useShortDoctype: true,
                  removeEmptyAttributes: true,
                  removeStyleLinkTypeAttributes: true,
                  keepClosingSlash: true,
                  minifyJS: true,
                  minifyCSS: true,
                  minifyURLs: true,
                },
              }
            : undefined
        )
      ),
      // Inlines the webpack runtime script. This script is too small to warrant
      // a network request.
      // https://github.com/facebook/create-react-app/issues/5358
      isEnvProduction &&
        shouldInlineRuntimeChunk &&
        new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
      // Makes some environment variables available in index.html.
      // The public URL is available as %PUBLIC_URL% in index.html, e.g.:
      // <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
      // It will be an empty string unless you specify "homepage"
      // in `package.json`, in which case it will be the pathname of that URL.
      new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
      // This gives some necessary context to module not found errors, such as
      // the requesting resource.
      new ModuleNotFoundPlugin(paths.appPath),
      // Makes some environment variables available to the JS code, for example:
      // if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
      // It is absolutely essential that NODE_ENV is set to production
      // during a production build.
      // Otherwise React will be compiled in the very slow development mode.
      new webpack.DefinePlugin(env.stringified),
      // Experimental hot reloading for React .
      // https://github.com/facebook/react/tree/main/packages/react-refresh
      isEnvDevelopment &&
        shouldUseReactRefresh &&
        new ReactRefreshWebpackPlugin({
          overlay: false,
        }),
      // Watcher doesn't work well if you mistype casing in a path so we use
      // a plugin that prints an error when you attempt to do this.
      // See https://github.com/facebook/create-react-app/issues/240
      isEnvDevelopment && new CaseSensitivePathsPlugin(),
      isEnvProduction &&
        new MiniCssExtractPlugin({
          // Options similar to the same options in webpackOptions.output
          // both options are optional
          filename: 'static/css/[name].[contenthash:8].css',
          chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
        }),
      // Generate an asset manifest file with the following content:
      // - "files" key: Mapping of all asset filenames to their corresponding
      //   output file so that tools can pick it up without having to parse
      //   `index.html`
      // - "entrypoints" key: Array of files which are included in `index.html`,
      //   can be used to reconstruct the HTML if necessary
      new WebpackManifestPlugin({
        fileName: 'asset-manifest.json',
        publicPath: paths.publicUrlOrPath,
        generate: (seed, files, entrypoints) => {
          const manifestFiles = files.reduce((manifest, file) => {
            manifest[file.name] = file.path;
            return manifest;
          }, seed);
          const entrypointFiles = entrypoints.main.filter(
            fileName => !fileName.endsWith('.map')
          );

          return {
            files: manifestFiles,
            entrypoints: entrypointFiles,
          };
        },
      }),
      // Moment.js is an extremely popular library that bundles large locale files
      // by default due to how webpack interprets its code. This is a practical
      // solution that requires the user to opt into importing specific locales.
      // https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
      // You can remove this if you don't use Moment.js:
      new webpack.IgnorePlugin({
        resourceRegExp: /^\.\/locale$/,
        contextRegExp: /moment$/,
      }),
      // Generate a service worker script that will precache, and keep up to date,
      // the HTML & assets that are part of the webpack build.
      isEnvProduction &&
        fs.existsSync(swSrc) &&
        new WorkboxWebpackPlugin.InjectManifest({
          swSrc,
          dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
          exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
          // Bump up the default maximum size (2mb) that's precached,
          // to make lazy-loading failure scenarios less likely.
          // See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
          maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
        }),
      // TypeScript type checking
      useTypeScript &&
        new ForkTsCheckerWebpackPlugin({
          async: isEnvDevelopment,
          typescript: {
            typescriptPath: resolve.sync('typescript', {
              basedir: paths.appNodeModules,
            }),
            configOverwrite: {
              compilerOptions: {
                sourceMap: isEnvProduction
                  ? shouldUseSourceMap
                  : isEnvDevelopment,
                skipLibCheck: true,
                inlineSourceMap: false,
                declarationMap: false,
                noEmit: true,
                incremental: true,
                tsBuildInfoFile: paths.appTsBuildInfoFile,
              },
            },
            context: paths.appPath,
            diagnosticOptions: {
              syntactic: true,
            },
            mode: 'write-references',
            // profile: true,
          },
          issue: {
            // This one is specifically to match during CI tests,
            // as micromatch doesn't match
            // '../cra-template-typescript/template/src/App.tsx'
            // otherwise.
            include: [
              { file: '../**/src/**/*.{ts,tsx}' },
              { file: '**/src/**/*.{ts,tsx}' },
            ],
            exclude: [
              { file: '**/src/**/__tests__/**' },
              { file: '**/src/**/?(*.){spec|test}.*' },
              { file: '**/src/setupProxy.*' },
              { file: '**/src/setupTests.*' },
            ],
          },
          logger: {
            infrastructure: 'silent',
          },
        }),
      !disableESLintPlugin &&
        new ESLintPlugin({
          // Plugin options
          extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
          formatter: require.resolve('react-dev-utils/eslintFormatter'),
          eslintPath: require.resolve('eslint'),
          failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
          context: paths.appSrc,
          cache: true,
          cacheLocation: path.resolve(
            paths.appNodeModules,
            '.cache/.eslintcache'
          ),
          // ESLint class options
          cwd: paths.appPath,
          resolvePluginsRelativeTo: __dirname,
          baseConfig: {
            extends: [require.resolve('eslint-config-react-app/base')],
            rules: {
              ...(!hasJsxRuntime && {
                'react/react-in-jsx-scope': 'error',
              }),
            },
          },
        }),
    ].filter(Boolean),
    // Turn off performance processing because we utilize
    // our own hints via the FileSizeReporter
    performance: false,
  };
};

target
  • 如果项目中有browserslist配置,webpack将会用它 - 确定可用于生成运行时代码的 ES 功能 - 推断环境,可以不再配置output.environment
mode
  • mode设置为’production’,会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 TerserPlugi
bail
  • 在生产环境编译遇到错误直接抛出并终止
devtool
  • devtool: 生产环境使用shouldUseSourceMap控制是否需要source map。在实践中,生产环境打包可能需要生成source map方便监控或日志平台进行定位,但也可能不需要,因此这里设置了一个变量,由process.env.GENERATE_SOURCEMAP控制
entry
  • 入口文件
output
  • 输出配置

     output: {
        // 输出目录,比如这里是'D:\\cra-demo1\\build'.
        path: paths.appBuild,
         // 开发环境输出代码里增加/* filename */注释
        pathinfo: isEnvDevelopment,
        // 主bundle
        filename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].js'
        : isEnvDevelopment && 'static/js/bundle.js',
        // 代码分隔后的chunk 文件
        chunkFilename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].chunk.js'
        : isEnvDevelopment && 'static/js/[name].chunk.js',
        assetModuleFilename: 'static/media/[name].[hash][ext]',
        // 这里是'/'
        publicPath: paths.publicUrlOrPath,
        // source map路径
        devtoolModuleFilenameTemplate: isEnvProduction
        ? info =>
            path
                .relative(paths.appSrc, info.absoluteResourcePath)
                .replace(/\\/g, '/')
        : isEnvDevelopment &&
            (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
    },
    
    
cache
  • webpack5自带的缓存,加快构建速度

    cache: {
        // 将缓存保存在文件中
        type: 'filesystem',
        // 缓存版本,版本更新将使原缓存失效
        version: createEnvironmentHash(env.raw),
        // 缓存的目录:node_modules/.cache
        cacheDirectory: paths.appWebpackCache,
        // 当编译器空闲时将数据存储在一个文件中,用于所有缓存项
        store: 'pack',
        // 缓存的依赖,这里的变更将使缓存失效
        buildDependencies: {
            defaultWebpack: ['webpack/lib/'],
            config: [__filename],
            tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
                fs.existsSync(f)
            ),
        },
    },
    
    
infrastructureLogging
  • infrastructureLogging:基础日志级别,这里不开启,cra有自己的日志
optimization
  • optimization: 代码优化,比如压缩、分割等

  • js压缩

    • new TerserPlugin({
          terserOptions: {
          parse: {
              // 以es8语法解析
              ecma: 8,
          },
          compress: {
              ecma: 5,
              warnings: false,
              comparisons: false,
              inline: 2,
          },
          mangle: {
              // 解决Safari 10中的一个bug
              safari10: true,
          },
          // 是否保留classnames
          keep_classnames: isEnvProductionProfile,
          keep_fnames: isEnvProductionProfile,
          output: {
              ecma: 5,
              comments: false,
              ascii_only: true,
          },
          },
      }),
      
      
  • css 压缩

    • new CssMinimizerPlugin(),
      
resolve

resolve:帮助找到模块的路径

  • modules:从哪里找模块

    resolve: {
        // 这个配置主要考虑了monorepo的场景
        modules: ['node_modules', paths.appNodeModules].concat(
            modules.additionalModulePaths || []
        ),
       ...
    },
    
    
  • extensions: 可以省略的后缀名,包含了: [ ‘web.mjs’, ‘mjs’, ‘web.js’, ‘js’, ‘web.ts’, ‘ts’, ‘web.tsx’, ‘tsx’, ‘json’, ‘web.jsx’, ‘jsx’, ];

     extensions: paths.moduleFileExtensions
            .map(ext => `.${ext}`)
            .filter(ext => useTypeScript || !ext.includes('ts')),
        
    
  • alias:modules.webpackAliases中只有src

    alias: {
            'react-native': 'react-native-web',
            // Allows for better profiling with ReactDevTools
            ...(isEnvProductionProfile && {
                'react-dom$': 'react-dom/profiling',
                'scheduler/tracing': 'scheduler/tracing-profiling',
            }),
            // 这里只设置了src
            ...(modules.webpackAliases || {}),
        },
        
    
    
  • plugins

    plugins: [
        // 这个插件用来防止用户从src之外的地方导入文件
            new ModuleScopePlugin(paths.appSrc, [
                paths.appPackageJson,
                reactRefreshRuntimeEntry,
                reactRefreshWebpackPluginRuntimeEntry,
                babelRuntimeEntry,
                babelRuntimeEntryHelpers,
                babelRuntimeRegenerator,
            ]),
        ],
    
    
module

如何处理不同类型的模块

  • strictExportPresence
module: {
    // 将缺失的导出作为error,而不是warning
    strictExportPresence: true,
    ...
   
},
  • rules: 这里用了oneOfapi,遇到第一个匹配的就会终止,如果没有匹配的,就会执行最下面的
 rules: [
        // 处理第三方库的source map
        shouldUseSourceMap && {
            enforce: 'pre',
            exclude: /@babel(?:\/|\\{1,2})runtime/,
            test: /\.(js|mjs|jsx|ts|tsx|css)$/,
            loader: require.resolve('source-map-loader'),
        },
        {
            // "oneOf" 遍历下面所有的loader,直到第一个符合的,如果没有找到,则使用最下面的'file loader'
            // webpack5取消了file-loader,因此这里加了个引号
            oneOf: [
                {
                    test: [/\.avif$/],
                    type: 'asset',
                    mimetype: 'image/avif',
                    // 这个是webpack5的配置,取消raw-loader、url-loader、file-loader
                    parser: {
                        dataUrlCondition: {
                            maxSize: imageInlineSizeLimit,
                        },
                    },
                },
                {
                    test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
                    type: 'asset',
                    parser: {
                    dataUrlCondition: {
                        maxSize: imageInlineSizeLimit,
                    },
                    },
                },
                {
                    test: /\.svg$/,
                    use: [
                        {
                            //可以将svg以组件的形式导入 import Star from './star.svg'
                            loader: require.resolve('@svgr/webpack'),
                            options: {
                                prettier: false,
                                svgo: false,
                                svgoConfig: {
                                    plugins: [{ removeViewBox: false }],
                                },
                                titleProp: true,
                                ref: true,
                            },
                        },
                        {
                            loader: require.resolve('file-loader'),
                            options: {
                            name: 'static/media/[name].[hash].[ext]',
                            },
                        },
                    ],
                    // 在这些条件中生效
                    issuer: {
                    and: [/\.(ts|tsx|js|jsx|md|mdx)$/],
                    },
                },
                {
                    test: /\.(js|mjs|jsx|ts|tsx)$/,
                    include: paths.appSrc,
                    loader: require.resolve('babel-loader'),
                    options: {
                        // babel-preset-react-app是cra自定义的preset,包括了 JSX, Flow, TypeScript, and some ESnext features
                        customize: require.resolve(
                            'babel-preset-react-app/webpack-overrides'
                        ),
                        presets: [
                            [
                            require.resolve('babel-preset-react-app'),
                            {
                                runtime: hasJsxRuntime ? 'automatic' : 'classic',
                            },
                            ],
                        ],
                        // 一下两个eject后会移除
                        babelrc: false,
                        configFile: false,
                        // 确保 cache identifier的唯一性,eject后会移除
                        cacheIdentifier: getCacheIdentifier(
                            isEnvProduction
                            ? 'production'
                            : isEnvDevelopment && 'development',
                            [
                            'babel-plugin-named-asset-import',
                            'babel-preset-react-app',
                            'react-dev-utils',
                            'react-scripts',
                            ]
                        ),
                        plugins: [
                            isEnvDevelopment &&
                            shouldUseReactRefresh &&
                            require.resolve('react-refresh/babel'),
                        ].filter(Boolean),
                        // babel-loader能将缓存保存在./node_modules/.cache/babel-loader/
                        cacheDirectory: true,
                        cacheCompression: false,
                        compact: isEnvProduction,
                    },
                },
                // 处理其他js
                {
                    test: /\.(js|mjs)$/,
                    exclude: /@babel(?:\/|\\{1,2})runtime/,
                    loader: require.resolve('babel-loader'),
                    options: {
                        ... 同上
                    },
                },
                {
                    test: cssRegex,
                    exclude: cssModuleRegex,
                    use: getStyleLoaders({
                    importLoaders: 1,
                    sourceMap: isEnvProduction
                        ? shouldUseSourceMap
                        : isEnvDevelopment,
                    modules: {
                        mode: 'icss',
                    },
                    }),
                    sideEffects: true,
                },
                {
                    test: cssModuleRegex,
                    use: getStyleLoaders({
                    importLoaders: 1,
                    sourceMap: isEnvProduction
                        ? shouldUseSourceMap
                        : isEnvDevelopment,
                    modules: {
                        mode: 'local',
                        getLocalIdent: getCSSModuleLocalIdent,
                    },
                    }),
                },
                {
                    test: sassRegex,
                    exclude: sassModuleRegex,
                    use: getStyleLoaders(
                    {
                        importLoaders: 3,
                        sourceMap: isEnvProduction
                        ? shouldUseSourceMap
                        : isEnvDevelopment,
                        modules: {
                        mode: 'icss',
                        },
                    },
                    'sass-loader'
                    ),
                    sideEffects: true,
                },
                {
                    test: sassModuleRegex,
                    use: getStyleLoaders(
                    {
                        importLoaders: 3,
                        sourceMap: isEnvProduction
                        ? shouldUseSourceMap
                        : isEnvDevelopment,
                        modules: {
                        mode: 'local',
                        getLocalIdent: getCSSModuleLocalIdent,
                        },
                    },
                    'sass-loader'
                    ),
                },
                // 兜底的'file loader'
                {
                    exclude: [/^$/, /\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
                    type: 'asset/resource',
                },
            ],
        },
    ].filter(Boolean),

plugins

各种插件

plugins: [
    new HtmlWebpackPlugin(
    Object.assign(
        {},
        {
        inject: true,
        template: paths.appHtml,
        },
        isEnvProduction
        ? {
            minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
            },
            }
        : undefined
    )
    ),
    isEnvProduction &&
    shouldInlineRuntimeChunk &&
    new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
    // 指定index.html中可以使用的变量,如<link rel="icon" href="%PUBLIC_URL%/favicon.ico">
    new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
    new ModuleNotFoundPlugin(paths.appPath),
    new webpack.DefinePlugin(env.stringified),
    isEnvDevelopment &&
    shouldUseReactRefresh &&
    new ReactRefreshWebpackPlugin({
        overlay: false,
    }),
    // 大小写敏感,这个插件挺有用的
    isEnvDevelopment && new CaseSensitivePathsPlugin(),
    isEnvProduction &&
    new MiniCssExtractPlugin({
        filename: 'static/css/[name].[contenthash:8].css',
        chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
    }),
 
    new WebpackManifestPlugin({
        fileName: 'asset-manifest.json',
        publicPath: paths.publicUrlOrPath,
        generate: (seed, files, entrypoints) => {
            const manifestFiles = files.reduce((manifest, file) => {
            manifest[file.name] = file.path;
            return manifest;
            }, seed);
            const entrypointFiles = entrypoints.main.filter(
            fileName => !fileName.endsWith('.map')
            );

            return {
            files: manifestFiles,
            entrypoints: entrypointFiles,
            };
        },
    }),
    // 不打包momentjs中的语言包
    new webpack.IgnorePlugin({
        resourceRegExp: /^\.\/locale$/,
        contextRegExp: /moment$/,
    }),
    // service worker
    isEnvProduction &&
    fs.existsSync(swSrc) &&
    new WorkboxWebpackPlugin.InjectManifest({
        swSrc,
        dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
        exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
        maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
    }),
    // 改动ts文件触发类型检查
    useTypeScript &&
    new ForkTsCheckerWebpackPlugin({
        async: isEnvDevelopment,
        typescript: {
            typescriptPath: resolve.sync('typescript', {
                basedir: paths.appNodeModules,
            }),
            configOverwrite: {
                compilerOptions: {
                sourceMap: isEnvProduction
                    ? shouldUseSourceMap
                    : isEnvDevelopment,
                skipLibCheck: true,
                inlineSourceMap: false,
                declarationMap: false,
                noEmit: true,
                incremental: true,
                tsBuildInfoFile: paths.appTsBuildInfoFile,
                },
            },
            context: paths.appPath,
            diagnosticOptions: {
                syntactic: true,
            },
            mode: 'write-references',
        },
        issue: {
            include: [
                { file: '../**/src/**/*.{ts,tsx}' },
                { file: '**/src/**/*.{ts,tsx}' },
            ],
            exclude: [
                { file: '**/src/**/__tests__/**' },
                { file: '**/src/**/?(*.){spec|test}.*' },
                { file: '**/src/setupProxy.*' },
                { file: '**/src/setupTests.*' },
            ],
        },
        logger: {
            infrastructure: 'silent',
        },
    }),
    !disableESLintPlugin &&
    new ESLintPlugin({
        extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
        formatter: require.resolve('react-dev-utils/eslintFormatter'),
        eslintPath: require.resolve('eslint'),
        failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
        context: paths.appSrc,
        cache: true,
        cacheLocation: path.resolve(
            paths.appNodeModules,
            '.cache/.eslintcache'
        ),
        // ESLint class options
        cwd: paths.appPath,
        resolvePluginsRelativeTo: __dirname,
        baseConfig: {
        extends: [require.resolve('eslint-config-react-app/base')],
        rules: {
            ...(!hasJsxRuntime && {
            'react/react-in-jsx-scope': 'error',
            }),
        },
        },
    }),
].filter(Boolean),

};

performance

performance:CRA自带了FileSizeReporter

  • performance: false

实现

1.create-react-app

下载

git clone https://github.com/facebook/create-react-app.git --depth=1
cd create-react-app
yarn install

package.json

package.json

"scripts": {
+  "create": "node ./packages/create-react-app/index.js",
}

重要步骤

  • 将命令行参数发送到npm脚本

    npm run [command] [-- <args>]
    
    yarn install  #安装项止依赖和软链接
    npm run create -- aaa  #执行创建命令
    Installing packages. This might take a couple of minutes. #安装依赖包
    Installing react, react-dom, and react-scripts with cra-template... #安装依赖包
    Installing template dependencies using yarnpkg... #安装模板依赖
    Removing template package using yarnpkg... #移除模板模块
    Removing module cra-template...  #移除cra-template模块
    Success! Created aaa at C:\aprepare\create-react-app\aaa #成功创建
    Inside that directory, you can run several commands: #执行命令
    cd aaa
    yarn start
    

.vscode\launch.json

.vscode\launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch via NPM",
            "request": "launch",
            "runtimeArgs": [
                "run-script",
                "create"
            ],
            "runtimeExecutable": "npm",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "type": "pwa-node"
        }
    ]
}

2.实现init方法

2.1 package.json

package.json

  "scripts": {
+    "version": "node ./packages/create-react-app3/index.js --version",
+    "create": "node ./packages/create-react-app3/index.js aaa"
  }

2.2 create-react-app3\package.json

packages\create-react-app3\package.json

{
+ "main": "./index.js"
}

2.3 create-react-app3\index.js

packages\create-react-app3\index.js

#!/usr/bin/env node
const { init } = require('./createReactApp');
init();

2.4 createReactApp.js

packages\create-react-app3\createReactApp.js

const {Command} = require('commander');
const chalk = require('chalk');
const packageJson = require('./package.json');
let appName;
async function init() {
    new Command(packageJson.name)
        .version(packageJson.version)
        .arguments('<project-directory>')
        .usage(`${chalk.green('<project-directory>')} [options]`)
        .action(projectDirectory => {
            appName = projectDirectory;
        })
        .parse(process.argv);
    console.log('appName=', appName);
}
module.exports = {
    init
}

2.5 执行命令

npm run create

3.实现createApp方法

3.1 createReactApp.js

packages\create-react-app3\createReactApp.js

const {Command} = require('commander');
const chalk = require('chalk');
+const fs = require('fs-extra');
+const path = require('path');
const packageJson = require('./package.json');
let appName;
async function init() {
    new Command(packageJson.name)
        .version(packageJson.version)
        .arguments('<project-directory>')
        .usage(`${chalk.green('<project-directory>')} [options]`)
        .action(projectDirectory => {
            appName = projectDirectory;
        })
        .parse(process.argv);
    console.log('appName=', appName);
+   await createApp(appName);
}
+async function createApp(appName) {
+    const root = path.resolve(appName);
+    fs.ensureDirSync(appName);
+    console.log(`Creating a new React app in ${chalk.green(root)}.`);
+    const packageJson = {
+      name: appName,
+      version: '0.1.0',
+      private: true,
+    };
+    fs.writeFileSync(
+      path.join(root, 'package.json'),
+      JSON.stringify(packageJson, null, 2)
+    );
+    const originalDirectory = process.cwd();
+    process.chdir(root);
+    console.log('root',root);
+    console.log('appName',appName);
+    console.log('originalDirectory',originalDirectory);
+  }
module.exports = {
    init
}

4.实现run方法

4.1 createReactApp.js

packages\create-react-app3\createReactApp.js

const {Command} = require('commander');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
+const spawn = require('cross-spawn');
const packageJson = require('./package.json');
let appName;
async function init() {
    new Command(packageJson.name)
        .version(packageJson.version)
        .arguments('<project-directory>')
        .usage(`${chalk.green('<project-directory>')} [options]`)
        .action(projectDirectory => {
            appName = projectDirectory;
        })
        .parse(process.argv);
    console.log('appName=', appName);
    await createApp(appName);
}
async function createApp(appName) {
    const root = path.resolve(appName);
    fs.ensureDirSync(appName);
    console.log(`Creating a new React app in ${chalk.green(root)}.`);
    const packageJson = {
      name: appName,
      version: '0.1.0',
      private: true,
    };
    fs.writeFileSync(
      path.join(root, 'package.json'),
      JSON.stringify(packageJson, null, 2)
    );
    const originalDirectory = process.cwd();
    process.chdir(root);
    console.log('root',root);
    console.log('appName',appName);
    console.log('originalDirectory',originalDirectory);
+   await run(
+        root,
+        appName,
+        originalDirectory
+   );
}
+async function run(root,appName,originalDirectory) {
+    const scriptName = 'react-scripts';
+    const templateName = 'cra-template';
+    const allDependencies = ['react', 'react-dom', scriptName, templateName];
+    console.log('Installing packages. This might take a couple of minutes.');
+    console.log(
+      `Installing ${chalk.cyan('react')}, ${chalk.cyan(
+        'react-dom'
+      )}, and ${chalk.cyan(scriptName)} with ${chalk.cyan(templateName)}`
+    );
+    await install(root, allDependencies);
+}
+function install(root, allDependencies) {
+    return new Promise((resolve) => {
+      const command = 'yarnpkg';
+      const args = ['add', '--exact', ...allDependencies, '--cwd', root];
+      console.log('command:',command,args);
+      const child = spawn(command, args, { stdio: 'inherit' });
+      child.on('close', resolve);
+    });
+}
module.exports = {
    init
}

command: yarnpkg [
  'add',
  '--exact',
  'react',
  'react-dom',
  'react-scripts',
  'cra-template',
  '--cwd',
  'C:\\aprepare\\create-react-app3\\aaa'
]

yarnpkg add --exact react react-dom react-scripts cra-template --cwd C:\\aprepare\\create-react-app3\\aaa

5.执行init初始化命令

5.1 createReactApp.js

packages\create-react-app3\createReactApp.js

const {Command} = require('commander');
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('path');
const spawn = require('cross-spawn');
const packageJson = require('./package.json');
let appName;
async function init() {
    new Command(packageJson.name)
        .version(packageJson.version)
        .arguments('<project-directory>')
        .usage(`${chalk.green('<project-directory>')} [options]`)
        .action(projectDirectory => {
            appName = projectDirectory;
        })
        .parse(process.argv);
    console.log('appName=', appName);
    await createApp(appName);
}
async function createApp(appName) {
    const root = path.resolve(appName);
    fs.ensureDirSync(appName);
    console.log(`Creating a new React app in ${chalk.green(root)}.`);
    const packageJson = {
      name: appName,
      version: '0.1.0',
      private: true,
    };
    fs.writeFileSync(
      path.join(root, 'package.json'),
      JSON.stringify(packageJson, null, 2)
    );
    const originalDirectory = process.cwd();
    process.chdir(root);
    console.log('root',root);
    console.log('appName',appName);
    console.log('originalDirectory',originalDirectory);
    await run(
        root,
        appName,
        originalDirectory
    );
}
async function run(root,appName,originalDirectory) {
    const scriptName = 'react-scripts';
    const templateName = 'cra-template';
    const allDependencies = ['react', 'react-dom', scriptName, templateName];
    console.log('Installing packages. This might take a couple of minutes.');
    console.log(
      `Installing ${chalk.cyan('react')}, ${chalk.cyan(
        'react-dom'
      )}, and ${chalk.cyan(scriptName)} with ${chalk.cyan(templateName)}`
    );
    await install(root, allDependencies);
+    let data = [root, appName, true, originalDirectory, templateName];
+    let source = `
+    var init = require('react-scripts/scripts/init.js');
+    init.apply(null, JSON.parse(process.argv[1]));
+  `
+    await executeNodeScript({ cwd: process.cwd() }, data, source);
+    console.log('Done.');
+    process.exit(0);
}
+function executeNodeScript({ cwd }, data, source) {
+  return new Promise((resolve) => {
+    const child = spawn( 
+      process.execPath,
+      ['-e', source, '--', JSON.stringify(data)],
+      { cwd, stdio: 'inherit' }
+    );
+    child.on('close', resolve);
+  });
+}
function install(root, allDependencies) {
    return new Promise((resolve) => {
      const command = 'yarnpkg';
      const args = ['add', '--exact', ...allDependencies, '--cwd', root];
      console.log('command:',command,args);
      const child = spawn(command, args, { stdio: 'inherit' });
      child.on('close', resolve);
    });
}
module.exports = {
    init
}
  • 35
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值