Rolldown 插件对象式 Hook 详解:order 排序、filter 过滤与 sequential 兼容
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
导读
在 Rolldown(基于 Rust 的高性能 JavaScript/TypeScript 打包器,提供 Rollup 兼容 API)中,插件 Hook 既可以写成纯函数,也可以写成携带附加属性的对象形式(Object Hook)。对象形式为插件作者提供了两个关键能力:通过order控制同一 Hook 在多个插件间的执行顺序,通过filter让 Hook 仅在匹配条件下被调用(由 Rust 侧预先判定,省去 JS/Rust 往返开销)。本文以 object-hook.md 为核心,结合仓库源码深入讲解这两种附加属性(Additional Properties)的语义、类型约束、使用示例与底层实现,帮助你在编写 Rolldown / Rollup 兼容插件时精确掌控 Hook 行为。
什么是对象式 Hook:函数形式与对象形式
Rolldown 的类型系统中,一个 Hook 的完整形态被定义为ObjectHook<T, O>,见 plugin/index.ts:
export type ObjectHook<T, O = {}> = T | ({ handler: T } & ObjectHookMeta & O);这意味着每个 Hook 都有两种合法写法:
- 函数形式(简写):直接提供一个函数(或字符串,如
banner/footer/intro/outro等 addon Hook),例如:
export default function myPlugin() { return { name: 'my-plugin', transform(code, id) { return code.replace('foo', 'bar'); }, }; }- 对象形式(Object Hook):将真正的回调放进
handler字段,同时可以附加order、filter等元信息:
export default function myPlugin() { return { name: 'my-plugin', transform: { filter: { id: /\.js$/ }, handler(code, id) { return code.replace('foo', 'bar'); }, }, }; }底层适配层在把插件桥接到 Rust 侧(binding)之前,会先通过normalizeHook对两种形式做归一化,见 utils/normalize-hook.ts:函数/字符串形式被包装为{ handler, options: {}, meta: {} };对象形式则把handler、order从其余附加属性(即filter等)中分离出来,order进入meta,其余进入options,随后一并传递给bindingifyHook(见 bindingify-plugin-hook-meta.ts)与各bindingify*绑定函数。
order:控制多插件间的 Hook 执行顺序
类型与语义
order属性的类型为:
type PluginOrder = 'pre' | 'post' | null;见 plugin/index.ts。其语义为:当多个插件实现了同一个 Hook 时,"pre"表示该插件最先运行,"post"表示最后运行,不写或写null则表示保持在用户指定的插件顺序位置上。如果多个插件同时使用了"pre"或"post",Rolldown 会按照它们在用户配置中出现的先后顺序依次执行。该选项适用于所有插件 Hook。
示例:优先拦截外部模块解析
原文档给出的resolveId示例完整展示了order: 'pre'的用法:
export default function resolveFirst() { return { name: 'resolve-first', resolveId: { order: 'pre', handler(source) { if (source === 'external') { return { id: source, external: true }; } return null; }, }, }; }当source恰好为'external'时,该插件率先将其标记为 external 模块(通过返回{ id, external: true }),其余情况下返回null将解析权让渡给后续的resolveIdHook 乃至默认解析逻辑(resolveId的完整返回值语义见 plugin/index.ts)。
底层实现:order 如何被消费
从源码结构看,order的传递链路是这样的:
normalizeHook把对象形式中的order提取到meta.order;bindingifyPluginHookMeta将'pre' | 'post' | null | undefined映射为 Rust 侧的枚举BindingPluginOrder(Pre/Post/ 空),见 [bindingify-plugin-hook-meta.ts](https://gitcode.com/GitHub_Trending/ro/rolldown/blob/8744801f470e1305a735ad4089c3c3eff4c3a33e/packages/rolldown/src/plugin/bindingify-plugin-hook-meta.ts?utm_source=gitcode_repo_files#L6-L24),非法值会抛出Unknown plugin order错误;- 最终通过
BindingPluginOptions上的*Meta字段(如resolveIdMeta)下发给 Rust 运行时进行调度,见 [bindingify-plugin.ts](packages/rolldown/src/plugin/bindingify-plugin.ts#L80-L189)。
因此,order的生效发生在 Rust 核心的 Hook 调度阶段,而不是 JS 层手动排序——这也是它性能友好、语义统一的原因。
filter:让 Hook 只在匹配时被调用
类型与适用 Hook
filter属性的类型为HookFilter或TopLevelFilterExpression[](具体取决于 Hook)。它仅对resolveId、load、transform三个 Hook 可用,这一点在类型层通过HookFilterExtension精确约束,见 plugin/index.ts:
transform:filter支持id、moduleType、code;load:filter仅支持id;resolveId:filter仅支持id(且必须是RegExp,不支持字符串,原因下文详述);renderChunk:类型上同样支持基于code的 filter(见同一文件HookFilterExtension中对renderChunk的扩展)。
HookFilter接口定义在 plugin/hook-filter.ts,其核心字段为:
| 字段 | 类型 | 语义 | 可用 Hook |
|---|---|---|---|
id | GeneralHookFilter(字符串/正则/数组/{include, exclude}对象) | 按模块 id 匹配;字符串按 glob 处理,正则按路径匹配 | resolveId、load、transform |
moduleType | ModuleType[]或{ include } | 按模块类型(如js、tsx、json)匹配 | 仅transform |
code | GeneralHookFilter | 按模块源码内容匹配 | 仅transform |
其中GeneralHookFilter支持MaybeArray<Value>或{ include?, exclude? }两种形态:
export type GeneralHookFilter<Value = StringOrRegExp> = | MaybeArray<Value> | { include?: MaybeArray<Value>; exclude?: MaybeArray<Value>; };示例:按 id 与代码内容双重过滤
原文档中的transform示例展示了同时使用id与code两个维度进行过滤:
export default function jsxAdditionalTransform() { return { name: 'jsxAdditionalTransform', transform: { filter: { id: '*.jsx', code: '<Custom', }, handler(code) { // transform <Custom /> here }, }, }; }只有当模块 id 匹配 glob*.jsx且源码包含字符串'<Custom'时,handler才会被调用。多个 filter 属性之间是“且”的关系——只要其中一个不匹配,整个 Hook 就被跳过。include内部是“或”的关系,exclude优先级高于include(完整匹配规则见 docs/apis/plugin-api/hook-filters.md)。
filter 的价值:把匹配判定下沉到 Rust 侧
与在 handler 内部用if提前 return 的传统写法相比,filter 的关键差异在于判定发生在 Rust 侧:Rolldown 只在 filter 命中时才发起 Rust→JS 的跨语言调用。文档 docs/apis/plugin-api/hook-filters.md 指出,这能避免大量无谓的 JS 调用并提升并行化空间。
底层实现:filter 的绑定与校验
bindingify-hook-filter.ts(packages/rolldown/src/plugin/bindingify-hook-filter.ts)负责把 JS 侧的 filter 描述编译成 Rust 侧可消费的BindingFilterToken[](token 序列包括And/Or/Not/Id/ImporterId/ModuleType/Code/Include/Exclude/QueryKey/QueryValue/CleanUrl等),并做了两条重要的合法性校验:
importerId只能用于resolveId:assertNoImporterId会检查每个 filter 表达式树,若在其他 Hook(如load/transform/renderChunk)中使用了importerId,直接抛错The importerId filter can only be used with the resolveId hook(见 bindingify-hook-filter.ts)。resolveId的字符串idfilter 不被支持:因为resolveId收到的id是 import 语句里的原始写法(通常不是绝对路径),glob 与之无意义,必须使用RegExp;否则抛错A string id filter is not supported for the resolveId hook(见同文件 L141-L166)。
resolveId、load、transform三个 Hook 的绑定函数分别在bindingifyBuildStart/bindingifyResolveId/bindingifyLoad/bindingifyTransform中把options.filter转交给对应的bindingify*Filter函数,并把结果作为filter字段与plugin、meta一起放入BindingPluginOptions(见 bindingify-build-hooks.ts)。
组合过滤器(Composable Filters)
对于更复杂的过滤逻辑,还可以直接传入TopLevelFilterExpression[],使用@rolldown/pluginutils导出的组合函数(如and、or、not、id、importerId、moduleType、code、query、include、exclude、queries)来构建表达式树。例如:
import { and, id, include, moduleType } from '@rolldown/pluginutils'; export default function myPlugin() { return { name: 'my-plugin', transform: { filter: [include(and(id(/\.ts$/), moduleType('ts')))], handler(code, id) { // 仅当 id 匹配 /\.ts$/ 且 moduleType 为 'ts' 时调用 return transformedCode; }, }, }; }需要说明的是,组合过滤器目前仅适用于 Rolldown 插件(Vite 与 unplugin 中暂不支持),且字符串id在组合表达式中按“精确相等”匹配而非 glob。
为第三方插件注入 filter:withFilter 工具
如果你使用的插件没有自带 filter,但又想限制其 Hook 的触发范围,Rolldown 提供了withFilter辅助函数,从 plugin/with-filter.ts 导出:
import yaml from '@rollup/plugin-yaml'; import { defineConfig } from 'rolldown'; import { withFilter } from 'rolldown/filter'; export default defineConfig({ plugins: [ // 仅对以 .yaml 结尾的模块运行 yaml 插件的 transform Hook withFilter(yaml({}), { transform: { id: /\.yaml$/ } }), ], });withFilter会按插件name(支持字符串精确匹配与RegExp模式)定位目标插件,然后改写其transform/resolveId/load三个 Hook:若已是对象形式则覆盖filter,若是函数形式则包装成{ handler, filter }对象(见 with-filter.ts)。
sequential:已废弃的 Rollup 兼容选项
sequential是对象式 Hook 的一个**已废弃(Deprecated)**属性:
sequential?: boolean;它仅仅是为了兼容 Rollup 的插件类型而存在。在 Rolldown 中,Hook 的执行方式始终等价于sequential: true——即所有 Hook 都会以串行顺序依次等待执行,不存在sequential: false的并行语义。这一点在类型注释中有明确说明,见 plugin/index.ts:
/** * @deprecated * this is only for rollup Plugin type compatibility. * hooks always work as `sequential: true`. */ sequential?: boolean;编写插件时无需(也不应)依赖该选项控制并行行为;保留它只是为了能让现有 Rollup 插件代码在不改类型的情况下直接运行在 Rolldown 上。如果你想了解 Rolldown 到底支持哪些 Hook 以何种模式(sync/async、sequential/parallel/first)运行,可以参考FunctionPluginHooks中每个 Hook 的@kind注释以及FirstPluginHooks/SequentialPluginHooks/ParallelPluginHooks三个类型别名(见 plugin/index.ts)。
小结与建议
- 何时用对象形式:需要给 Hook 附加
order(调整多插件执行顺序)或filter(Rust 侧预筛、减少跨语言调用)时,把回调放入handler即可;其余场景函数简写完全等价。 order的选择:"pre"适合需要在其他插件之前介入的场景(如最先拦截解析、最先做代码注入),"post"适合收尾类逻辑;同优先级下按配置顺序执行。filter的选择:优先用对象形式 +filter代替 handler 内部if判断,尤其适合resolveId/load/transform这三个高频 Hook;注意resolveId的id只接受RegExp,transform还可以按moduleType与code过滤。sequential不要用:它是纯兼容字段,Rolldown 中 Hook 始终串行执行。
相关源码与文档的进一步阅读入口:object-hook.md、plugin/index.ts、bindingify-plugin.ts、bindingify-hook-filter.ts、hook-filter.ts、with-filter.ts、docs/apis/plugin-api/hook-filters.md。
【免费下载链接】rolldownFast Rust bundler for JavaScript/TypeScript with Rollup-compatible API.项目地址: https://gitcode.com/GitHub_Trending/ro/rolldown
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考