Rolldown Plugin
References:
Overview
Plugins allow customizing Rolldown's behavior. Some use cases:
Transpile code before bundling.
Shim built-in modules.
Inject virtual modules.
Rolldown's plugin interface is almost fully compatible with Rollup's. (For context, Rolldown is a rust migration of Rollup, if I recall correctly)
By definition:
A plugin is just an object that satisfies the specific plugin interface of Rolldown.
Typically it is distributed as a package that exports a factory function: The function takes plugin-specific options, returns the plugin object.
Remark: I have seen plugins registered like this
{
plugins: [
plugin(options) // `plugin` is the factory function that creates a plugin object
]
}See an example here: https://rolldown.rs/apis/plugin-api#example (there's a notice about using hook filters where possible). Essentially:
The plugin package exports a plugin factory.
The plugin factory returns a plugin object.
Conventions
Naming: Plugin names should be prefixed with
rolldown-plugin-.package.jsonkeywords: Includerolldown-plugin.Source mappings should be correctly output.
Virtual modules have their own conventions (see below).
User-facing ID should be prefixed with
virtual:. Example:virtual:example,virtual:posts/helpers.Use the plugin name as a namespace to avoid collisions. Example:
rolldown-plugin-postsusesvirtual:posts.Prefix the resolved ID with
\0(null byte).-> This tells other plugins and Rolldown itself "this is virtual, don't try to resolve it on disk".S
-> Sourcemaps also use this to distinguish virtual modules from real files.
Note:
Modules derived from a real file (like submodules from
.vueor.svelteSFCs) should NOT use the\0prefix.
Using it would break sourcemaps, since those submodules can be mapped back to the actual file on disk.
Plugin Interface
The plugin object has:
One required property:
name.Everything else is optional hooks.
Hooks
Definition: Hooks are methods on the plugin object that Rolldown calls at various stages of the build.
Basically, it is something like this:
{
name: "...",
hook1 () { console.log("Rolldown will call this at a known point during the buld") }
}Hooks can:
Affect how a build runs.
Provide info about it.
Modify it after completion.
When multiple plugins define the same hook (e.g. both pluginA and pluginB define transform), the hook's kind determines how Rolldown coordinates them.
The type is fixed per hook in Rolldown's spec, not chosen by the plugin author. For example, resolveId is always first, transform is always sequential. The plugin author just defines the method, and Rolldown knows how to coordinate it.
The following specifies the hook kinds:
<span id="hook-kind-async"></span>
async:The hook may return a Promise resolving to the same type of value.
Otherwise it is
sync.
<span id="hook-kind-first"></span>
first:Plugins implementing this hook run sequentially until one returns a non-
null/non-undefinedvalue.The rest are skipped.
<span id="hook-kind-sequential"></span>
sequential:All plugins run in the specificed plugin order.
If
async, each waits for the previous to resolve.
<span id="hook-kind-parallel"></span>
parallel:All plugins run in the specified plugin order.
If
async, they run concurrently (don't wait for each other).
Remark: A hook can also be specified as an object with a
handlerproperty instead of a plain method. This is theObjectHookform, which allows attaching additional metadata to control behavior (e.g. hook filters).
There are two types of hooks:
Build hooks: Run during the build phase.
Output generation hooks: Run during output generation.
Remark: Ok, following convention, we will distinguish between:
Hook kind: Rolldown specification of how a defined hook is coordinated if multiple plugins define the hook.
Hook type: Rolldown specification of when the hook is run.
Build Hooks
Build hooks are concerned with:
Locating
Providing
Transforming
input files before Rolldown processes them.
Remarks: So basically, a build hooks can either provide an input or transform an existing input.
The lifecycle:
First hook:
options.Last hook: always
buildEnd.If a build error occurs,
closeBundleis called afterbuildEnd.
Remark: There is an internal step called
internalTransformin Rolldown's pipeline graph. This is NOT a plugin hook. It is where Rolldown transforms non-JS code to JS.
In watch mode:
watchChangecan be triggered at any time to notify that a new run will start once the current run finishes its outputs.closeWatcheris triggered when the watcher closes.
The following are supported by Rollup but not Rolldown:
shouldTransformCachedModule(rolldown#4389).
Output Generation Hooks
Output generation hooks:
Provide information about a generated bundle.
Modify a build once complete (Post-transform).
Plugins that ONLY use output generation hooks can also be passed in via the output options, so they run only for certain outputs.
The lifecycle:
First hook:
renderStart.Last hook depends on the outcome:
generateBundleif output was successfully generated viabundle.generate(...).writeBundleif output was successfully generated viabundle.write(...).renderErrorif an error occurred during output generation.
Remark:
bundle.generate()produces the output in memory only.bundle.write()does the same but also writes files to disk. SogenerateBundlefires in both cases, whilewriteBundleonly fires when files are actually written. The sequence forbundle.write()is:generateBundlethenwriteBundle.
closeBundlecan be called as the very last hook, but the user must manually callbundle.close()to trigger it. The CLI always does this automatically.
Remark:
minifyin the pipeline graph is NOT a plugin hook. It is the step where Rolldown runs the minifier. Similarly,postBannerandpostFooterare output options, not hooks (unlikebannerandfooterwhich do have corresponding hooks).
The following are supported by Rollup but not Rolldown:
resolveImportMeta(rolldown#1010).
renderDynamicImport(rolldown#4532).
Plugin Context
Inside most hooks, this refers to a PluginContext object that provides utility functions and build information. For example, this.resolve() to resolve an import, this.emitFile() to emit a file, this.getModuleInfo() to inspect a module, etc.
Remark: This means hooks must be regular functions (not arrow functions) to access
this.
Supporting TypeScript and JSX
Rolldown runs internalTransform (TS/JSX to JS) after the transform hooks. This means plugins using transform receive TypeScript/JSX code, not plain JS.
Two ways to handle this:
Parse the TS/JSX directly:
this.parsesupports TypeScript and JSX via alangoption. So if your plugin works with the AST, just pass the right lang and it works.Transform to JS first: If working with TS/JSX AST is not an option, use the
transformfunction fromrolldown/utilsto convert to JS before processing. This has additional overhead since it runs an extra transform pass.
Remark: This is a key difference from Rollup, where transforms typically receive plain JS. In Rolldown, you might see type annotations and JSX in the
transformhook input.
Notable Differences from Rollup
While Rolldown's plugin interface is largely compatible with Rollup's, there are some behavioral differences.
Output Generation Handling
In Rollup, all outputs are generated together in a single process. Rolldown handles each output generation separately. If you have multiple output configurations, Rolldown processes each output independently. This affects plugins that maintain state across the build.
Concrete differences:
outputOptionshook is called before build hooks in Rolldown. Rollup calls them after.Build hooks are called for each output separately in Rolldown. Rollup calls them once for all outputs.
closeBundlehook is called only whengenerate()orwrite()was called at least once. Rollup calls it regardless.
Watch Mode
In Rollup, the options hook is called on every rebuild in watch mode. In Rolldown, options is only called once when the watcher is created, not on subsequent rebuilds.
Sequential Hook Execution
In Rollup, writeBundle is parallel by default, so plugins need to explicitly set sequential: true if they need ordered execution.
In Rolldown, writeBundle is already sequential by default.