输出处理流程

概述

Webpack 编译完成后,ncc 在 finalizeHandler 函数(src/index.js:463-635)中对编译产物进行一系列后处理。这些步骤按严格顺序执行,每一步都依赖前一步的结果。

处理管道

flowchart TD
    A["从 memory-fs 读取编译产物"] --> B["收集资源文件和符号链接"]
    B --> C["Terser 压缩(可选)"]
    C --> D["Source Map 资源注册"]
    D --> E["V8 编译缓存生成(可选)"]
    E --> F["Source Map 运行时注册"]
    F --> G["ESM package.json 生成"]
    G --> H["Shebang 恢复"]
    H --> I["__webpack_require__ 重命名"]
    I --> J["嵌套资源构建(可选)"]
    J --> K["返回最终结果"]

各步骤详解

1. 从内存文件系统读取产物

// src/index.js:464-479
const assets = Object.create(null);
getFlatFiles(mfs.data, assets, relocateLoader.getAssetMeta, fullTsconfig);

getFlatFiles 递归遍历 memory-fs 的数据结构,将所有文件展平为 { path: { source, permissions } } 映射。对 .d.ts 文件,会根据 tsconfig 的 outDir 调整相对路径。

2. 符号链接过滤

// src/index.js:467-472
const symlinks = Object.create(null);
for (const [key, value] of Object.entries(relocateLoader.getSymlinks())) {
  const resolved = join(dirname(key), value);
  if (resolved in assets) symlinks[key] = value;
}

只保留指向已存在资源的符号链接,丢弃悬空引用。

3. Terser 压缩

// src/index.js:481-513
if (minify) {
  result = await terser.minify(code, {
    module: esm,
    compress: false,              // 不做代码压缩,只做 mangle
    mangle: {
      keep_classnames: true,      // 保留类名(运行时反射需要)
      keep_fnames: true           // 保留函数名(错误堆栈可读性)
    },
    sourceMap: map ? { content: map, filename, url: `${filename}.map` } : false
  });
}

关键设计:compress: false 意味着 Terser 只做标识符混淆(mangle),不做死代码消除或表达式简化。这减少了代码体积同时避免了语义变更的风险。同时保留类名和函数名以确保运行时兼容性。

如果 Terser 失败(如返回 undefined),ncc 会静默回退到未压缩的代码,仅打印警告。

4. V8 编译缓存

// src/index.js:519-533
if (v8cache) {
  const { Script } = require('vm');
  assets[`${filename}.cache`] = { source: new Script(code).createCachedData(), ... };
  assets[`${filename}.cache${ext}`] = { source: code, ... };
  code = `const { readFileSync, writeFileSync } = require('fs'), ...`;
}

V8 缓存模式下:

  • 生成 .cache 文件包含 V8 编译后的字节码
  • 生成 .cache.js 文件包含原始源码
  • 主文件变为一个加载器,读取缓存并通过 vm.Script 执行
  • 首次运行后如果缓存过期,会在进程退出时更新缓存

注意:ESM 模式下不支持 V8 缓存(src/index.js:64-65),因为 vm.Script 不支持 ES modules。

5. Source Map 运行时注册

// src/index.js:535-539
if (map && sourceMapRegister) {
  code = (esm ? `import './sourcemap-register${registerExt}';` : `require('./sourcemap-register${registerExt}');`) + code;
  assets[`sourcemap-register${registerExt}`] = { source: fs.readFileSync(...), ... };
}

当启用 source map 时,自动在输出代码前注入 source-map-support 的注册代码。ESM 模式下使用 .cjs 扩展名确保 source-map-support 以 CJS 方式加载。

6. ESM package.json 生成

// src/index.js:541-549
if (esm && !filename.endsWith('.mjs')) {
  assets[pjsonPath] = { source: JSON.stringify({ type: 'module' }, null, 2) + '\n', ... };
}

ESM 输出且文件名不是 .mjs 时,自动生成一个 package.json 文件声明 "type": "module",确保 Node.js 将输出识别为 ESM。

7. Shebang 恢复

// src/index.js:551-555
if (shebangMatch) {
  code = shebangMatch[0] + code;
  if (map) map.mappings = ";" + map.mappings;
}

如果原始输入文件有 shebang 行(如 #!/usr/bin/env node),在最终输出前将其恢复。同时在 source map 中添加一行偏移。

8. __webpack_require__ 重命名

// src/index.js:560-576
if (code.indexOf('"__webpack_require__"') === -1) {
  // 处理多层嵌套
  code = code.replace(/__webpack_require__/g, '__nccwpck_require__');
}

这是支持 ncc 产物嵌套使用的关键机制。当一个 ncc 产物被另一个 ncc 构建包含时,内层的 __webpack_require__ 不会与外层冲突。支持最多 9 层嵌套(__nccwpck_require2___nccwpck_require9_)。

跳过条件:如果代码中包含字符串字面量 "__webpack_require__",则跳过重命名(这表示代码可能在检测 webpack 环境)。

9. 嵌套资源构建

// src/index.js:579-630
if (assetBuilds) {
  for (const asset of subbuildAssets) {
    const { code, assets: subbuildAssets } = await ncc(path, {
      // ...递归调用 ncc
      assetBuilds: false,        // 不递归嵌套
      transpileOnly: true,       // 子构建跳过类型检查
    });
  }
}

--asset-builds 启用时,ncc 会检查所有 .js/.cjs/.mjs/.ts 资产文件,如果它们有对应的源路径元数据,就用 ncc 重新编译它们。这用于处理 worker 文件等需要独立打包的嵌套代码。

限制:

  • 不递归进行嵌套资源构建(assetBuilds: false
  • 子构建使用 transpileOnly: true 跳过类型检查以节省 CPU

最终返回值

// src/index.js:634
return { code, map, assets, symlinks, stats };
字段类型说明
codestring编译后的代码(包含所有后处理)
mapstring | undefinedJSON 格式的 Source Map
assetsobject附属文件映射 { [path]: { source, permissions } }
symlinksobject符号链接映射 { [path]: target }
statsobjectWebpack stats 对象