Skip to main content
Bun provides a universal plugin API that works in both the runtime and the bundler. Plugins intercept imports and perform custom loading logic: reading files, transpiling code, transforming content, and more. Use plugins to:
  • Add support for file types Bun doesn’t handle natively (.scss, .yaml, .svg, etc.)
  • Implement custom module resolution rules
  • Inject code or environment data as virtual modules
  • Apply framework-level transforms like CSS extraction or macros

Creating a plugin

A plugin is a plain object with a name string and a setup function. The setup function receives a build object with lifecycle hook registration methods.
my-plugin.ts
import type { BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "my-plugin",
  setup(build) {
    // register lifecycle hooks here
  },
};

Using a plugin at runtime

To apply a plugin when running code with bun run, use Bun.plugin() in a preload file and register that file in bunfig.toml.
1

Define the plugin

my-plugin.ts
import { plugin } from "bun";

plugin({
  name: "text-loader",
  setup(build) {
    build.onLoad({ filter: /\.txt$/ }, async (args) => {
      const text = await Bun.file(args.path).text();
      return {
        contents: `export default ${JSON.stringify(text)}`,
        loader: "js",
      };
    });
  },
});
2

Register as a preload in bunfig.toml

bunfig.toml
preload = ["./my-plugin.ts"]
3

Import the custom file type

index.ts
import message from "./greeting.txt";
console.log(message); // contents of greeting.txt as a string

Using a plugin in the bundler

Pass the plugin to Bun.build via the plugins array.
build.ts
import { myPlugin } from "./my-plugin";

await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  plugins: [myPlugin],
});

Lifecycle hooks

onLoad

onLoad intercepts a module after it has been resolved and lets you replace its contents before Bun parses it.
build.onLoad(
  args: { filter: RegExp; namespace?: string },
  callback: (args: { path: string; importer: string; namespace: string }) => {
    loader?: Loader;
    contents?: string;
    exports?: Record<string, any>;
  }
): void;
The filter regex is matched against the file path. Return contents (a string) and optionally a loader to tell Bun how to interpret the content. Example: virtual environment module
import { plugin } from "bun";

plugin({
  name: "env-plugin",
  setup(build) {
    build.onLoad({ filter: /^env$/, namespace: "file" }, () => {
      return {
        contents: `export default ${JSON.stringify(process.env)}`,
        loader: "js",
      };
    });
  },
});

// Usage: import env from "env"
// env.NODE_ENV === "production"
Example: load .txt files as strings
import { plugin } from "bun";

plugin({
  name: "txt-loader",
  setup(build) {
    build.onLoad({ filter: /\.txt$/ }, async (args) => {
      const text = await Bun.file(args.path).text();
      return {
        contents: `export default ${JSON.stringify(text)}`,
        loader: "js",
      };
    });
  },
});

.defer()

The second argument to onLoad is a defer function. Calling await defer() pauses execution of this callback until all other modules have been loaded. This is useful when a module’s content depends on what other modules export.
plugin({
  name: "import-stats",
  setup(build) {
    const transpiler = new Bun.Transpiler();
    const trackedImports: Record<string, number> = {};

    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 for every other onLoad to finish
      await defer();
      return {
        contents: `export default ${JSON.stringify(trackedImports)}`,
        loader: "json",
      };
    });
  },
});

onResolve

onResolve intercepts module resolution and lets you redirect imports to different paths.
build.onResolve(
  args: { filter: RegExp; namespace?: string },
  callback: (args: { path: string; importer: string }) => {
    path: string;
    namespace?: string;
  } | void
): void;
Example: redirect image imports
import { plugin } from "bun";

plugin({
  name: "image-resolver",
  setup(build) {
    build.onResolve({ filter: /.*/, namespace: "file" }, (args) => {
      if (args.path.startsWith("images/")) {
        return {
          path: args.path.replace("images/", "./public/images/"),
        };
      }
    });
  },
});

onStart

onStart registers a callback that runs once when the bundler starts. It can be async; the bundler waits for all onStart callbacks to resolve before continuing.
import { plugin } from "bun";

plugin({
  name: "start-logger",
  setup(build) {
    build.onStart(async () => {
      const now = Date.now();
      await Bun.$`echo ${now} > build-time.txt`;
    });
  },
});
onStart callbacks cannot modify build.config. To mutate the build config, do so directly inside setup() before registering any hooks.

Available loaders

The loader field in an onLoad return value tells Bun how to parse the returned contents.
LoaderDescription
jsJavaScript
jsxJavaScript with JSX
tsTypeScript
tsxTypeScript with JSX
jsonJSON — exported as a default object
jsoncJSON with comments
tomlTOML — exported as a default object
yamlYAML — exported as a default object
textPlain text — exported as a default string
fileBinary file — exported as a file path
cssCSS stylesheet
htmlHTML document
wasmWebAssembly module
napiNative Node-API module

Namespaces

Every module has a namespace. The default namespace is "file". Namespaces are used to prefix module paths in transpiled code.
import foo from "bun:test"    → namespace: "bun",  path: "test"
import bar from "node:fs"     → namespace: "node", path: "fs"
import baz from "./file.ts"   → namespace: "file", path: "./file.ts"
Specifying a custom namespace in onLoad or onResolve scopes the hook to only matching modules:
build.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
  // only runs for imports like: import x from "virtual:..."
});

Native plugins

JavaScript plugins run on a single thread. For maximum performance, you can write a native plugin as a Node-API (NAPI) module. Native plugins run on multiple threads and avoid UTF-8 to UTF-16 conversion overhead.

onBeforeParse

The onBeforeParse hook is the only lifecycle hook available to native plugins. It runs immediately before a file is parsed, on any thread, and can replace the file’s source code.
import myNativeAddon from "./my-native-addon";

await Bun.build({
  entrypoints: ["./app.tsx"],
  plugins: [
    {
      name: "native-transform",
      setup(build) {
        build.onBeforeParse(
          { namespace: "file", filter: /\.tsx$/ },
          {
            napiModule: myNativeAddon,
            symbol: "replace_foo_with_bar",
          },
        );
      },
    },
  ],
});
The native module exports a C ABI function matching the onBeforeParse signature. Here is an example in Rust using the bun-native-plugin crate:
lib.rs
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, BunLoader};
use napi_derive::napi;

define_bun_plugin!("replace-foo-with-bar");

#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
  let input = handle.input_source_code()?;
  let output = input.replace("foo", "bar");
  handle.set_output_source_code(output, BunLoader::BUN_LOADER_JSX);
  Ok(())
}

Plugin type reference

type BunPlugin = {
  name: string;
  setup(build: PluginBuilder): void | Promise<void>;
};

type PluginBuilder = {
  onStart(callback: () => void | Promise<void>): void;
  onResolve(
    args: { filter: RegExp; namespace?: string },
    callback: (args: { path: string; importer: string }) =>
      { path: string; namespace?: string } | void,
  ): void;
  onLoad(
    args: { filter: RegExp; namespace?: string },
    callback: (args: { path: string; importer: string; namespace: string; defer: () => Promise<void> }) =>
      { loader?: Loader; contents?: string; exports?: Record<string, any> } | undefined,
  ): void;
  config: BuildConfig;
};

type Loader =
  | "js" | "jsx" | "ts" | "tsx"
  | "json" | "jsonc" | "toml" | "yaml"
  | "text" | "file" | "css" | "html"
  | "wasm" | "napi";

Build docs developers (and LLMs) love