.scss
or .yaml
. In the context of Bun’s bundler, plugins can be used to implement framework-level features like CSS extraction, macros, and client-server code co-location.
Lifecycle hooks
Plugins can register callbacks to be run at various points in the lifecycle of a bundle:onStart()
: Run once the bundler has started a bundleonResolve()
: Run before a module is resolvedonLoad()
: Run before a module is loaded.onBeforeParse()
: Run zero-copy native addons in the parser thread before a file is parsed.
Reference
A rough overview of the types (please refer to Bun’sbun.d.ts
for the full type definitions):
Usage
A plugin is defined as simple JavaScript object containing aname
property and a setup
function.
plugins
array when calling Bun.build
.
Plugin lifecycle
Namespaces
onLoad
and onResolve
accept an optional namespace
string. What is a namespace?
Every module has a namespace. Namespaces are used to prefix the import in transpiled code; for instance, a loader with a filter: /\.yaml$/
and namespace: "yaml:"
will transform an import from ./myfile.yaml
into yaml:./myfile.yaml
.
The default namespace is "file"
and it is not necessary to specify it, for instance: import myModule from "./my-module.ts"
is the same as import myModule from "file:./my-module.ts"
.
Other common namespaces are:
"bun"
: for Bun-specific modules (e.g."bun:test"
,"bun:sqlite"
)"node"
: for Node.js modules (e.g."node:fs"
,"node:path"
)
onStart
Promise
. After the bundle process has initialized, the bundler waits until all onStart()
callbacks have completed before continuing.
For example:
onStart()
(sleeping for 10 seconds) has completed, as well as the second onStart()
(writing the bundle time to a file).
Note that onStart()
callbacks (like every other lifecycle callback) do not have the ability to modify the build.config
object. If you want to mutate build.config
, you must do so directly in the setup()
function.
onResolve
onResolve()
plugin lifecycle callback allows you to configure how a module is resolved.
The first argument to onResolve()
is an object with a filter
and namespace
property. The filter is a regular expression which is run on the import string. Effectively, these allow you to filter which modules your custom resolution logic will apply to.
The second argument to onResolve()
is a callback which is run for each module import Bun finds that matches the filter
and namespace
defined in the first argument.
The callback receives as input the path to the matching module. The callback can return a new path for the module. Bun will read the contents of the new path and parse it as a module.
For example, redirecting all imports to images/
to ./public/images/
:
onLoad
onLoad()
plugin lifecycle callback allows you to modify the contents of a module before it is read and parsed by Bun.
Like onResolve()
, the first argument to onLoad()
allows you to filter which modules this invocation of onLoad()
will apply to.
The second argument to onLoad()
is a callback which is run for each matching module before Bun loads the contents of the module into memory.
This callback receives as input the path to the matching module, the importer of the module (the module that imported the module), the namespace of the module, and the kind of the module.
The callback can return a new contents
string for the module as well as a new loader
.
For example:
import env from "env"
into a JavaScript module that exports the current environment variables.
.defer()
One of the arguments passed to the onLoad
callback is a defer
function. This function returns a Promise
that is resolved when all other modules have been loaded.
This allows you to delay execution of the onLoad
callback until all other modules have been loaded.
This is useful for returning contents of a module that depends on other modules.
Example: tracking and reporting unused exports
.defer()
function currently has the limitation that it can only be called once per onLoad
callback.
Native plugins
One of the reasons why Bun’s bundler is so fast is that it is written in native code and leverages multi-threading to load and parse modules in parallel. However, one limitation of plugins written in JavaScript is that JavaScript itself is single-threaded. Native plugins are written as NAPI modules and can be run on multiple threads. This allows native plugins to run much faster than JavaScript plugins. In addition, native plugins can skip unnecessary work such as the UTF-8 -> UTF-16 conversion needed to pass strings to JavaScript. These are the following lifecycle hooks which are available to native plugins:onBeforeParse()
: Called on any thread before a file is parsed by Bun’s bundler.
Creating a native plugin in Rust
Native plugins are NAPI modules which expose lifecycle hooks as C ABI functions. To create a native plugin, you must export a C ABI function which matches the signature of the native lifecycle hook you want to implement.terminal
terminal
lib.rs
file, we’ll use the bun_native_plugin::bun
proc macro to define a function which
will implement our native plugin.
Here’s an example implementing the onBeforeParse
hook: