ESM、CJS、UMD、IIFE、MF 产物矩阵

为什么要逐个看 format

format 不是输出文件后缀的别名。它决定一组互相关联的产物语义:

  • Rspack parser 如何理解模块。
  • Rspack output 是否启用 module。
  • library type 是什么。
  • chunk loading 用 importrequire 还是关闭。
  • externalsType 如何生成外部依赖引用。
  • splitChunks 和 runtimeChunk 是否允许。
  • target 默认是 node 还是 web。
  • 是否能 bundleless。
  • 是否适合 dts auto extension。

如果只把 format 理解成 esm -> .mjscjs -> .cjs,维护时很容易改坏运行时语义。

总览表

format定位是否允许 bundleless默认 targetlibrary typeexternalsTypechunk loading关键风险
esm现代库产物允许nodemodern-modulemodule-importimportexternal type、Node ESM 扩展名、CJS 互操作
cjsCommonJS 库产物允许nodecommonjs-staticcommonjs-importrequireimport.meta.* shim、静态导出语义
umd浏览器/多环境 bundle不允许node 默认但常配 webumdumdasync chunks 关闭必须单 bundle、global name、external 映射
iife浏览器直接执行 bundle不允许node 默认但常配 webmodule with iifeglobalasync chunks 关闭globalObject、不可 bundleless
mfModule Federation remote不允许web由 MF 插件主导global由 MF runtime 主导更像应用,不是普通 library

ESM

ESM 是默认 format。Rslib 为 ESM 设置:

  • output.module = true
  • library.type = "modern-module"
  • chunkLoading = "import"
  • workerChunkLoading = "import"
  • chunkFormat = false
  • avoidEntryIife = true
  • splitChunks.chunks = "async"
  • runtimeChunk 根据 bundle 和 entry 数量决定
  • externalsType = "module-import"

为什么 ESM external 特别敏感

ESM 产物的 external 不是简单字符串替换。module-import 会生成 ESM import 形态。若源码中从 CommonJS issuer 发起 request,可能出现语义不符合预期的 external。

因此 Rslib 有 composeExternalsWarnConfig,专门在 ESM 下探测 externalized commonjs request 并警告用户显式指定 external type。

ESM 和 autoExtension

ESM 在 Node 中对扩展名很敏感。Node ESM loader 不做扩展名搜索:

import './Button';

不会自动找 Button.js。所以 bundleless ESM 通常需要把输出 import 改成:

import './Button.js';

如果 package type 或用户 filename 让 ESM 输出变成 .mjs,内部 import 也要跟着变。

ESM 的维护风险

  • 不要把 CJS external type 用到 ESM。
  • 不要生成无扩展名相对 import。
  • 不要让 runtime chunk 名在多 environment 下冲突。
  • 不要让 CJS shim 默认进入 ESM,除非用户显式开启。

CJS

CJS 产物设置:

  • output.module = false
  • library.type = "commonjs-static"
  • chunkLoading = "require"
  • workerChunkLoading = "async-node"
  • chunkFormat = "commonjs"
  • splitChunks.chunks = "async"
  • externalsType = "commonjs-import"

为什么需要 commonjs-static

库产物希望保留比较静态的导出结构,便于下游工具和 Node 解析。commonjs-static 比动态挂载 exports 更适合库输出。

CJS 和 import.meta.*

很多现代源码会写:

new URL('./asset.png', import.meta.url);

或使用 import.meta.dirnameimport.meta.filename。CJS 没有这些语义。Rslib 默认在 CJS 下启用对应 shims,避免源码从 ESM 转 CJS 后直接坏掉。

CJS 的维护风险

  • shim 注入必须尊重 shebang 和 "use strict"
  • require chunk loading 不应混入 ESM import chunk。
  • .cjs / .js 扩展名要和 package type 对齐。
  • external helper、Node builtins、target node 的组合要保持可运行。

UMD

UMD 是最终 bundle 格式,不支持 bundleless。Rslib 会在 bundle: false 时直接报错。

UMD 设置:

  • library.type = "umd"
  • 可选 library.name = umdName
  • asyncChunks = false
  • splitChunks = false
  • nodeEnv = process.env.NODE_ENV
  • externalsType = "umd"

为什么 UMD 必须 bundle

UMD 的目标是一个可直接通过 script、AMD 或 CJS 使用的 bundle。它不适合保留多文件源码结构。如果 UMD bundleless,产物会同时要求 UMD wrapper 和相对模块 import,这两种模型冲突。

UMD 的维护风险

  • external 映射必须适合 UMD 消费方式。
  • 多入口可能产生多个 UMD wrapper,要确认用户预期。
  • 不要打开 splitChunks,否则不是单一分发 bundle。
  • umdName 对浏览器 global 消费非常关键。

IIFE

IIFE 是浏览器直接执行的 bundle,也不支持 bundleless。

设置包括:

  • output.iife = true
  • library.type = "module"
  • asyncChunks = false
  • splitChunks = false
  • chunkFormat = false
  • chunkLoading = "import"
  • globalObject = "globalThis"
  • externalsType = "global"

为什么 IIFE external 用 global

如果 external type 用 var,像 @scope/pkg 这种包名会生成非法变量引用。如果用 umd,环境中 define 等变量可能影响判断。Rslib 对 IIFE 使用 global,更适合浏览器和多环境下的 global lookup。

IIFE 的维护风险

  • 不能产生 async chunks。
  • external 名称要能在 globalThis 上找到。
  • 不要混入 Node target 假设。
  • minify 和 annotations 需要保证库入口仍可用。

MF

MF format 是最特殊的。它更像应用 remote,不是普通 library format。

设置包括:

  • 必须 bundle: true
  • 默认 target 为 web
  • dev.writeToDisk = true
  • output uniqueName = pkgJson.name
  • nodeEnv 在 development/production 中设置具体字符串
  • 必须有 Module Federation 插件
  • 不使用普通 RslibPlugin 的所有 library 假设

为什么 MF 不是普通 library

Module Federation 需要 runtime 来加载 remote、shared modules 和 chunks。它的输出语义不是“把库发布到 npm 后被 import”,而是“运行时通过 federation 协议加载 remote”。

因此很多 library 默认值不能直接套到 MF:

  • nodeEnv = false 不适合 shared module 构建。
  • target 通常是 web。
  • dev server 有意义。
  • remoteEntry 需要写盘。
  • minify 默认更积极,因为网络资源可能不再由项目侧压缩。

MF 的维护风险

  • 不要把 MF 当 ESM/CJS 的一个变体。
  • 不要让 Node target 默认渗入 MF。
  • 不要移除 MF plugin 检查。
  • 不要把 ordinary library 的 runtime chunk 策略直接套给 MF。

format 与测试

修改 format 行为时,至少检查:

  • tests/integration/format
  • tests/integration/umd
  • tests/integration/iife
  • tests/integration/externals
  • tests/integration/auto-external
  • tests/e2e/module-federation
  • tests/e2e/react-component 中 UMD 消费

如果改动影响 bundleless,还要加 bundle-falseredirect

修改 format 的审查问题

  1. 是否改变外部依赖生成方式?
  2. 是否改变 chunk loading?
  3. 是否改变 output.module?
  4. 是否改变 target 默认值?
  5. 是否改变 autoExternal 默认值?
  6. 是否影响 dts extension?
  7. 是否影响 CSS/asset redirect?
  8. 是否影响 --lib 多 environment 构建?

format 是 Rslib 的主轴之一。任何 format 行为变化都应被视为高风险改动。