Rolldown generateBundle 钩子完全指南:在产物落盘前对输出文件做增删改
2026/9/15 18:44:30 网站建设 项目流程

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 中与outputOptionsrenderStartrenderChunkaugmentChunkHashwriteBundle等并列注册为ENUMERATED_OUTPUT_PLUGIN_HOOK_NAMES

从 Rust 核心的调用链可以精确还原它的触发时机。在 crates/rolldown/src/bundle/bundle.rs 的bundle_up中:

  1. GenerateStage::generate完成 chunk 实例化与代码生成;
  2. 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
  3. 然后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>

三个参数的含义如下:

参数类型说明
outputOptionsNormalizedOutputOptions归一化后的输出选项,如formatentryFileNamessourcemap
bundleOutputBundle文件名为键的输出文件集合,值为OutputChunkOutputAsset
isWriteboolean本次调用是bundle.generate()false,仅生成内存产物)还是bundle.write()true,会落盘)

OutputBundle的类型定义见 types/output-bundle.ts:

export interface OutputBundle { [fileName: string]: OutputAsset | OutputChunk; }

chunk 对象暴露的字段可在 types/output-chunk-impl.ts 中查到:fileNamenamecodeexportsisEntryisDynamicEntryfacadeModuleIdmoduleIdsmodulesimportsdynamicImportsmapsourcemapFileNamepreliminaryFileName等。

三、操作一:删除文件,阻止其被发出

核心语义(来自关联文档原文):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 thethis.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 的emitPrebuiltChunkemitChunkemitFile

五、危险操作:禁止直接向 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.filemap.mappingsmap.sourcesmap.sourcesContentmap.namesmap.x_google_ignoreListmap.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会把被更新文件的codefilenamenamemapimports等字段打包回传 Rust,覆盖掉对应资产的内容,最终写出的是修改后的版本——测试afterTest中断言chunks[0].code'console.error()'即验证了这一点。

注意:并非所有属性都能回传,collectChangedBundle只挑选了codefilenamenameisEntryexportsimportsdynamicImportsfacadeModuleIdisDynamicEntrymoduleIdsmapsourcemapFilenamepreliminaryFilename等核心字段,源码注释也注明“not all properties modifications are reflected to Rust side”(并非所有属性修改都会反映到 Rust 侧)。

七、执行顺序与 writeBundle 的区别

generateBundle按插件的注册顺序依次调用,同一钩子内所有插件可依赖“前一个插件的修改对后一个插件可见”。测试 generate-bundle/_config.ts 中声明了test-plugintest-plugin-2test-update-sourcemaptest-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
augmentChunkHashchunk hash 计算前依据内容向 hash 注入额外字符
generateBundle全部产物定型后、写盘前跨文件的整体增删改:删除冗余 chunk、追加清单/元数据文件、改写文件内容
writeBundle文件已写入磁盘后记录产物、做后处理通知,此时不能通过删除来阻止写盘

八、实战要点小结

围绕 plugin-hooks-generatebundle.md 的核心语义,落地到实际插件开发中,请牢记以下三条铁律:

  1. 阻止输出用delete bundle[fileName]:这是唯一受支持的文件移除方式,绑定层会把它记入deleted并回传 Rust(参考 tests/fixtures/misc/remove-chunk/_config.ts)。
  2. 新增输出一律this.emitFile:无论是 asset、chunk 还是 prebuilt-chunk,只有经过file_emitter登记的文件才会在钩子返回后被add_additional_files追加进产物(output_hooks.rs);任何直接向bundle赋值的尝试都会触发UNSUPPORTED_BUNDLE_ASSIGNMENT警告并被忽略。
  3. 就地修改有限但够用codefileNamemap(含debugIdx_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询