配置编排常规说明

为什么配置编排是 core 的核心

Rslib 的大部分价值都体现在配置编排里。用户写的是面向库构建的 RslibConfig,底层执行的是 Rsbuild 和 Rspack 需要的 environment config。中间这层翻译既要隐藏库构建的复杂度,也要保留足够的用户覆盖能力。

src/config.ts 不是简单把字段改名。它要同时处理以下问题:

  • 多个 lib item 如何变成多个 environment。
  • 不同 format 如何设置 Rspack output、parser、library、runtime。
  • bundle 和 bundleless 如何使用不同的 entry 和 external 策略。
  • package.json 依赖如何转成自动 external。
  • Node builtins 如何在 node target 下被 external。
  • CSS、asset、dts、exe 如何接入构建生命周期。
  • 用户的 Rsbuild 配置如何保留优先级。

理解配置编排时,最有效的方式是从“最终要生成什么”倒推。最终生成的是一个 Record<string, EnvironmentConfig>,key 是 environment id,value 是 Rsbuild environment 配置。

主流程

这个流程对应三个公开或半公开入口:

  • composeCreateRsbuildConfig:把 Rslib config 变成带 format 和 id 信息的 Rsbuild config 列表。
  • composeRsbuildEnvironments:给每个 config 分配 environment id,返回 environment map。
  • pruneEnvironments:根据 --lib 或 API 参数裁剪 environment。

合并顺序

最终 config 的合并顺序是:

mergeRsbuildConfig(
  constantRsbuildConfig,
  libRsbuildConfig,
  userConfigWithoutLibOnlyFields,
);

这个顺序不是随意的:

  1. constantRsbuildConfig 提供基础默认值。
  2. libRsbuildConfig 根据 Rslib 字段覆盖或补充默认值。
  3. userConfigWithoutLibOnlyFields 让用户仍能覆盖大部分 Rsbuild 和 Rspack 配置。

维护时最常见的问题是误把某个字段留在 userConfig 里。比如 source.entry 先被 composeEntryConfig 用来计算 entry、outBase、runtime chunk、dts entry,然后会被重置为空对象,避免后续 merge 时覆盖已经派生好的 entry。output.externals 也会被删除,因为 externals 的顺序需要 Rslib 手动控制,不能让 merge 机制打乱。

内置常量配置

createConstantRsbuildConfig 表达的是“Rslib 认为库构建应当默认这样”的底线:

  • 关闭 html plugin。
  • chunkSplit.strategy = "custom"
  • 开启 build cache。
  • 设置 Rspack optimization.nodeEnv = false,避免库构建中硬编码 process.env.NODE_ENV
  • 设置 TypeScript extension alias,支持用户在源码里写 .js 引用但解析到 .ts
  • 默认 target 为 node。
  • JS 和 CSS distPath 默认都输出到根目录。

这些默认值通常不应该被轻易删除。要改它们,必须确认是用户配置覆盖不足,还是默认值本身不再适合库构建。

composeLibRsbuildConfig 的结构

composeLibRsbuildConfig 可以看成一个配置装配流水线。它先读取输入状态,再调用一系列 compose 函数,最后按顺序 merge。

输入状态包括:

  • pkgJson:用于 autoExternal、extension、externalHelpers、MF uniqueName。
  • compilerOptions:用于 decorators 默认值。
  • formatbundleautoExtensionautoExternalredirectshims
  • source.entrysource.tsconfigPath
  • output.targetoutput.externalsoutput.filenameoutput.minify
  • experiments.exe

输出状态包括:

  • 格式化后的 Rspack output。
  • 入口配置和 bundleless outBase。
  • externals 列表。
  • CSS 和 asset 插件。
  • dts 插件。
  • exe 插件。
  • 语法 target 和 browserslist。
  • 输出文件名和 chunk 文件名。

Compose 函数组别

组别函数说明
格式组composeFormatConfigcomposeTargetConfigcomposeModuleIdsConfig决定产物模块语义和运行环境
入口组composeEntryConfiggetRuntimeChunkConfig决定 entry、outBase、runtime chunk
外部化组composeExternalsWarnConfigcomposeExternalsConfigcomposeAutoExternalConfigcomposeBundlelessExternalConfig决定依赖保留和路径重写
输出组composeOutputFilenameConfigcomposeMinifyConfigcomposePrintFileSizeConfig决定文件名、压缩和输出日志
兼容组composeShimsConfigcomposeDecoratorsConfigcomposeExternalHelpersConfig决定语言和运行时兼容
产物组composeCssConfigcomposeAssetConfigcomposeDtsConfigcomposeExeConfig接入 CSS、资源、声明文件和可执行文件

维护时不要只看某个 compose 函数本身,还要看它在 merge 列表里的位置。

