> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/oven-sh/bun/llms.txt
> Use this file to discover all available pages before exploring further.

# Bundler Plugins

> Extend Bun's bundler with custom loaders, module resolution, and build lifecycle hooks using a universal plugin API compatible with both the runtime and bundler.

Bun provides a universal plugin API that works in both the bundler (`Bun.build()`) and the runtime (via `preload` in `bunfig.toml`). Plugins intercept imports and perform custom loading logic — reading files, transforming code, resolving modules to virtual paths, and more.

## Creating a plugin

A plugin is a plain object with a `name` string and a `setup` function:

```typescript myPlugin.ts theme={null}
import type { BunPlugin } from "bun";

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

Pass it to `Bun.build()`:

```typescript theme={null}
await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./out",
  plugins: [myPlugin],
});
```

## SVG plugin example

A complete plugin that imports SVG files as string exports:

```typescript theme={null}
import type { BunPlugin } from "bun";

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

await Bun.build({
  entrypoints: ["./index.ts"],
  outdir: "./out",
  plugins: [svgPlugin],
});
```

## Lifecycle hooks

### `onStart`

Called once when the bundler starts a new bundle. Useful for setup, logging, or initializing shared state.

```typescript theme={null}
build.onStart(() => {
  console.log("Build started!");
});
```

`onStart` callbacks can be async. The bundler waits for all `onStart` callbacks to resolve before continuing.

<Note>
  `onStart` callbacks cannot modify `build.config`. Mutate the config directly inside `setup()` if needed.
</Note>

***

### `onResolve`

Intercepts module resolution. Lets you redirect imports to different paths or virtual namespaces.

```typescript theme={null}
build.onResolve(
  { filter: /.*/, namespace: "file" },
  (args) => {
    if (args.path.startsWith("images/")) {
      return {
        path: args.path.replace("images/", "./public/images/"),
      };
    }
  }
);
```

The callback receives `{ path, importer }` and can return `{ path, namespace? }` to override resolution, or `undefined` to let other resolvers handle it.

***

### `onLoad`

Intercepts module loading. Lets you replace or transform a file's contents before it is parsed.

```typescript theme={null}
build.onLoad({ filter: /env/, namespace: "file" }, (args) => {
  return {
    contents: `export default ${JSON.stringify(process.env)}`,
    loader: "js",
  };
});
```

The callback receives `{ path, importer, namespace, kind }` and can return:

| Field      | Type                  | Description                            |
| ---------- | --------------------- | -------------------------------------- |
| `contents` | `string`              | Replacement source code                |
| `loader`   | `Loader`              | How to interpret the returned contents |
| `exports`  | `Record<string, any>` | Directly export values (skips parsing) |

**Available loaders:** `js`, `jsx`, `ts`, `tsx`, `json`, `jsonc`, `toml`, `yaml`, `file`, `napi`, `wasm`, `text`, `css`, `html`

***

### `onEnd`

Called after the bundle is complete. Receives the full `BuildOutput` object.

```typescript theme={null}
build.onEnd((result) => {
  console.log(`Build completed: ${result.outputs.length} output files`);
  if (!result.success) {
    for (const log of result.logs) {
      console.error(log);
    }
  }
});
```

`onEnd` callbacks can be async. The promise returned by `Bun.build()` does not resolve until all `onEnd` callbacks have completed — useful for post-build tasks like uploading artifacts:

```typescript theme={null}
build.onEnd(async (result) => {
  if (!result.success) return;
  for (const output of result.outputs) {
    await uploadToS3(output);
  }
});
```

***

### `onBeforeParse` (native plugins only)

A native plugin hook that runs on any thread before a file is parsed. Only available to NAPI modules. See [native plugins](#native-plugins) below.

## Namespaces

Every module has a namespace. Common namespaces:

* `"file"` — files on disk (default)
* `"bun"` — Bun built-in modules (`bun:test`, `bun:sqlite`)
* `"node"` — Node.js built-in modules (`node:fs`, `node:path`)

Use namespaces to create virtual modules:

```typescript theme={null}
build.onResolve({ filter: /^virtual:/, namespace: "file" }, (args) => {
  return {
    path: args.path.slice("virtual:".length),
    namespace: "virtual",
  };
});

build.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
  return {
    contents: `export const name = ${JSON.stringify(args.path)}`,
    loader: "js",
  };
});
```

## `.defer()` in `onLoad`

The `defer` argument in `onLoad` returns a `Promise` that resolves when all other modules have been loaded. This lets you produce module contents that depend on the full import graph.

```typescript theme={null}
const transpiler = new Bun.Transpiler();
const importCounts: Record<string, number> = {};

build.onLoad({ filter: /\.ts/ }, async ({ path }) => {
  const imports = transpiler.scanImports(await Bun.file(path).arrayBuffer());
  for (const i of imports) {
    importCounts[i.path] = (importCounts[i.path] || 0) + 1;
  }
  return undefined; // let Bun load the file normally
});

build.onLoad({ filter: /stats\.json/ }, async ({ defer }) => {
  await defer(); // wait for all other files to be loaded first
  return {
    contents: `export default ${JSON.stringify(importCounts)}`,
    loader: "json",
  };
});
```

<Warning>`.defer()` can only be called once per `onLoad` callback.</Warning>

## Native plugins

JavaScript plugins are single-threaded. Native plugins are NAPI modules that run on multiple threads alongside Bun's parser, offering significantly better performance.

Native plugins implement the `onBeforeParse` lifecycle hook, which is called before a file is parsed.

### Creating a native plugin in Rust

```bash theme={null}
bun add -g @napi-rs/cli
napi new
cargo add bun-native-plugin
```

```rust lib.rs theme={null}
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 source = handle.input_source_code()?;
  let output = source.replace("foo", "bar");
  handle.set_output_source_code(output, BunLoader::BUN_LOADER_JSX);
  Ok(())
}
```

```typescript index.ts theme={null}
import myNativeAddon from "./my-native-addon";

Bun.build({
  entrypoints: ["./app.tsx"],
  plugins: [
    {
      name: "replace-foo-with-bar",
      setup(build) {
        build.onBeforeParse(
          { namespace: "file", filter: "**/*.tsx" },
          { napiModule: myNativeAddon, symbol: "replace_foo_with_bar" }
        );
      },
    },
  ],
});
```

## Runtime plugins

Plugins can also be used at runtime (outside the bundler) to intercept `import` and `require` calls in Bun's module loader:

```typescript preload.ts theme={null}
import { plugin } from "bun";

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

Register it as a preload in `bunfig.toml`:

```toml bunfig.toml theme={null}
preload = ["./preload.ts"]
```

Or pass it via CLI:

```bash theme={null}
bun --preload ./preload.ts run index.ts
```

<Note>
  Runtime plugins registered with `plugin()` are active for the current Bun process only. Bundler plugins registered via `Bun.build()` run only during bundling.
</Note>

## Plugin type reference

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

type PluginBuilder = {
  config: BuildConfig;
  onStart(callback: () => void | Promise<void>): void;
  onEnd(callback: (result: BuildOutput) => 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;
      kind: ImportKind;
      defer: () => Promise<void>;
    }) => {
      loader?: Loader;
      contents?: string;
      exports?: Record<string, any>;
    } | undefined | void
  ): void;
};
```
