Rolldown 的 output.preserveModulesRoot 选项详解:精确控制 preserveModules 模式下的输出目录结构
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
导读
output.preserveModulesRoot是 Rolldown(一个使用 Rust 编写、兼容 Rollup API 的 JavaScript/TypeScript 打包器)在preserve modules 模式(即output.preserveModules: true)下用于控制输出目录结构的关键选项。它通过剥离输入模块路径中共享的目录前缀,让产物的目录结构不再受源代码目录深度影响,从而在输出目录迁移、monorepo 多包开发、第三方模块未标记external等场景下保持稳定的产物路径。读完本文,你将掌握该选项的完整配置方法、适用场景、底层实现原理及其与preserveModules、virtualDirname、input、external等选项的协同关系。
一、选项定位:它是preserveModules的目录修剪器
Rolldown 的preserveModules模式与 Rollup 保持行为一致:不再像默认模式那样“尽量少产出 chunk”,而是为每个模块生成独立的 chunk,并使用原始模块名作为文件名。与此同时,tree-shaking 依然生效——未被入口引用、且执行无副作用(side effects)的模块文件会被抑制不产出,非入口模块中未使用的导出也会被移除。
但仅仅保留模块名是不够的:如果两个入口模块分处不同的目录层级(例如src/module.js与src/another/module.js),它们产出的文件会带着各自的目录结构写入output.dir。此时,preserveModulesRoot的价值就体现出来了——它指定一个“输入模块的根目录路径”,在输出时把该前缀从产物路径中剥离,从而裁剪掉你不想保留的目录层级。
官方对该选项的使用场景说明非常明确:当输出目录结构可能发生变化时,这个选项尤其有用。典型情况包括:
- 第三方模块未被标记为
external:这些模块被打包进来后,其文件路径会被保留到产物中,导致output.dir下出现不受控的深层目录; - monorepo 多包相互依赖且未标记
external:多个 package 之间互相引用时,产物路径中可能包含各自包的目录前缀,剥离公共根目录可以让输出结构更干净、更可预期。
二、完整配置示例与输出效果
以下示例继承自官方文档,并补充了必要的上下文注释:
import { defineConfig } from 'rolldown'; export default defineConfig({ input: ['src/module.js', 'src/another/module.js'], output: { dir: 'dist', preserveModules: true, preserveModulesRoot: 'src', }, });设置preserveModulesRoot: 'src'后,输入模块会被输出到如下路径:
dist/module.js // 来自 src/module.js dist/another/module.js // 来自 src/another/module.js对比不设置该选项的行为:src/module.js与src/another/module.js的目录结构原本会被原样带入产物,得到dist/src/module.js与dist/src/another/module.js。preserveModulesRoot剥离了公共前缀src/,这正是它名字中 “Root” 的含义——它定义的是“从哪个根目录开始保留结构”。
在 TypeScript 类型定义中,该选项声明为preserveModulesRoot?: string;,位于 output-options.ts,其语义描述为:“在使用 preserve modules 模式时,需要从output.dir中剥离的输入模块目录路径”,文档说明正是通过{@include ./docs/output-preserve-modules-root.md}机制内嵌到该选项的 API 注释中(参见 output-options.ts)。
配套选项速查
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
preserveModules | boolean | false | 开启 preserve modules 模式,为所有模块按原始模块名产出独立 chunk(见 output-preserve-modules.md) |
preserveModulesRoot | string | 未设置 | 指定从输出路径中剥离的输入模块根目录 |
virtualDirname | string | '_virtual' | 插件产出的“虚拟”模块文件的目录名(见 output-options.ts) |
三、底层实现:Rolldown 如何在 Rust 中剥离路径前缀
preserveModulesRoot从 JavaScript 侧经 binding 层传递到 Rust 核心。参数绑定路径为:packages/rolldown/src/utils/bindingify-output-options.ts序列化选项 →crates/rolldown_binding/src/utils/normalize_binding_options.rs中的preserve_modules_root: output_options.preserve_modules_root,最终进入NormalizedBundlerOptions(定义于 normalized_bundler_options.rs)。
真正的路径计算发生在生成阶段的generate_chunk_name_and_preliminary_filenames函数中(crates/rolldown/src/stages/generate_stage/mod.rs),核心逻辑可归纳为三步:
- 判断模式:仅当
self.options.preserve_modules为true时,才对入口模块走 preserve modules 的命名分支(见 mod.rs)。 - 剥离根前缀:读取
preserve_modules_root后,通过strip_path_prefix_to_slash函数把该前缀从模块的绝对路径中裁剪掉,得到相对路径(见 mod.rs)。 - 失败回退:如果前缀剥离失败(例如模块路径并不以
preserveModulesRoot开头),实现会回退为relative_path_to_slash(abs, input_base.as_str())——即退而求其次,基于入口路径的公共基目录(input_base)来计算相对路径,保证产物文件名仍然稳定可用(见 mod.rs)。
此外,源码还处理了路径风格归一化:当模块 id 是“类绝对路径”形式(如/favicon)时,实现会将这种无卷标的根路径锚定到 cwd 所在卷根(Windows 的盘符或 UNC 共享),避免剥离前缀时误吞开头的斜杠,从而把盘符/前导斜杠泄漏进[name](见 mod.rs)。
虚拟文件目录的补充说明
在 preserve modules 模式下,如果插件为了达成某些效果而产出额外的“虚拟”文件,这些文件会被以实际文件形式输出,命名模式为${output.virtualDirname}/fileName.js(默认_virtual/)。preserveModulesRoot只负责裁剪真实模块路径的公共前缀,虚拟文件则由virtualDirname单独管理,两者互不干扰(详见 output-preserve-modules.md)。
四、跨平台与路径规范化:来自测试用例的验证
路径处理最容易在 Windows 与类 Unix 系统之间出现行为差异。仓库中有一个专门的集成测试验证了这一场景:preserve_modules_root_with_slash_normalized_ids(见 crates/rolldown/tests/rolldown/issues/9593/mod.rs)。
该测试构造了一个返回slash 规范化 id(把\统一替换为/,见测试中的resolve_id实现,mod.rs)的 resolve 插件,并以preserve_modules: Some(true)、preserve_modules_root: Some("src".into())构建打包配置。它验证了:即使插件提供的模块 id 经过了反斜杠到正斜杠的归一化,preserveModulesRoot的剥离逻辑依然能够正确工作。从源码结构看,这正是通过node_style_absolute与strip_path_prefix_to_slash的组合来兼容“模块 id 保持原生分隔符、而输出路径统一使用/”这两种情形(参见 mod.rs 及注释指向的 internal-docs 说明)。
这一测试同时印证了官方文档开篇的定位:该选项的设计目标就是让产物目录结构“稳定且可预测”,即使源代码目录树或模块解析方式发生变化。
五、使用建议与注意事项
结合官方文档与源码行为,给出以下实操建议:
- 与
preserveModules: true成对使用:preserveModulesRoot仅在 preserve modules 模式下生效(源码在 mod.rs 处先判断preserve_modules),单独设置不会产生任何效果。 - 不要盲目用它做全量格式转换:官方文档明确指出,如果目的是把整个文件结构转换成另一种格式并直接导入,不建议盲目开启 preserve modules——因为 tree-shaking 可能让某些预期中的导出缺失。此时更合适的做法是把所有文件显式加入
input选项对象,并可通过 glob 模式动态指定(见 output-preserve-modules.md)。 - 优先把相互依赖的包标记为
external:文档强调该选项主要面向“未标记 external”的场景。在 monorepo 中,正确标记external(见 input-options.ts 的ExternalOption类型)可以从根源上避免依赖包路径被写入产物,而preserveModulesRoot则是在无法标记时的兜底手段。 - 善用
input的对象形式与命名:当需要精确控制每个产物的输出名时,应优先使用对象形式input: { name: 'path' }配合entryFileNames等命名模板,而不是依赖路径推导。
六、小结
output.preserveModulesRoot是 Rolldown preserve modules 模式下控制产物目录结构的精准“修剪器”:它以一行配置剥离输入模块的公共目录前缀,让输出目录结构在输入目录变化时保持稳定,尤其适合第三方模块未标记external与 monorepo 多包开发的场景。其实现位于 Rust 生成阶段的 chunk 命名流程中,包含“优先剥离前缀、失败回退到入口基目录”的健壮逻辑,并有针对 slash 规范化模块 id 的跨平台测试用例作为保障。理解这一选项,有助于你在使用 Rolldown 时产出目录结构干净、可预期且与 Rollup 行为一致的打包产物。
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考