从 CLI 到 Rsbuild 的生命周期
一句话概括
Core 的运行链路是:CLI 或 API 收到用户意图,加载 Rslib 配置和 env,创建 RslibInstance,把 lib 数组转成 Rsbuild environments,再调用 Rsbuild 的 build、inspect 或 dev server 能力。
这条链路看起来直线,但维护时要注意两个事实:
- CLI 和编程式 API 共享
createRslib主路径,不能让 CLI 形成一条只在命令行生效的旁路。 - Build、inspect、mf-dev 都会调用配置编排,但它们选择 environment 和设置 mode 的方式不同。
入口:runCLI
src/cli/index.ts 做的是启动前准备,不负责构建细节:
initNodeEnv根据命令设置默认NODE_ENV。setupLogLevel在任何日志打印前处理--log-level。showGreeting打印版本。setupCommands注册和执行命令。
这里的维护重点是顺序。日志级别必须在 greeting 前设置,否则用户传 --log-level silent 也可能看到输出。NODE_ENV 必须在命令执行前设置,因为后续 Rsbuild 和 Rspack 配置可能读取它。
命令注册:setupCommands
src/cli/commands.ts 使用 cac 注册三个主命令:
公共参数由 applyCommonOptions 注册,构建参数只挂在 build command 上。维护 CLI 参数时应明确它是通用参数还是 build 专用参数:
- 通用参数应进入
CommonOptions,例如root、config、envMode、lib、logLevel。 - 构建参数应进入
BuildOptions,例如format、entry、bundle、dts、externals、distPath。
如果一个参数会影响配置加载本身,例如 --root、--config、--env-dir,必须在 init 之前生效。如果一个参数只影响 lib item,则应通过 applyCliOptions 覆盖配置。
CLI 初始化:init
src/cli/init.ts 是 CLI 和 core API 的桥。它做四件事:
- 从
process.cwd()和--root得到项目 root。 - 调用
baseLoadConfig加载配置。 - 在没有配置文件时填充默认
lib: [{}]。 - 调用
createRslib,并传入 env 加载选项。
CLI 参数覆盖集中在 applyCliOptions。这个集中化很重要:如果每个命令自己改配置,build、inspect、mf-dev 很容易出现行为不一致。当前 inspect 和 mf-dev 只用通用参数,build 用完整 BuildOptions。
配置加载:loadConfig
src/loadConfig.ts 使用 Rsbuild 的 loadConfig 作为底层加载器。Rslib 自己负责:
- 解析显式 config path。
- 在默认文件名列表中查找 config。
- 暴露
defineConfig类型辅助。 - 将返回内容标记为
RslibConfig。
默认文件名顺序偏向常用和性能:
rslib.config.mjsrslib.config.tsrslib.config.jsrslib.config.cjsrslib.config.mtsrslib.config.cts
显式传 config 时,如果文件不存在,Rslib 会抛出带 [rslib:loadConfig] 前缀的错误。这类错误属于用户配置错误,应保持文案直接,不要让底层 loader 错误冒泡成难懂栈。
创建实例:createRslib
src/createRslib.ts 是 build、inspect、mf-dev 的共同核心。创建实例时会:
- 根据
options.loadEnv调用 Rsbuild 的loadEnv。 - 如果配置是函数,则调用它得到最终配置。
- 将 public env vars 合并到
config.source.define。 - 解析
config.root为绝对路径。 - 把 env 文件路径放到
_privateMeta.envFilePaths,供 restart 监听。 - 暴露
onAfterCreateRsbuild回调机制。
这里有一个重要设计:createRslib 不立即创建 Rsbuild 实例,而是在 build、inspect、mf-dev 被调用时再创建。原因是不同操作需要不同 mode、不同 environment 裁剪和不同 Rsbuild API。
创建 Rsbuild 实例
createRslib 内部的 createRsbuildInstance 会把 Rslib 配置中的共享字段传给 Rsbuild:
cwdcallerName: "rslib"moderootpluginsdevserverlogLevelenvironments
它还会:
- 在 env 加载开启时注册 cleanup。
- 在 debug 模式下挂载 inspect 插件。
- 执行
onAfterCreateRsbuild回调。
维护这里时要特别小心 plugins、dev、server 的来源:这些是顶层共享 Rsbuild 配置,不属于单个 lib item。单个 lib 的插件和 output 则在 environment config 中处理。
Build 生命周期
Build 的具体流程:
Watch 模式下,build 还会向 config.plugins 追加一个 rslib:on-after-build 插件,用于首次编译后输出 watch 状态。CLI 层会调用 watchFilesForRestart 监听配置文件和 env 文件变化,变更时重新执行 cliBuild。
这里有一个维护陷阱:watch restart 是 CLI 层循环,不是 Rsbuild 自己的普通 watch。配置文件或 env 文件变化后需要关闭旧 build 实例,否则可能出现多个 watcher 和 server 同时存在。
Inspect 生命周期
Inspect 不是构建,但它必须走同一套配置编排。它会:
- 设置 mode,默认 production。
- 调用
composeRsbuildEnvironments。 - 创建 Rsbuild 实例。
- 调用
rsbuildInstance.inspectConfig。 - 在
extraConfigs中附带 Rslib 原始配置。 - 返回格式化后的
rslibConfig字符串和 Rsbuild inspect 结果。
维护 inspect 时要坚持“所见即所得”:inspect 应尽量反映真实 build 会使用的 config,而不是另一份手写配置。
MF Dev 生命周期
startMFDevServer 只选择 format: "mf" 的 environment。原因是普通 library environment 不应该启动应用 dev server。
选择逻辑是:
- 没传
lib时,选择所有 MF environment。 - 传了
lib时,只选择 id 在列表中的 MF environment。 - 如果没有选中任何 MF environment,抛出明确错误。
MF dev server 创建后,会把 server close 注册到 onBeforeRestart,这样配置变化重启时能先关闭旧 server。
Restart 系统
src/restart.ts 做的是配置级重启,不是普通源码热更新。它监听:
- Rslib config file。
- env files。
getWatchFilesForRestart 从 rslib.getRslibConfig()._privateMeta 读取这些路径。watchFilesForRestart 使用 chokidar 动态导入,并通过 debounce 合并快速连续变更。
重启前会执行所有 onBeforeRestart 注册的 cleaner,并清空控制台重新打印启动信息。这里的目标是让 watch 模式在配置变化后像一次干净启动,而不是继续沿用旧 Rsbuild 实例。
错误处理模型
CLI 的错误处理有三个层次:
最后都会 process.exit(1)。这对 CLI 是合理的,但编程式 API 不应随意退出进程。维护时要确认错误是在 CLI 层退出,还是在底层配置函数中直接 process.exit。后者需要更谨慎,只适合确实不可恢复且已有先例的场景。
维护建议
- 新增 CLI 参数时,先决定它属于 CommonOptions 还是 BuildOptions。
- 修改 config 加载时,同时检查 CLI、API、watch restart 和 env file paths。
- 修改 createRslib 时,同时检查 build、inspect、mf-dev 三条路径。
- 修改 watch 行为时,确认旧实例会关闭,避免资源泄漏。
- 修改错误文案时,跑对应 CLI 测试,因为测试可能断言 stdout 或 stderr。