> ## 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.

# Plugins

> Extend Bun's runtime and bundler with custom loaders, module resolution, and native hooks.

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.

```typescript my-plugin.ts theme={null}
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`.

<Steps>
  <Step title="Define the plugin">
    ```typescript my-plugin.ts theme={null}
    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",
          };
        });
      },
    });
    ```
  </Step>

  <Step title="Register as a preload in bunfig.toml">
    ```toml bunfig.toml theme={null}
    preload = ["./my-plugin.ts"]
    ```
  </Step>

  <Step title="Import the custom file type">
    ```typescript index.ts theme={null}
    import message from "./greeting.txt";
    console.log(message); // contents of greeting.txt as a string
    ```
  </Step>
</Steps>

## Using a plugin in the bundler

Pass the plugin to `Bun.build` via the `plugins` array.

```typescript build.ts theme={null}
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.

```typescript theme={null}
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**

```typescript theme={null}
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**

```typescript theme={null}
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.

```typescript theme={null}
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.

```typescript theme={null}
build.onResolve(
  args: { filter: RegExp; namespace?: string },
  callback: (args: { path: string; importer: string }) => {
    path: string;
    namespace?: string;
  } | void
): void;
```

**Example: redirect image imports**

```typescript theme={null}
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.

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

plugin({
  name: "start-logger",
  setup(build) {
    build.onStart(async () => {
      const now = Date.now();
      await Bun.$`echo ${now} > build-time.txt`;
    });
  },
});
```

<Note>
  `onStart` callbacks cannot modify `build.config`. To mutate the build config, do so directly inside `setup()` before registering any hooks.
</Note>

## Available loaders

The `loader` field in an `onLoad` return value tells Bun how to parse the returned `contents`.

| Loader  | Description                               |
| ------- | ----------------------------------------- |
| `js`    | JavaScript                                |
| `jsx`   | JavaScript with JSX                       |
| `ts`    | TypeScript                                |
| `tsx`   | TypeScript with JSX                       |
| `json`  | JSON — exported as a default object       |
| `jsonc` | JSON with comments                        |
| `toml`  | TOML — exported as a default object       |
| `yaml`  | YAML — exported as a default object       |
| `text`  | Plain text — exported as a default string |
| `file`  | Binary file — exported as a file path     |
| `css`   | CSS stylesheet                            |
| `html`  | HTML document                             |
| `wasm`  | WebAssembly module                        |
| `napi`  | Native 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:

```typescript theme={null}
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.

```typescript theme={null}
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:

```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 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

```typescript theme={null}
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";
```
