输出处理流程
概述
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 };