架构概览

系统定位

ncc 本质上是一个 Webpack 编排器(orchestrator),为 Node.js 模块打包提供零配置体验。它不是自己实现一个 bundler,而是在 Webpack 之上构建一个定制化的配置层和后处理管道。

核心架构图

flowchart TD
    subgraph 入口
        CLI["CLI (src/cli.js)"]
        API["API (src/index.js)"]
    end

    subgraph 核心引擎
        Config["Webpack 配置生成"]
        Resolve["模块解析策略"]
        External["Externals 处理"]
        Plugins["Webpack 插件"]
    end

    subgraph Loader管道
        Relocate["relocate-loader<br/>(资源重定位)"]
        TSLoader["ts-loader<br/>(TypeScript 编译)"]
        Empty["empty-loader<br/>(包过滤)"]
        Shebang["shebang-loader<br/>(shebang 移除)"]
        NotFound["notfound-loader<br/>(未找到模块处理)"]
    end

    subgraph 后处理
        Minify["Terser 压缩"]
        SourceMap["Source Map 处理"]
        V8Cache["V8 编译缓存"]
        AssetBuild["嵌套资源构建"]
        Rename["__webpack_require__ 重命名"]
    end

    subgraph 输出
        Code["code (字符串)"]
        Map["map (Source Map)"]
        Assets["assets (附属文件)"]
        Symlinks["symlinks (符号链接)"]
    end

    CLI --> API
    API --> Config
    Config --> Resolve
    Config --> External
    Config --> Plugins
    Config --> Relocate
    Config --> TSLoader
    Config --> Empty
    Config --> Shebang
    Config --> NotFound

    Plugins --> |"Webpack 编译"| Minify
    Minify --> SourceMap
    SourceMap --> V8Cache
    V8Cache --> AssetBuild
    AssetBuild --> Rename
    Rename --> Code
    Rename --> Map
    Rename --> Assets
    Rename --> Symlinks

数据流

编译流程

  1. 入口解析:CLI 或 API 接收输入文件路径,解析为绝对路径
  2. 配置生成:根据选项生成完整的 Webpack 配置对象
  3. Webpack 编译:调用 webpack() 并输出到内存文件系统(memory-fs
  4. 后处理:对编译结果进行 Terser 压缩、Source Map 处理、V8 缓存生成等
  5. 输出:返回 { code, map, assets, symlinks, stats } 对象

模块解析策略

ncc 使用定制化的模块解析策略:

  • 对 CJS 和 ESM 依赖分别配置 conditionNames
  • 使用 TsconfigPathsPlugin 支持 TypeScript paths 别名
  • 自定义 resolve 插件将未找到的模块转为运行时 require(而非构建时报错)
  • TypeScript .js 导入自动解析到 .ts/.tsx 文件

ESM 与 CJS 检测

ncc 根据以下规则自动判断输出格式:

  • .mjs 输入 → ESM 输出
  • .cjs 输入 → CJS 输出
  • .js 输入 + "type": "module" 包边界 → ESM 输出
  • 其他 → CJS 输出

关键设计决策

1. 内存文件系统

使用 memory-fs 作为 Webpack 的输出文件系统,避免中间磁盘 I/O。所有编译产物先写入内存,后处理完成后再由 CLI 层写入磁盘。

2. 未找到模块的宽容处理

当模块解析失败时,ncc 不会在构建时报错,而是将其转为运行时 require() 调用。这通过 src/@@notfound.js + notfound-loader.js + 自定义 resolve 插件实现。这使得动态依赖不会阻断构建。

3. 自定义 Terser 集成

ncc 不使用 Webpack 内建的压缩,而是在后处理阶段手动调用 Terser。原因是某些包(如 auth0)在 Webpack 的 Terser 集成中会返回 undefined

4. __webpack_require__ 重命名

为支持嵌套 ncc 构建(一个 ncc 产物作为另一个的输入),ncc 将 __webpack_require__ 重命名为 __nccwpck_require__,支持最多 9 层嵌套。

5. V8 编译缓存

可选的 V8 缓存模式会预编译代码并生成 .cache 文件,后续启动时直接加载编译后的字节码,显著提升冷启动速度。

6. 自举编译

ncc 使用自身来编译自身。scripts/build.js 调用 ncc API 编译 src/ 中的每个入口点,产物写入 dist/。这既是一个验证手段(ncc 能处理自己的代码),也是项目分发策略(只发布编译后的单文件)。

与 Webpack 的关系

ncc 对 Webpack 的使用方式与典型的 Web 应用构建有本质区别:

维度典型 Web 构建ncc
目标浏览器Node.js 14+
输出多文件(chunk splitting)单文件
文件系统磁盘内存
代码分割开启关闭
Tree-shaking开启开启(sideEffects: true
压缩Webpack 内建外部 Terser
Source Map开发时使用可选,通过 source-map-support 运行时注册
node 选项通常 polyfillfalse(保持原生)