输出与模块格式

为什么 format 是核心维度

Rslib 支持的 format 不只是文件后缀差异。它决定了产物运行方式、外部依赖类型、chunk 加载方式、library 声明方式、runtime chunk 策略和默认 target。

当前 format 包括:

format主要用途默认倾向
esm现代 ESM 库产物适合 Node ESM 和现代 bundler
cjsCommonJS 库产物适合 Node CJS 生态
umd浏览器和多环境全局包必须 bundle
iife浏览器直接执行模块必须 bundle
mfModule Federation 远程模块更接近应用构建

维护 format 逻辑时,不能把某个 format 当成只是 output.library.type 的变化。每个 format 都有一套运行时假设。

ESM

ESM 是 Rslib 的默认 format。核心输出特征:

  • output.module = true
  • Rspack library.type = "modern-module"
  • chunk loading 使用 import
  • worker chunk loading 使用 import
  • filenameHash 默认 false。
  • splitChunks 只允许 async chunks,避免 sync chunk 影响 entry inline。
  • avoidEntryIife = true
  • concatenateModules = false

ESM 的难点主要在 external 和 Node 兼容。Rslib 使用 externalsType = "module-import",但如果源码里从 CommonJS issuer external 了某个 request,可能会让最终 ESM import type 不符合用户预期。因此有 composeExternalsWarnConfig 提示用户显式设置 external type。

ESM target 为 node 时,还会涉及 Node builtins、__dirname__filenamerequire shim。默认不启用这些 CJS 全局 shim,但用户可通过 shims.esm 打开。

CJS

CJS 产物核心特征:

  • output.module = false
  • Rspack library.type = "commonjs-static"
  • chunk loading 使用 require
  • worker chunk loading 使用 async-node
  • filenameHash 默认 false。
  • splitChunks 只允许 async chunks。

CJS 的兼容重点是 import.meta.*。源码可能使用 import.meta.urlimport.meta.dirnameimport.meta.filename,但 CJS 运行时没有这些语义。Rslib 默认为 CJS 启用这些 shim:

  • import.meta.url
  • import.meta.dirname
  • import.meta.filename

这些 shim 由 pluginCjsShimsEntryChunkPlugin 共同参与。修改 CJS 输出时,必须检查 shebang、"use strict" 和 shim 插入顺序,避免破坏可执行脚本或 strict directive。

UMD

UMD 适合需要兼容 AMD、CommonJS 和浏览器 global 的场景。Rslib 对 UMD 有强约束:

  • 必须 bundle: true
  • 关闭 async chunks。
  • 关闭 splitChunks。
  • library.type = "umd"
  • 用户可通过 umdName 指定 library name。

UMD 不支持 bundleless 是合理的,因为 UMD 目标是一个可直接分发的最终 bundle。保留源码结构和多个相对 import 不符合 UMD 的运行模型。

维护 UMD 时,重点关注:

  • externalsType 为 umd
  • nodeEnv 使用 process.env.NODE_ENV
  • 不要引入需要 ESM runtime 的配置。
  • 多入口或 chunk 拆分是否会破坏单 bundle 假设。

IIFE

IIFE 适合浏览器中直接执行。Rslib 对 IIFE 也要求 bundle: true,并设置:

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

IIFE 的 external type 使用 global。原因是如果对包名如 @pkg 使用 var,可能生成非法变量名;如果使用 umd,又可能受环境中 define 等变量影响。global 是更稳妥的折中。

Module Federation

MF format 和其他 format 的区别最大。它更像应用构建而不是库构建:

  • 必须 bundle: true
  • 默认 target 是 web。
  • dev.writeToDisk = true
  • 需要用户使用 Module Federation 插件。
  • Rslib 会设置 Rspack output uniqueName = pkgJson.name
  • nodeEnv 在 development 和 production 中使用对应字符串。

core 会通过 checkMFPlugin 检查 MF 插件是否存在,避免用户只写 format: "mf" 却没有配置 federation 插件。

MF dev server 只选择 MF environment。普通 ESM/CJS library environment 不会进入 mf-dev

Target 与 format 的关系

Rslib 的 target 默认规则:

  • format: "mf" 默认 web
  • 其他 format 默认 node

这只是默认值,用户仍可配置 output.target。target 会影响:

  • Rspack target。
  • Node builtins external。
  • moduleIds 策略。
  • syntax 到 browserslist 的默认推导。
  • exe 是否允许生成。

Node target 下,Rslib 会把 Node builtins external 掉。这对库构建很重要,因为把 node:fspath 等内置模块打进库产物通常不是用户想要的结果。

文件扩展名

format 与扩展名的关系由 getDefaultExtension 处理。维护时应理解三个输入:

  • format
  • package.json type
  • autoExtension

常见目标是:

  • ESM 在合适场景输出 .mjs.js
  • CJS 在合适场景输出 .cjs.js
  • dts 对应 .d.mts.d.cts.d.ts

文件扩展名不是纯展示问题。Node ESM loader 不做扩展名搜索;bundleless 输出中如果 import 没有扩展名,运行时可能无法解析。因此 bundleless redirect 会主动补扩展名。

Runtime chunk

Rslib 对 runtime chunk 的处理也和 format、bundle、entry 数量有关:

  • bundleless 模式使用固定 rslib-runtime
  • bundle 单入口通常不需要单独 runtime chunk。
  • bundle 多入口需要 runtime chunk,避免重复 runtime 或入口无法正确共享。
  • 多 compiler 时,runtime chunk 名会带 compiler index,避免多个 environment 输出冲突。

Runtime chunk 修改需要特别小心,因为它不一定在简单单入口测试中暴露问题。多入口、多 format 和 bundleless 测试更能覆盖这类风险。

Minify 策略

Rslib 默认的 minify 不是追求最小体积,而是面向库构建的保守优化:

  • 开启 JS minify 管线。
  • 不 mangle。
  • 默认不做 aggressive minify。
  • 主要做 unused 和 dead code。
  • 保留部分注释和 annotations。

MF 是例外。MF 资源通过网络加载,项目侧不一定再压缩 remoteEntry 和相关资源,因此 MF 会更积极地 minify。

外部依赖类型

不同 format 的 external type 不同:

formatexternalsType
esmmodule-import
cjscommonjs-import
umdumd
mfglobal
iifeglobal

这意味着同一个 output.externals 在不同 format 下会生成不同运行时引用。维护 externals 时必须同时考虑 format 和 target,而不是只看依赖名。

新增 format 的成本

新增 format 不是加一个字符串。至少要检查:

  1. Format 类型。
  2. composeFormatConfig
  3. composeExternalsConfig 的 externalsType 和 globalObject。
  4. composeTargetConfig 默认 target 是否特殊。
  5. getDefaultExtension
  6. autoExternal 默认值。
  7. dts extension。
  8. runtime chunk。
  9. CLI 参数说明。
  10. integration tests 和文档。

如果新增 format 不能明确回答这些问题,就不应进入 core。

排查输出格式问题

输出格式问题通常表现为:

  • Node import 失败。
  • 浏览器全局变量不存在。
  • chunk 加载路径错误。
  • external 依赖引用方式不对。
  • .d.ts 扩展名与 JS 不匹配。
  • bundleless import 指向源码扩展名。

排查建议:

  1. 先运行 rslib inspect 看最终 Rspack output。
  2. 查看 dist 内实际文件名和 chunk 名。
  3. 搜索产物内部 import 或 require。
  4. 对照 format 的 externalsType。
  5. 检查 package.json type
  6. 检查 autoExtensionredirect
  7. 用最小 integration case 断言真实运行结果。