实验性 exe 接入点

Core 层关注什么

experiments.exe 的实现分布在 src/exe,但从 core 角度看,它是一个在 JS 构建成功后运行的附加产物生成流程。Core 层主要负责:

  • 校验用户配置是否满足 exe 前提。
  • 解析 target 配置。
  • 在 Rsbuild environment 中挂载 ExePlugin
  • 调整 Rspack 配置,保证输出适合作为 Node SEA main。

真正的 binary 下载、checksum、SEA 配置、--build-sea 调用由 exe 子模块内部完成。

为什么 exe 要求严格

生成单文件可执行程序比普通 JS 产物限制更多。Core 明确校验:

  • 只支持 format: "esm"format: "cjs"
  • 必须 bundle: true
  • 必须 output.target = "node"
  • 每个 lib 只能有一个 entry。
  • 当前 Node runtime 必须支持 SEA。
  • ESM exe 不能使用 snapshot。

这些限制都来自 Node SEA 和 Rspack bundle 输出的现实约束。如果放宽限制,用户可能得到一个构建成功但无法运行的二进制文件。

接入位置

composeLibRsbuildConfig 在 entry、format、target 等配置计算后调用 composeExeConfig。这是必要的,因为 exe 校验需要知道:

  • 当前 format。
  • 当前 bundle 值。
  • 当前 source entry。
  • 当前 target。
  • 项目 root。

composeExeConfig 返回一个 environment config,包含:

  • plugins: [ExePlugin(...)]
  • optimization.runtimeChunk = false
  • optimization.splitChunks = false
  • output.asyncChunks = false

这些 Rspack 配置确保最终 JS 产物适合作为单入口 SEA main。

Target 解析

用户可以通过 experiments.exe.targets 指定多个目标。Core 会把它们规范化为 NormalizedExeTarget

  • 没写 platform 时使用当前平台。
  • 没写 arch 时使用当前架构。
  • 没写 nodeVersion 时使用当前 Node 版本。
  • 字符串 target 视为自定义 Node binary 路径。
  • 多 target 时添加 suffix,避免输出覆盖。
  • 跨平台 target 自动关闭 snapshot 和 code cache。

维护这里时要区分“目标 binary”和“构建 binary”。目标 binary 是最终注入 SEA blob 的模板;构建 binary 必须能在当前机器执行 --build-sea

ExePlugin 的生命周期

ExePlugin 运行在 onAfterEnvironmentCompile。它会先检查 Rspack stats:

  • 如果没有 stats,直接返回。
  • 如果 compilation 有错误,直接返回。
  • 如果 entrypoint 数量不是 1,抛错。

然后它会:

  1. 找到构建后的 main file。
  2. 解析所有目标 binary。
  3. 为每个 target 计算 executable 输出路径。
  4. 对自定义 binary 做版本一致性最终校验。
  5. 调用 buildExecutable
  6. 打印生成文件路径、大小和耗时。

这个生命周期意味着 exe 不会在 JS 构建失败时继续执行,避免拿不完整产物生成二进制。

输出路径

exe 输出路径由 resolveExecutableOutputPath 计算。它需要综合:

  • environment output path。
  • JS main file。
  • 用户 fileName
  • 用户 outputPath
  • target suffix。
  • Windows .exe 后缀。

多 target 输出路径尤其需要注意。没有 suffix 时,不同平台产物可能互相覆盖;有 suffix 时,用户仍可能通过 fileName 或 outputPath 产生冲突,需要测试覆盖。

Binary 下载和缓存

当 target 不是当前 runtime 时,Rslib 会下载 Node release。下载流程包括:

  • 获取 SHASUMS256.txt
  • 找到 archive checksum。
  • 下载 archive。
  • 校验 sha256。
  • 解压。
  • chmod。
  • 缓存到 exe cache dir。

这里 checksum 是安全边界,不能为了简化逻辑移除。并发下载同一 archive 时,inFlightBinaryDownloads 会复用 promise,避免重复下载和解压。

SEA 构建

buildExecutable 会创建临时目录,写入 sea-config.json,再运行:

node --build-sea sea-config.json

SEA config 包括:

  • main
  • mainFormat
  • output
  • executable
  • disableExperimentalSEAWarning
  • useSnapshot
  • useCodeCache
  • execArgv
  • execArgvExtension
  • assets

构建完成后,macOS 会尝试签名。临时目录在 finally 中删除。

为什么这是 experimental

这个能力依赖 Node SEA,而 SEA 本身还在演进。维护时要保持保守:

  • 不要默认扩大平台支持。
  • 不要吞掉 Node binary 版本不一致。
  • 不要在 cross-platform 时启用 snapshot 或 code cache。
  • 不要让 exe 逻辑影响普通 JS 构建。

它应作为 lib item 的附加能力,而不是改变整个 Rslib 构建模型。

测试建议

exe 改动测试成本较高,但至少要覆盖:

  • 不支持 format 时的错误。
  • bundle: false 错误。
  • 非 node target 错误。
  • 多 entry 错误。
  • 默认当前 runtime target。
  • 自定义 fileName 和 outputPath。
  • 多 target suffix。
  • custom binary 版本读取失败。

仓库把 exe integration 拆成 integration-exe project,就是为了让慢速 SEA case 可以单独调度。