Webpack 配置与打包流程

配置生成

ncc 在 src/index.js 中动态构建完整的 Webpack 配置对象。配置的每个部分都由用户选项和文件特征驱动。

Webpack 配置结构

入口与输出

// src/index.js:261-305
{
  entry,                           // 用户指定的输入文件
  output: {
    path: "/",                     // 输出到内存 FS 的根目录
    filename: "index.js",          // 或 .cjs/.mjs
    libraryTarget: esm ? 'module' : 'commonjs2',
    module: esm,                   // ESM 模式下启用 module 输出
    strictModuleExceptionHandling: true
  }
}

输出文件名的 .cjs 扩展名有特殊处理:由于 Webpack 只为 .js 文件生成 source map,.cjs 输入会临时加 .js 后缀编译,后处理时再处理。

缓存配置

// src/index.js:263-270
cache: cache === false ? undefined : {
  type: "filesystem",
  cacheDirectory: typeof cache === 'string' ? cache : nccCacheDir,
  name: `ncc_${hashOf(entry)}`,   // 基于入口文件路径的 SHA-256 前 10 位
  version: nccVersion              // ncc 版本变更时缓存失效
}

缓存目录默认为 $XDG_CACHE_HOME/ncc/<project-sha1>~/.cache/ncc/<project-sha1>

模块解析

// src/index.js:306-327
resolve: {
  extensions: [".js", ".mjs", ".ts", ".tsx", ".json", ".node"],
  byDependency: {
    esm: { conditionNames: ["import", "node", "production"] },
    commonjs: { conditionNames: ["require", "node", "production"] },
    // ...
  },
  mainFields: ['main'],            // 不使用 Webpack 默认的 ['module', 'main']
  plugins: resolvePlugins
}

关键设计决策:mainFields 只包含 main,不包含 module。这是因为 ncc 的目标是 Node.js,而 Node.js 本身不会使用 module 字段。

优化配置

// src/index.js:278-288
optimization: {
  nodeEnv: false,              // 不替换 process.env.NODE_ENV
  minimize: false,             // 不使用 Webpack 内建压缩
  moduleIds: 'deterministic',  // 确定性模块 ID
  chunkIds: 'deterministic',
  mangleExports: true,         // 压缩导出名
  concatenateModules: true,    // 模块连接(scope hoisting)
  innerGraph: true,            // 内部图分析
  sideEffects: true            // 副作用分析
}

minimize: false 是因为 ncc 使用自己的 Terser 后处理,不走 Webpack 的压缩插件。

实验性特性

// src/index.js:275-278
experiments: {
  topLevelAwait: true,         // 支持顶层 await
  outputModule: esm            // ESM 模式下启用 module 输出
}

Loader 管道

Webpack 的 Loader 按配置顺序从下到上执行。ncc 的 Loader 管道按实际执行顺序:

.js/.mjs/.tsx?/.node 文件

  1. relocate-loader:分析和重定位资源引用(文件路径、__dirname__filename 等)
  2. empty-loader:过滤不可静态分析的包

.tsx? 文件(额外)

  1. ts-loader:TypeScript → JavaScript 转换
  2. uncacheable:标记为不可缓存(确保 TypeScript 配置变更时重新编译)

对所有非 .node/.json 文件

  1. shebang-loader:移除 shebang 行

@@notfound.js 匹配文件

  1. notfound-loader:将占位模块转为目标模块名称的运行时 require

插件系统

ncc 注册了几个自定义 Webpack 插件:

核心插件(src/index.js:213-246

{
  apply(compiler) {
    // 1. 初始化资源缓存
    compiler.hooks.compilation.tap("relocate-loader", compilation => {
      compilationStack.push(compilation);
      relocateLoader.initAssetCache(compilation);
    });

    // 2. watch 模式下触发 rebuild 回调
    compiler.hooks.watchRun.tap("ncc", () => {
      if (rebuildHandler) rebuildHandler();
    });

    // 3. 拦截 require 赋值,防止 CommonJsPlugin 处理
    compiler.hooks.normalModuleFactory.tap("ncc", NormalModuleFactory => {
      // 拦截 `require = ...` 赋值模式
    });
  }
}

第三个钩子的作用是:某些代码中 require 被重新赋值(如 require = createRequire(import.meta.url)),Webpack 的 CommonJsPlugin 会尝试处理这种模式,但 ncc 需要保留原始行为。

条件插件

  • LicenseWebpackPlugin:当 --license 选项指定时,收集所有依赖的许可信息
  • DefinePlugin:CJS 模式下将 import.meta.url 替换为等效的 require("url").pathToFileURL(__filename).href

Externals 处理

// src/index.js:329-333
externals({ context, request, dependencyType }, callback) {
  const external = externalMap.get(request);
  if (external) return callback(null, `${dependencyType === 'esm' && esm ? 'module' : 'node-commonjs'} ${external}`);
  return callback();
}

externals 支持三种形式:

  • 字符串数组["fs", "path"] → 保留为 require("fs")
  • 对象映射{ "original": "replacement" } → 别名
  • 正则表达式{ "/caniuse-lite(/.*)/" : "caniuse-lite$1" } → 模式匹配,支持捕获组替换

externalMap 是一个自定义的类 Map 对象,支持正则匹配和缓存(src/index.js:164-201)。

Watch 模式

watch: true 时,compiler.watch() 替代 compiler.run()。返回一个包含 handlerrebuildclose 方法的对象。

watch 模式的文件监视可以通过传入自定义 WatchFileSystem 类来覆盖(src/index.js:415-419)。