Rolldown generateBundle 钩子完全指南:在产物落盘前对输出文件做增删改
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
generateBundle是 Rolldown 输出阶段(output generation phase)的核心钩子:当所有 chunk 与 asset 已经渲染完毕、但尚未写入磁盘时,它允许插件按文件名删除输出文件以阻止其被发出,或通过this.emitFile新增额外文件,同时还能就地修改chunk 的代码、文件名与 sourcemap。本文以 plugin-hooks-generatebundle.md 为骨架,结合 Rolldown 仓库的 Rust 核心实现与 JS 绑定层源码,讲清该钩子的触发时机、三种合法操作、一个必须避开的陷阱,以及插件间的执行顺序。读完你就能编写出行为正确、与 Rollup 生态兼容的输出阶段插件。
一、何时触发:从生成到写入的最后一个关口
generateBundle属于输出阶段钩子,在 constants/plugin.ts 中与outputOptions、renderStart、renderChunk、augmentChunkHash、writeBundle等并列注册为ENUMERATED_OUTPUT_PLUGIN_HOOK_NAMES。
从 Rust 核心的调用链可以精确还原它的触发时机。在 crates/rolldown/src/bundle/bundle.rs 的bundle_up中:
GenerateStage::generate完成 chunk 实例化与代码生成;render_chunk_to_assets(见 crates/rolldown/src/stages/generate_stage/render_chunk_to_assets.rs)依次执行render_chunks(对应renderChunk钩子)、augment_chunk_hash(对应augmentChunkHash钩子)、压缩与 banner/footer 处理后,通过finalize_assets产出最终资产列表,并调用set_emitted_chunk_filenames把最终文件名同步给file_emitter;- 然后
bundle_up调用self.plugin_driver.generate_bundle(&mut output.assets, is_write, ...)(bundle.rs),此时所有文件内容已经定型、尚未写盘。
也就是说,generateBundle是你在文件真正落地前的最后一个改动机遇,之后只剩writeBundle(写入完成后通知)与closeBundle(构建收尾)。
二、钩子签名与参数
在 JS 插件中,generateBundle的签名与 Rollup 对齐:
generateBundle(outputOptions: NormalizedOutputOptions, bundle: OutputBundle, isWrite: boolean): void | Promise<void>三个参数的含义如下:
| 参数 | 类型 | 说明 |
|---|---|---|
outputOptions | NormalizedOutputOptions | 归一化后的输出选项,如format、entryFileNames、sourcemap等 |
bundle | OutputBundle | 以文件名为键的输出文件集合,值为OutputChunk或OutputAsset |
isWrite | boolean | 本次调用是bundle.generate()(false,仅生成内存产物)还是bundle.write()(true,会落盘) |
OutputBundle的类型定义见 types/output-bundle.ts:
export interface OutputBundle { [fileName: string]: OutputAsset | OutputChunk; }chunk 对象暴露的字段可在 types/output-chunk-impl.ts 中查到:fileName、name、code、exports、isEntry、isDynamicEntry、facadeModuleId、moduleIds、modules、imports、dynamicImports、map、sourcemapFileName、preliminaryFileName等。
三、操作一:删除文件,阻止其被发出
核心语义(来自关联文档原文):You can prevent files from being emitted by deleting them from the bundle object in this hook.(你可以通过在本钩子中从 bundle 对象上删除文件来阻止它们被输出。)
删除某个文件即把它从最终的输出清单中移除,Rolldown 不会再将其写出。仓库中的最小复现用例位于 tests/fixtures/misc/remove-chunk/_config.ts:
export default defineTest({ config: { plugins: [ { name: 'remove-chunk', generateBundle(outputOptions, bundle) { delete bundle['main.js']; }, }, ], }, });这个“删除”动作是被 JS 绑定层显式支持并会传回 Rust 侧的。在 utils/transform-to-rollup-output.ts 中,transformToOutputBundle为 bundle 对象套了一层Proxy,其deletePropertytrap 会把被删除的文件名记入changed.deleted;钩子返回后,collectChangedBundle(同文件 L197-L238)再把deleted集合回传 Rust,最终该文件不会出现在输出目录中。这正好印证了文档的表述:删除 = 阻止发出。
四、操作二:新增文件,必须走 this.emitFile
核心语义(来自关联文档原文):To emit additional files, use the
this.emitFilefunction.(要发出额外文件,请使用this.emitFile。)
this.emitFile支持三种类型,完整行为细节可进一步阅读 plugin-context-emitfile.md:
type: 'chunk':以给定模块id为入口发出新 chunk。不会导致模块图中出现重复模块;必要时会拆分既有 chunk 或创建带 reexport 的 facade chunk。带fileName的 chunk 总是独立成块,未指定时使用output.chunkFileNames模板。带 hash 的 chunk 文件名在generateBundle之前通过getFileName拿到的是占位符,Rolldown 会在generateBundle前用真实 hash 替换。type: 'prebuilt-chunk':直接发出内容固定(code)的 chunk。由于它不属于模块图,需要先在resolveId中把它标记为 external,再由buildStart中的emitFile发出,之后即可在源码中import { foo } from './my-prebuilt-chunk.js'正常引用(示例见 plugin-context-emitfile.md)。type: 'asset':发出任意内容的文件。带fileName的 asset 总是独立成文件;未指定时按output.assetFileNames模板取名,且同源内容可能被去重。
在 JS 绑定层,emitFile的实现在 plugin-context.ts:它对prebuilt-chunk校验code必须是非空字符串、fileName不能是绝对/相对路径片段,对chunk/asset也做了同样的路径片段校验(非法路径会抛出VALIDATION_ERROR),随后分别路由到 binding 的emitPrebuiltChunk、emitChunk、emitFile。
五、危险操作:禁止直接向 bundle 赋值新增文件
关联文档的
danger警告块原文:Do not directly add assets to the bundle. This will not work as expected as Rolldown will ignore those assets.(不要直接向 bundle 添加资源,这不会按预期工作,Rolldown 会忽略这些资源。)并强调“这在 Rollup 中同样不被推荐”,Always usethis.emitFile。
这条警告在源码中有明确的强制执行机制。transformToOutputBundle创建的 bundle Proxy 对settrap 的处理是(transform-to-rollup-output.ts):
set(_target, _p, _newValue, _receiver) { const message = 'This plugin assigns to bundle variable. This is discouraged by Rollup and is not supported by Rolldown. This will be ignored. https://rollupjs.org/plugin-development/#generatebundle'; context.warn({ message, code: 'UNSUPPORTED_BUNDLE_ASSIGNMENT' }); return true; }也就是说,在generateBundle里写bundle['foo.js'] = something会产生一条UNSUPPORTED_BUNDLE_ASSIGNMENT警告,并且该赋值被静默忽略——文件并不会因此出现在输出中。
其根本原因在 Rust 侧:bundle_up调用plugin_driver.generate_bundle之前,输出资产列表已经由GenerateStage完全定型(render_chunk_to_assets.rs);而在 plugin_driver/output_hooks.rs 的generate_bundle驱动循环中,每个插件执行完后都会调用ctx.file_emitter().add_additional_files(bundle, warnings)——只有通过emitFile登记进file_emitter的文件才会被追加进输出。随手往 bundle 上塞的对象没有经过file_emitter,自然会被丢弃。这也是文档要求“始终使用this.emitFile”的底层原因。
六、修改已有文件:变更如何传回 Rust 侧
除了删除与新增,你还可以直接修改 bundle 中既有 chunk/asset 的属性。官方测试 tests/fixtures/plugin/generate-bundle/_config.ts 演示了多种被支持的可变操作:
- 改代码:
chunk.code = 'console.error()'; - 加自定义属性:
(chunk as any).customProperty = 'customProperty'(Proxy 的settrap 会记录该文件已更新); - 改 sourcemap:
map.file、map.mappings、map.sources、map.sourcesContent、map.names、map.x_google_ignoreList、map.debugId均可更新,随后chunk.map = map; - 改文件名:
chunk.fileName = 'updated-main.js'; - 删除文件:
delete bundle['index.js']。
这些变更之所以能生效,是因为绑定层把每个 chunk/asset 也包成了带settrap 的 Proxy:一旦某属性被赋值,就把该文件名记入changed.updated(transform-to-rollup-output.ts)。钩子结束后collectChangedBundle会把被更新文件的code、filename、name、map、imports等字段打包回传 Rust,覆盖掉对应资产的内容,最终写出的是修改后的版本——测试afterTest中断言chunks[0].code为'console.error()'即验证了这一点。
注意:并非所有属性都能回传,
collectChangedBundle只挑选了code、filename、name、isEntry、exports、imports、dynamicImports、facadeModuleId、isDynamicEntry、moduleIds、map、sourcemapFilename、preliminaryFilename等核心字段,源码注释也注明“not all properties modifications are reflected to Rust side”(并非所有属性修改都会反映到 Rust 侧)。
七、执行顺序与 writeBundle 的区别
generateBundle按插件的注册顺序依次调用,同一钩子内所有插件可依赖“前一个插件的修改对后一个插件可见”。测试 generate-bundle/_config.ts 中声明了test-plugin→test-plugin-2→test-update-sourcemap→test-read-updated-sourcemap四个插件,afterTest断言调用顺序严格为['test-plugin', 'test-plugin-2'],且第四个插件能读到第三个插件更新的文件名updated-main.js与 sourcemap 字段,完整验证了链式可见性。
在 Rust 侧,output_hooks.rs 的驱动循环按order_by_generate_bundle_meta排序遍历插件,每个插件执行完毕立即把emitFile产生的新文件追加进输出,供后续插件读取。
与相邻钩子的分工建议:
| 钩子 | 时机 | 典型用途 |
|---|---|---|
renderChunk | 单个 chunk 渲染后 | 按 chunk 逐个改代码(可配合magicString/RolldownMagicString) |
augmentChunkHash | chunk hash 计算前 | 依据内容向 hash 注入额外字符 |
generateBundle | 全部产物定型后、写盘前 | 跨文件的整体增删改:删除冗余 chunk、追加清单/元数据文件、改写文件内容 |
writeBundle | 文件已写入磁盘后 | 记录产物、做后处理通知,此时不能通过删除来阻止写盘 |
八、实战要点小结
围绕 plugin-hooks-generatebundle.md 的核心语义,落地到实际插件开发中,请牢记以下三条铁律:
- 阻止输出用
delete bundle[fileName]:这是唯一受支持的文件移除方式,绑定层会把它记入deleted并回传 Rust(参考 tests/fixtures/misc/remove-chunk/_config.ts)。 - 新增输出一律
this.emitFile:无论是 asset、chunk 还是 prebuilt-chunk,只有经过file_emitter登记的文件才会在钩子返回后被add_additional_files追加进产物(output_hooks.rs);任何直接向bundle赋值的尝试都会触发UNSUPPORTED_BUNDLE_ASSIGNMENT警告并被忽略。 - 就地修改有限但够用:
code、fileName、map(含debugId、x_google_ignoreList)等核心字段的改动会通过collectChangedBundle回传 Rust 并写入最终产物;插件之间按注册顺序链式可见。
如需深入,推荐继续阅读:plugin-context-emitfile.md(emitFile三种类型的完整语义与示例)、bindingify-output-hooks.ts(JS 钩子到 binding 的桥接实现)、output_hooks.rs(Rust 侧驱动循环),以及 generate-bundle/_config.ts(覆盖增删改全部场景的官方回归测试)。
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考