核心编译引擎

文件位置

src/index.js — ncc 的核心模块,负责 Webpack 配置生成、编译执行和输出后处理。

模块接口

module.exports = ncc;
function ncc(entry, options) → Promise<Result> | WatchHandle

参数

参数类型说明
entrystring输入文件路径
optionsobject配置选项(详见 配置选项参考

返回值

  • 非 watch 模式Promise<{ code, map, assets, symlinks, stats }>
  • Watch 模式{ handler(cb), rebuild(cb), close() }

核心流程

1. 选项解析与默认值

// src/index.js:35-61
function ncc(entry, {
  cache,
  esm = entry.endsWith('.mjs') || !entry.endsWith('.cjs') && hasTypeModule(entry),
  externals = [],
  filename = 'index' + (/* 基于 esm 和扩展名计算 */),
  minify = false,
  sourceMap = false,
  sourceMapRegister = true,
  watch = false,
  v8cache = false,
  // ...
} = {}) {

esm 的自动检测逻辑:

  1. .mjs 扩展名 → ESM
  2. .cjs 且位于 "type": "module" 包边界内 → ESM
  3. 其他 → CJS

2. TSConfig 解析

// src/index.js:117-136
const configFileAbsolutePath = walkParentDirs({
  base: process.cwd(),
  start: dirname(entry),
  filename: 'tsconfig.json',
});
fullTsconfig = loadTsconfig(configFileAbsolutePath);

从入口文件所在目录向上搜索 tsconfig.json,加载后用于:

  • 配置 TsconfigPathsPlugin(路径别名)
  • 配置 ts-loader 的 compilerOptions
  • 处理 .d.ts 文件的输出路径

整个 TSConfig 加载包裹在 try-catch 中:如果没有 tsconfig,纯 JavaScript 项目仍可正常工作。

3. 自定义 Resolve 插件

// src/index.js:138-162
resolvePlugins.push({
  apply(resolver) {
    resolver.resolve = function(context, path, request, resolveContext, callback) {
      resolve.call(self, context, path, request, resolveContext, function(err, innerPath, result) {
        if (result) return callback(null, innerPath, result);
        // TypeScript .js → .ts/.tsx 回退
        if (request.endsWith('.js') && context.issuer?.endsWith('.ts')) {
          return resolve.call(self, context, path, request.slice(0, -3), resolveContext, ...);
        }
        // 未找到 → 转为运行时 require
        callback(null, __dirname + '/@@notfound.js?' + request, request);
      });
    };
  }
});

这个插件实现了两个关键行为:

  1. TypeScript .js 导入回退:TS 文件中 import './foo.js' 会尝试解析 ./foo.ts./foo.tsx
  2. 宽容的模块解析:任何找不到的模块都不会导致构建失败,而是转为运行时 require

4. ExternalMap

// src/index.js:164-201
const externalMap = (() => {
  const regexps = [];
  const aliasMap = new Map();
  const regexCache = new Map();
  // ...
  return { get, set };
})();

externalMap 是一个带正则匹配的映射结构:

  • set(key, value):key 可以是字符串或 RegExp
  • get(key):先精确匹配,再遍历正则表达式,匹配结果缓存在 regexCache

正则表达式支持捕获组替换:/caniuse-lite(.*)/ → caniuse-lite$1

5. Webpack 编译

// src/index.js:261-393
const compiler = webpack({ /* 配置 */ });
compiler.outputFileSystem = mfs;  // 使用内存文件系统

编译完成后进入 finalizeHandler(详见 输出处理流程)。

6. compilationStack

// src/index.js:210
const compilationStack = [];

compilationStack 跟踪当前活跃的 Webpack compilation 对象。由于 assetBuilds 可能触发嵌套编译,需要栈来跟踪层级关系。每次编译开始时 push,完成时 pop。

辅助函数

getFlatFiles

// src/index.js:639-660
function getFlatFiles(mfsData, output, getAssetMeta, tsconfig, curBase = "")

递归遍历 memory-fs 的内部数据结构(嵌套对象),将文件展平为路径→内容的映射。对 .d.ts 文件做特殊路径处理。

walkParentDirs

// src/index.js:663-680
function walkParentDirs({ base, start, filename })

start 目录向上搜索指定文件名,直到 base 目录。用于查找 tsconfig.json

hashOf

// src/index.js:22-27
const hashOf = name => crypto.createHash("sha256").update(name).digest("hex").slice(0, 10);

生成缓存名称的短哈希。

依赖关系

依赖用途
webpack核心打包引擎
memory-fs内存文件系统
terser代码压缩
resolve同步模块解析
graceful-fs文件系统增强
tsconfig-pathsTSConfig paths 解析
tsconfig-paths-webpack-pluginWebpack 集成
license-webpack-plugin许可信息收集
@vercel/webpack-asset-relocator-loader资源重定位

线程安全与并发

ncc 是单线程运行的,不支持同时进行多个编译。compilationStack 虽然看起来支持嵌套,但实际上嵌套调用(assetBuilds)是通过 await 串行执行的。