构建系统

概述

ncc 使用自举(bootstrapping)策略:用 ncc 自己来编译自己。构建脚本 scripts/build.js 调用 ncc 的 API 逐个编译源文件,输出到 dist/ncc/

构建入口

pnpm build        # 使用缓存
# 等价于
node scripts/build.js

# 无缓存构建(发布前)
node scripts/build.js --no-cache

构建流程

flowchart TD
    A["清理 dist/ 中的 .js/.cache/.ts 文件"] --> B["编译 CLI"]
    B --> C["编译核心引擎"]
    C --> D["编译 relocate-loader"]
    D --> E["编译 shebang-loader"]
    E --> F["编译 ts-loader"]
    F --> G["编译 stringify-loader"]
    G --> H["编译 source-map-support"]
    H --> I["写入所有产物"]
    I --> J["复制 TypeScript 类型文件"]
    J --> K["打印文件大小报告"]

各构建目标

CLI (src/cli.js)

// scripts/build.js:18-29
const { code: cli, assets: cliAssets } = await ncc(
  __dirname + "/../src/cli",
  {
    filename: "cli.js",
    externals: ["./index.js"],   // CLI 动态 require 核心引擎
    license: 'LICENSES.txt',
    minify, cache, v8cache
  }
);

CLI 将 ./index.js 标记为 external,因为核心引擎是单独编译的。运行时 CLI 的 require("./index.js") 会加载同目录下的 index.js

核心引擎 (src/index.js)

// scripts/build.js:31-39
const { code: index, assets: indexAssets } = await ncc(
  __dirname + "/../src/index",
  { filename: "index.js", minify, cache, v8cache }
);

核心引擎不设置 externals,所有依赖(webpack、terser、memory-fs 等)都被打包进去。

已知的合法资源输出:locales/(来自某些依赖的 i18n 文件)、worker.jsindex1.jsminify.js

Loaders

每个 Loader 单独编译为一个文件:

入口输出特殊处理
src/loaders/relocate-loaderdist/ncc/loaders/relocate-loader.js
src/loaders/shebang-loaderdist/ncc/loaders/shebang-loader.js
src/loaders/ts-loaderdist/ncc/loaders/ts-loader.jsnoAssetBuilds: true
src/loaders/stringify-loaderdist/ncc/loaders/stringify-loader.js

Source Map Support

// scripts/build.js:74-78
const { code: sourcemapSupport } = await ncc(
  require.resolve("source-map-support/register"),
  { filename: "sourcemap-register.js", minify, cache, v8cache }
);

source-map-support 包编译为独立文件,运行时按需加载。

非编译文件(直接复制)

以下文件不经过 Webpack 编译,直接复制到 dist/

源文件原因
src/typescript.js需要在运行时动态加载用户的 TypeScript
src/loaders/uncacheable.js极简 Loader,无需打包
src/loaders/empty-loader.js包含运行时 require 逻辑
src/loaders/notfound-loader.js同上
src/@@notfound.js运行时占位文件

V8 缓存产物

每个编译目标同时生成:

  • .js — 主文件(实际是 V8 缓存加载器)
  • .js.cache — V8 编译后的字节码
  • .js.cache.js — 原始源码(供 V8 缓存过期时使用)

资源检查

// scripts/build.js:81-86
function checkUnknownAssets(buildName, assets) {
  assets = assets.filter(name =>
    !name.endsWith('.cache') && !name.endsWith('.cache.js') &&
    !name.endsWith('LICENSES.txt') && name !== 'processChild.js' && name !== 'mappings.wasm'
  );
  if (!assets.length) return;
  console.error(`New assets are being emitted by the ${buildName} build`);
}

构建过程会检查意外的资源输出。已知的合法资源被白名单过滤,新增的意外资源会打印警告(但不阻断构建)。

TypeScript 类型文件

// scripts/build.js:119-122
await copy(
  __dirname + "/../node_modules/typescript/lib/*.ts",
  __dirname + "/../dist/ncc/loaders/typescript/lib/"
);

TypeScript 的 .d.ts 库文件被复制到 dist/,供 ts-loader 在编译用户 TypeScript 代码时使用。

缓存策略

  • 默认缓存目录:<项目根>/.cache
  • --no-cache 参数禁用缓存
  • 缓存以 ncc 版本为 key,版本升级自动失效
  • prepublishOnly 中强制无缓存构建确保发布产物的确定性

产物目录结构

dist/
└── ncc/
    ├── cli.js              # CLI(V8 缓存加载器)
    ├── cli.js.cache        # CLI V8 字节码
    ├── cli.js.cache.js     # CLI 源码
    ├── index.js            # 核心引擎(V8 缓存加载器)
    ├── index.js.cache      # 核心引擎 V8 字节码
    ├── index.js.cache.js   # 核心引擎源码
    ├── typescript.js       # TS 版本解析(直接复制)
    ├── sourcemap-register.js
    ├── LICENSES.txt        # 第三方许可信息
    ├── @@notfound.js       # 未找到模块占位
    ├── loaders/
    │   ├── relocate-loader.js (+ .cache + .cache.js)
    │   ├── shebang-loader.js (+ .cache + .cache.js)
    │   ├── ts-loader.js (+ .cache + .cache.js)
    │   ├── stringify-loader.js (+ .cache + .cache.js)
    │   ├── uncacheable.js
    │   ├── empty-loader.js
    │   ├── notfound-loader.js
    │   └── typescript/lib/  # TypeScript .d.ts 文件
    └── buildin/