配置顺序和失败形态

为什么顺序重要

Rslib 的配置编排不是把对象合并起来这么简单。很多配置项必须按固定顺序生效,否则最终产物会变错。

最典型的是 externals、entry、output filename、CSS、asset、dts 的顺序。这些系统互相依赖:

  • Bundleless external 需要知道 JS 输出扩展名。
  • CSS handler 需要知道 CSS Modules auto 规则和 JS 扩展名。
  • dts 插件需要知道 dts 扩展名。
  • asset preserve 需要知道 bundle 和 format。
  • 用户 externals 必须早于 autoExternal。
  • bundleless external 必须晚于 package external。

顺序错了,构建可能仍成功,但产物不可用。

当前 merge 顺序

composeLibRsbuildConfig 最终大致按以下顺序 merge:

  1. bundle 检查。
  2. format config。
  3. moduleIds。
  4. shims。
  5. syntax。
  6. externalHelpers。
  7. output filename。
  8. target。
  9. externals warn。
  10. user externals。
  11. auto external。
  12. target externals。
  13. bundleless external。
  14. entry。
  15. CSS。
  16. asset。
  17. entry chunk。
  18. minify。
  19. dts。
  20. banner/footer。
  21. decorators。
  22. print file size。
  23. exe。

这不是绝对不能调整,但任何调整都要说明行为影响。

失败形态一:用户 externals 被 autoExternal 覆盖

用户可能写:

output: {
  externals: {
    react: 'react-custom',
  },
}

如果 autoExternal 不排除用户 external key,可能同时生成两套 external 规则。结果取决于顺序,产物可能引用 react,也可能引用 react-custom,行为不稳定。

当前 composeAutoExternalConfig 会在用户 externals 是 object 时排除这些 key。

失败形态二:bundleless 把 npm 包改成相对路径

错误产物:

import React from '../node_modules/react/index.js';

这通常意味着 bundleless external 在 package external 之前执行,或 outBase 判断不严。

正确产物应保持:

import React from 'react';

或者按用户 externals 生成目标格式的 external 引用。

失败形态三:entry 被用户配置覆盖

Bundleless entry 是 glob 扫描后动态生成的 Rspack entry。如果最终 merge 时又把用户原始 source.entry 合进去,可能覆盖掉派生 entry。

所以 composeCreateRsbuildConfig 会在派生后重置:

userConfig.source.entry = {};

这看起来奇怪,但必要。它表示“这个字段已经被 Rslib 消费并转成底层配置,不应该再次作为用户原始字段参与 merge”。

失败形态四:externals 被普通 merge 打乱

Rspack externals 是数组,顺序有语义。普通深 merge 不知道哪些 external 应该排前面。Rslib 手动组合 externals,然后删除用户 config 中的 output.externals,避免最终 merge 再插入一次。

如果不这样做,bundleless external 可能不再是最后兜底。

失败形态五:dts 扩展名和 JS 扩展名不一致

例如 JS 输出:

index.mjs

但 dts 输出:

index.d.ts

在某些 package exports 和 NodeNext 解析场景下,这可能不是用户想要的。dts.autoExtension 打开时,Rslib 会把 dtsExtension 传给 plugin-dts,让它输出 .d.mts.d.cts

这里依赖 composeOutputFilenameConfig 先计算 extension,再 composeDtsConfig 接收它。

失败形态六:CSS Modules 指向错误扩展名

CSS Modules 在 bundleless 中需要输出 JS mapping。如果 JS extension 是 .mjs,CSS Modules import 应指向 .mjs。如果 CSS handler 不知道最终 JS extension,就可能输出:

import styles from './Button.module.css';

而实际 mapping 文件是:

Button.module.mjs

因此 cssExternalHandler 需要 jsExtension

失败形态七:exe 拿到多入口或 split chunk

Exe 生成需要单入口 JS main。如果 splitChunks 或 runtimeChunk 仍开启,SEA main 可能不是完整单文件入口,或者需要异步 chunk。composeExeConfig 会关闭:

  • runtimeChunk。
  • splitChunks。
  • asyncChunks。

它必须在已经知道 format、bundle、target、entry 后运行。

审查顺序改动的方法

看到 composeLibRsbuildConfig merge 顺序变化时,按下面清单审查:

  1. 是否影响 externals 顺序。
  2. 是否影响 bundleless external 最后执行。
  3. 是否影响 entry 派生和用户 entry 重置。
  4. 是否影响 output extension 传给 CSS、dts、redirect。
  5. 是否影响 target external 和 Node builtins。
  6. 是否影响 exe 对 splitChunks 的覆盖。
  7. 是否影响用户 tools.rspack 最终覆盖能力。

如果 PR 只说“重构顺序更清晰”,但没有解释行为等价性,应该要求补充测试或说明。

最小验证策略

顺序类改动建议用 inspect 和产物一起验证:

  1. rslib inspect 看最终 Rspack config。
  2. 查看 dist 文件名。
  3. 查看 JS 产物内部 import。
  4. 查看 CSS import 和 asset url。
  5. 查看 dts import。
  6. 对多 format 同时构建做验证。

只看测试是否通过不够,尤其是快照覆盖不完整时。