Plugin hook filters
NOTE
For more details about why you need plugin hook filter please refer why-plugin-hook-filter
One important thing to note for JavaScript plugins in Rolldown is that every plugin hook call incurs a small communication overhead between Rust and the JavaScript runtime (i.e. Node.js).
Consider the following plugin:
export default function myPlugin() {
return {
name: 'example',
transform(code, id) {
if (!id.endsWith('.data')) {
// early return
return
}
// perform actual transform
return transformedCode
},
}
}Line 5 is a very common pattern in Rollup plugins: check the file extension of a module inside the plugin hook to determine whether it needs to be processed.
However, this would be sub-optimal in a Rust bundler like Rolldown. Imagine this plugin is used in a large app with thousands of modules - Rolldown would have to invoke a Rust-to-JS call for every module, and in many cases just for the plugin to find out it doesn't actually need to do anything. Due to the single-threaded nature of JavaScript, unnecessary Rust-to-JS calls can also de-optimize parallelization.
Ideally, we want to be able to determine whether a plugin hook needs to be invoked at all without leaving Rust. This is why Rolldown augments Rollup plugin's object hook format with the additional filter property. The above plugin can be updated to:
export default function myPlugin() {
return {
name: 'example',
transform: {
filter: {
id: /\.data$/
},
handler (code) {
// perform actual transform
return transformedCode
},
}
}
}Rolldown can now compile and execute the regular expression on the Rust side, and can avoid invoking JS if the filter does not match.
In addition to id, you can also filter based on moduleType and the module's source code. The filter property works similarly to createFilter from @rollup/pluginutils. Here are some important details:
- If multiple values are passed to
include, the filter matches if any of them match. - If a filter has both
includeandexclude,excludetakes precedence. - If multiple filter properties are specified, the filter matches when all of the specified properties match. In other words, if even one property fails to match, it is excluded, regardless of the other properties. For example, the following filter matches a module only if its file names ends with
.js, its source code containsfoo, and does not containbar:js{ id: { include: /\.js$/, exclude: /\.ts$/ }, code: { include: 'foo', exclude: 'bar' } }
Full HookFilter interface for the filter property:
interface HookFilter {
/**
* This filter is used to do a pre-test to determine whether the hook should be called.
*
* @example
* Include all `id`s that contain `node_modules` in the path.
* ```js
* { id: '**'+'/node_modules/**' }
* ```
* @example
* Include all `id`s that contain `node_modules` or `src` in the path.
* ```js
* { id: ['**'+'/node_modules/**', '**'+'/src/**'] }
* ```
* @example
* Include all `id`s that start with `http`
* ```js
* { id: /^http/ }
* ```
* @example
* Exclude all `id`s that contain `node_modules` in the path.
* ```js
* { id: { exclude: '**'+'/node_modules/**' } }
* ```
* @example
* Formal pattern to define includes and excludes.
* ```
* { id : {
* include: ['**'+'/foo/**', /bar/],
* exclude: ['**'+'/baz/**', /qux/]
* }}
* ```
*/
id?: StringFilter;
moduleType?: ModuleTypeFilter;
code?: StringFilter;
}The following properties are supported by each hook:
resolveIdhook:idloadhook:idtransformhook:id,moduleType,code
NOTE
id is treated as a glob pattern when you pass a string, and treated as a regular expression when you pass a RegExp. In the resolve hook, id must be a RegExp. strings are not allowed. This is because the id value in resolveId is the exact text written in the import statement and usually not an absolute path, while glob patterns are designed to match absolute paths.
Improving interoperability
Plugin hook filters are supported in Rollup 4.38.0+, Vite 6.3.0+, and all versions of Rolldown. However, if you're authoring a plugin that needs to support older versions of Rollup (< 4.38.0) or Vite (< 6.3.0), you can provide a fallback implementation that works in both environments.
The strategy is to use the object hook format with filters when available, and fall back to a regular function that checks conditions internally for older versions:
const idFilter = /\.data$/;
export default function myPlugin() {
return {
name: 'my-plugin',
transform: {
// Filter is used by Rolldown and newer Rollup/Vite versions
filter: { id: idFilter },
// Handler is called when filter matches
handler(code, id) {
// Double-check in handler for compatibility with older versions
// This is only necessary if you're supporting older versions
if (!idFilter.test(id)) {
return null;
}
// perform actual transform
return transformedCode;
},
},
};
}This approach ensures your plugin will:
- Use filters for optimal performance in Rolldown and newer Rollup/Vite versions
- Still work correctly in older versions (they will call the handler for all files, but the internal check ensures correct behavior)
TIP
When supporting older versions, keep both the filter pattern and the internal check in sync to avoid confusion.
moduleType filter is not supported by Rollup / Vite 7 and below
Module Type concept does not exist in Rollup / Vite 7 and below. For that reason, moduleType filter is not supported by those tools and will be ignored.