Externals 的顺序语义

Externals 是配置编排中最敏感的顺序之一。当前顺序是:

  1. warn config。
  2. user externals。
  3. auto external。
  4. target externals。
  5. bundleless externals。

这样安排的原因是:

  • warn config 必须先看到可能被 external 的 commonjs request,才能给 ESM external type 提示。
  • 用户 externals 的优先级应该高于 Rslib 自动推导。
  • auto external 应该在 bundleless 路径重写前生效,否则依赖可能被错误解析成文件。
  • Node builtins external 是 target 规则。
  • bundleless external 是最后兜底,只处理源文件内部引用的输出路径重写。

如果把 bundleless external 放早了,第三方依赖或用户 external 可能被 resolver 解析到本地文件,导致库发布后 import 指向错误。这个问题通常不会在简单 fixture 中暴露,需要通过 package.json 依赖和 node_modules 结构测试覆盖。

Entry 和 outBase 的联动

Entry 是 bundle 和 bundleless 分岔的起点。

Bundle 模式下,entry 必须是文件。因为 bundler 会从入口打包依赖图,glob 或目录入口没有明确的单 bundle 语义。Rslib 会提前校验,避免让 Rspack 报出更难理解的错误。

Bundleless 模式下,entry 通常是 glob。Rslib 会扫描所有匹配文件,过滤声明文件,然后根据 outBase 生成 entry name。默认 outBase 是所有非声明输入文件的最长公共路径。这个设计让输出保留源码目录结构。

outBase 后续还会被多个系统使用:

  • bundleless external 判断请求是否来自源码范围。
  • CSS 插件判断全局 CSS entry 和输出路径。
  • EntryChunkPlugin 在 watch 中登记 context dependency。
  • dts bundleless 输出需要和 JS 输出保持路径关系。

因此 outBase 不是一个局部变量,而是 bundleless 的全局坐标系。

文件扩展名的联动

composeOutputFilenameConfig 输出两个重要结果:jsExtensiondtsExtension。它们不只用于文件名:

  • JS entry 输出用 jsExtension
  • chunk filename 推导也用它。
  • bundleless JS redirect 会把 .ts.tsx、无扩展名 import 改成目标 JS 扩展名。
  • CSS Modules import 会指向对应 JS 模块扩展名。
  • asset redirect 在某些配置下也会把资源请求改成 JS 输出扩展名。
  • dts 插件用 dtsExtension 决定 .d.ts.d.mts.d.cts

改扩展名相关逻辑时,必须用实际产物验证 import 是否存在。只看输出文件名是不够的,真正重要的是产物内部 import graph 是否能被 Node、浏览器或 bundler 解析。

User config 的保留和剥离

Rslib 允许用户传 Rsbuild 配置,但 LibConfig 中有一些字段是 Rslib 专属的,不能直接交给 Rsbuild。例如:

  • format
  • bundle
  • autoExtension
  • autoExternal
  • redirect
  • syntax
  • externalHelpers
  • dts
  • shims
  • umdName
  • outBase
  • experiments

composeCreateRsbuildConfig 在最终 merge 前会用 omit 去掉这些字段。这样既允许用户在 lib item 中写 Rsbuild 字段,又避免 Rslib 专属字段泄漏到底层。

何时新增 compose 函数

新增逻辑时,不要默认塞进已有大函数。可以用以下规则判断:

  • 如果逻辑只服务某个 format,优先放到 composeFormatConfig 对应分支。
  • 如果逻辑产生独立 Rsbuild config,且需要在 merge 顺序中明确位置,可以新增 compose 函数。
  • 如果逻辑依赖多个前序计算结果,例如 outBasejsExtension,应在这些结果计算后调用。
  • 如果逻辑只是小型工具,放到 utils 或本文件局部函数。

新增 compose 函数时,文档和测试应说明它放在 merge 列表中的原因。

审查配置编排 PR 的方法

审查这类 PR 时,建议按下面顺序看:

  1. 看类型变化,确认用户 API 是否变化。
  2. 看默认值变化,确认是否 breaking。
  3. 看 compose 顺序变化,确认有没有影响 externals、entry、dts、CSS。
  4. 看多 format 行为,确认是否只测了 esm。
  5. 看 bundleless 行为,确认是否只测了 bundle。
  6. 看 inspect 输出,确认用户能否诊断最终配置。
  7. 看测试 fixture,确认真实产物是否被断言,而不是只断言 config。

配置编排 bug 的特点是“看起来只是配置”,但最终会表现为运行时 import 失败、声明文件路径错误、CSS 丢失、Node builtins 被打包、或者 UMD/IIFE runtime 不符合预期。测试应尽量断言产物内容和运行结果。