Universal plugin API for extending the Bun runtime and bundler
Bun provides a universal plugin API that can be used to extend both the runtime and bundler.Plugins intercept imports and perform custom loading logic: reading files, transpiling code, etc. They can be used to add support for additional file types, like .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.
onLoad and onResolve accept an optional namespace string. What is a namespace?Each module has a namespace. Namespaces are used to prefix import paths in transpiled code; for instance, a loader with filter: /\.yaml$/ and namespace: "yaml:" would convert an import ./myfile.yaml to yaml:./myfile.yaml.The default namespace is "file", which usually doesn’t need to be explicitly specified, e.g.: import myModule from "./my-module.ts" is equivalent to import myModule from "file:./my-module.ts".Other common namespaces:
The callback can return a Promise. After the bundler initializes, it will wait for all onStart() callbacks to complete before proceeding.For example:
const result = await Bun.build({ entrypoints: ["./app.ts"], outdir: "./dist", sourcemap: "external", plugins: [ { name: "Sleep for 10 seconds", setup(build) { build.onStart(async () => { await Bun.sleep(10_000); }); }, }, { name: "Log bundle time to a file", setup(build) { build.onStart(async () => { const now = Date.now(); await Bun.$`echo ${now} > bundle-time.txt`; }); }, }, ],});
In the example above, Bun will wait for both the first onStart() (sleep 10 seconds) and the second onStart() (write bundle time to file) to complete before continuing.
onStart() callbacks (and all lifecycle callbacks) cannot modify the build.config object. To modify build.config, you must do so directly in the setup() function.
To bundle a project, Bun traverses the dependency tree of all modules. For each imported module, Bun needs to find and read the module. This process of “finding” a module is called “resolution”.The onResolve() plugin lifecycle callback lets you customize the module resolution logic.The first argument to onResolve() is an object with a filter and optional namespace property. The filter is a regular expression used to match against import strings; in practice, it’s used to filter which modules should use the custom resolution logic.The second argument is a callback function that will be called for each imported module that matches the filter and namespace from the first argument.The callback receives the matched module path as input, and 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, to redirect all imports of images/ to ./public/images/:
After the Bun bundler resolves a module, it needs to read the module contents and parse them.The onLoad() plugin lifecycle callback lets you modify the contents of a resolved module before Bun reads and parses them.Like onResolve(), the first argument to onLoad() is used to filter which modules this call should apply to.The second argument is a callback that will be called before the contents of each matched module are loaded.The callback receives the path of the matched module, the module’s importer, the module namespace, and the module kind.The callback can return a new contents string and a new loader for the module.For example:
One of the arguments passed to the onLoad callback is a defer function. This function returns a Promise that only resolves after all other modules have finished loading.This allows you to defer the execution of the onLoad callback until all other modules are loaded.This is useful for returning module contents that depend on the contents of other modules.
Example: Track and report unused exports
import { plugin } from "bun";plugin({ name: "track imports", setup(build) { const transpiler = new Bun.Transpiler(); let trackedImports: Record<string, number> = {}; // For each module that goes through this onLoad callback, // record its imports into trackedImports build.onLoad({ filter: /\.ts/ }, async ({ path }) => { const contents = await Bun.file(path).arrayBuffer(); const imports = transpiler.scanImports(contents); for (const i of imports) { trackedImports[i.path] = (trackedImports[i.path] || 0) + 1; } return undefined; }); build.onLoad({ filter: /stats\.json/ }, async ({ defer }) => { // Wait until all files have been loaded, ensuring // the above onLoad callback has executed for each file and imports are tracked await defer(); // Output a JSON with statistics for each import return { contents: `export default ${JSON.stringify(trackedImports)}`, loader: "json", }; }); },});