ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Rolldown 的 preserveEntrySignatures 深入解析:exports-only 模式下入口导出签名保留策略

Rolldown 的 preserveEntrySignatures 深入解析:exports-only 模式下入口导出签名保留策略 Rolldown 的 preserveEntrySignatures 深入解析exports-only 模式下入口导出签名保留策略【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown在 Rolldown使用 Rust 编写、API 兼容 Rollup 的 JavaScript/TypeScript 打包器中preserveEntrySignatures是一个决定入口 chunk 的导出面如何被保留的关键选项直接关系到库构建的导出稳定性与应用的打包体积。本文以仓库中的官方测试用例exports-only为骨架结合preserveEntrySignatures的选项文档与 Rust 源码实现完整剖析该选项的四种取值、exports-only模式的分支判定逻辑、facade chunk 机制、逐入口覆盖方式及边界行为帮助你在库与应用的构建中做出正确的取舍。一、认识 preserveEntrySignatures入口导出签名的意义当一个模块被声明为入口entry时它的导出签名即该模块对外导出的具名标识符集合决定了外部使用者import { xxx } from your-lib的调用方能拿到什么。打包器在做代码合并、公共代码抽取时可能希望把入口模块的实现搬进其他 chunk此时如果不保留签名就必须创建一层门面facadechunk 来继续对外提供原有导出。Rolldown 通过preserveEntrySignatures告诉打包器为了导出签名你愿意付出多少打包优化上的代价。它接受四种取值类型定义位于 packages/rolldown/src/options/input-options.tspreserveEntrySignatures?: false | strict | allow-extension | exports-only;在 Rust 侧该枚举定义在 crates/rolldown_common/src/inner_bundler_options/types/output_option/preserve_entry_signatures.rs并通过kebab-case序列化与 JS 侧的字符串一一对应JS 取值Rust 枚举变体Display 输出strictStrictstrictallow-extensionAllowExtensionallow-extensionexports-onlyExportsOnly#[default]exports-onlyfalseFalsefalse值得注意的是exports-only是默认值Rust 枚举上的#[default]标注因此理解它等价于理解 Rolldown 开箱即用的默认行为。二、exports-only 的核心语义按是否导出自动分流官方选项文档 packages/rolldown/src/options/docs/preserve-entry-signatures.md 对exports-only的定义只有一句话Followsstrictbehavior for entry modules that have exports, but allowsallow-extensionbehavior for entry modules without exports.即有导出的入口模块按strict处理无导出的入口模块按allow-extension处理。这是四种模式中最按需分配的一种也是绝大多数场景下的默认推荐。这一分流逻辑在 Rust 源码中有直接实现。入口 chunk 创建时crates/rolldown/src/stages/generate_stage/code_splitting.rs 会先判断当前入口是否为用户定义入口再对exports-only做精细判定let is_user_defined_entry self.link_output.user_defined_entry_modules.contains(module.idx); let preserve_entry_signature if is_user_defined_entry { match finalized_preserve_entry_signatures { PreserveEntrySignatures::AllowExtension | PreserveEntrySignatures::Strict | PreserveEntrySignatures::False Some(finalized_preserve_entry_signatures), PreserveEntrySignatures::ExportsOnly { let meta self.link_output.metas[module.idx]; if meta.sorted_and_non_ambiguous_resolved_exports.is_empty() { Some(PreserveEntrySignatures::AllowExtension) } else { Some(PreserveEntrySignatures::Strict) } } } } else { None };这里的关键判定条件是meta.sorted_and_non_ambiguous_resolved_exports.is_empty()入口模块解析出的导出集合为空 → 降级为allow-extension非空 → 升级为strict。而全局设置到具体入口的解析含this.emitFile的逐入口覆盖由normalize_preserve_entry_signature完成见 crates/rolldown/src/utils/chunk/mod.rs。三、测试用例逐文件剖析exports-only 的实证仓库将这一行为固化为集成测试 fixture位于 crates/rolldown/tests/rolldown/misc/preserve_entry_signature/exports-only/其readme.md附带了与 Rollup Playground 对照的共享链接用于验证 Rolldown 与 Rollup 在相同输入下的行为一致性。3.1 测试配置双入口 exports-only_config.json 声明了两个用户定义入口并显式开启exports-only{ config: { preserveEntrySignatures: exports-only, input: [ { name: main, import: ./main.js }, { name: main2, import: ./main2.js } ] } }3.2 输入两个入口模块的导出差异是实验的关键main.js无任何导出仅导入lib.js的值并触发动态导入dynamic.jsmain2.js导出了unused取自lib2.js的value同时动态导入dynamic2.jslib.jsexport const value liblib2.jsexport const value lib2dynamic.js / dynamic2.js两个动态入口分别依赖lib.js与lib2.js。这样构造的意图一目了然同一个全局设置下两个入口模块一个有导出、一个无导出正好验证exports-only的分流逻辑。3.3 输出artifacts.snap 快照证实分流该 fixture 的产物快照 artifacts.snap由 crates/rolldown_testing/src/integration_test.rs 生成展示了实际打包结果与上述源码判定逻辑完全吻合main.js无导出 → allow-extension 语义lib.js的实现被直接内联无需任何门面//#region main.js import(./dynamic.js); console.log(shared: , lib); //#endregiondynamic.js动态入口同样无需保留签名lib.js同样被内联为字面量//#region dynamic.js console.log(shared: , lib); //#endregionmain2.js有导出 → strict 语义导出签名被完整保留——unused依然原样导出实现通过从独立 chunklib2.js导入来满足import { t as value } from ./lib2.js; //#region main2.js import(./dynamic2.js); const unused value; //#endregion export { unused };lib2.js被 main2 与 dynamic2 共享抽为公共 chunk//#region lib2.js const value lib2; //#endregion export { value as t };dynamic2.jsimport { t as value } from ./lib2.js; //#region dynamic2.js console.log(shared: , value); //#endregion对比可见main因为无导出享受了allow-extension的内联优化红利main2因为导出了unused其导出面被严格保留依赖关系被整理为从公共 chunklib2.js导入。这一输出差异正是exports-only按需保留签名的活教材。四、另外三种模式与 exports-only 的对比选项文档 packages/rolldown/src/options/docs/preserve-entry-signatures.md 对另外三档的定义如下strict入口 chunk 必须与入口模块的导出完全一致。如果内部绑定需要额外暴露例如模块在 chunk 间共享Rolldown 会创建 facade chunk 来维持精确签名。官方建议用于库构建保证稳定、可预期的导出面。allow-extension入口 chunk 可以暴露入口模块的全部导出也允许在合并时附带其他模块的额外导出。优化空间更大但可能泄露内部实现细节。false最大化灵活性入口 chunk 可与其他 chunk 自由合并完全不受导出签名约束。体积优化空间最大但对外暴露的导出可能显著变化。官方建议用于应用构建。从 Rust 源码看这三种取值在入口 chunk 创建时直接透传见上文code_splitting.rs的match分支而exports-only则额外做了一次探测后二选一因此在行为上属于strict与allow-extension的按入口混合体。五、facade chunkstrict 签名的兜底机制文档 packages/rolldown/src/options/docs/preserve-entry-signatures.md 专门解释了 facade chunk当入口模块的实际实现被合并进其他 chunk 时Rolldown 会创建一个小的包装 chunk来对外维持原有导出签名。典型场景两个入口共享大量代码且设置strict。打包器可能将共享代码抽入公共 chunk为每个入口创建 facade chunk从公共 chunk 重新导出从而保证每个入口对外导出与源码完全一致。在exports-only下只有有导出的入口才可能触发 facade 逻辑对应 strict 语义无导出的入口则直接内联、无需门面——这也解释了为什么main.js在快照中连一行export都没有因为导出面为空时签名自然无保留价值。六、逐入口覆盖emitFile 与 preserveEntrySignaturepreserveEntrySignatures是全局设置。若希望对单个入口单独指定策略唯一途径是通过插件 APIthis.emitFile以type: chunk发射入口并传入preserveEntrySignature字段见 packages/rolldown/src/plugin/plugin-context.ts。Rust 侧在创建入口 chunk 时也会优先读取overrode_preserve_entry_signature_map覆盖 map见 crates/rolldown/src/module_loader/module_loader.rs 与 normalize_preserve_entry_signature。文档给出了库 应用混合构建的实战示例源自 packages/rolldown/src/options/docs/preserve-entry-signatures.md// rolldown.config.js export default { preserveEntrySignatures: exports-only, // Default for most entries plugins: [ { name: custom-entries, buildStart() { // Library entry that needs strict signature preservation this.emitFile({ type: chunk, id: src/library/index.js, fileName: library.js, preserveEntrySignature: strict, }); // Application entry that can be optimized this.emitFile({ type: chunk, id: src/app/main.js, fileName: app.js, preserveEntrySignature: false, }); }, }, ], };preserveEntrySignature支持与全局选项相同的四种取值false/strict/allow-extension/exports-only在文件发射层面完成对全局设置的覆盖。七、边界行为manual code splitting 的隐式覆盖还有一个容易踩坑的边界当用户使用手动代码分割manual code splitting且设置了includeDependenciesRecursively: false时依赖模块可能被留在入口 chunk 之外此时从入口 chunk 导出非入口模块是非法的。为避免该问题crates/rolldown/src/utils/prepare_build_context.rs 会在用户未显式设置preserveEntrySignatures时将其隐式改为allow-extension并发出告警let preserve_entry_signatures if let Some(manual_code_splitting) manual_code_splitting has_non_recursive_dependency_capture(manual_code_splitting) raw_options.preserve_entry_signatures.is_none() { warnings.push( BuildDiagnostic::invalid_option( InvalidOptionType::IncludeDependenciesRecursivelyWithImplicitPreserveEntrySignatures, ) .with_severity_warning(), ); PreserveEntrySignatures::AllowExtension } else { raw_options.preserve_entry_signatures.unwrap_or_default() };即一旦你手动设置了preserveEntrySignaturesRolldown 尊重你的选择只有未设置 非递归依赖捕获组合才隐式改写并提示。相关约束在手动代码分割文档 docs/in-depth/manual-code-splitting.md 中亦有说明。八、选型建议什么时候用哪一档综合选项文档 packages/rolldown/src/options/docs/preserve-entry-signatures.md 与源码实现可以给出如下决策指引场景推荐取值理由库构建需要稳定导出面strict导出签名精确保留必要时用 facade chunk 兜底大多数应用/混合场景exports-only默认有导出的入口保签名无导出的入口放开优化兼顾二者高级优化能接受暴露额外导出allow-extension入口可附带合并模块的导出优化空间更大极致体积导出面无关紧要falsechunk 自由合并优化空间最大库 应用混合构建全局exports-onlyemitFile逐入口覆盖用preserveEntrySignature为个别入口单独定制结语preserveEntrySignatures是连接导出契约与打包优化的调节阀而exports-only作为默认值用一行源码判定resolved_exports.is_empty()实现了有导出保签名、无导出放开优化的自适应策略。通过阅读 exports-only 测试 fixture、对照其 产物快照再到 code_splitting.rs 中的实现细节你可以完全掌握 Rolldown 在导出签名维度上的决策链路进而在库与应用的不同构建诉求间做出精准权衡。若需进一步探索仓库还提供了同一主题下的其他回归用例如 basic、issue-4873、issue_5026可作为延伸阅读。【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进