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

# Plugins

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

## Lifecycle hooks

Plugins can register callbacks that execute at various stages of the bundling lifecycle:

* `onStart()`: Runs once when the bundler starts a build
* `onResolve()`: Runs before a module is resolved
* `onLoad()`: Runs before a module is loaded
* `onBeforeParse()`: Runs zero-copy native plugins in the parser thread, before a file is parsed
* `onEnd()`: Runs after bundling completes

## Reference

A rough outline of the types (refer to Bun's `bun.d.ts` for complete type definitions):

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

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

## Usage

Plugins are defined as simple JavaScript objects containing a `name` property and a `setup` function.

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

const myPlugin: BunPlugin = {
  name: "Custom loader",
  setup(build) {
    // implementation
  },
};
```

This plugin can be passed to `Bun.build` in the `plugins` array.

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

## Plugin lifecycle

### Namespaces

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

* `"bun"`: Bun-specific modules (e.g. `"bun:test"`, `"bun:sqlite"`)
* `"node"`: Node.js modules (e.g. `"node:fs"`, `"node:path"`)

### onStart

```ts theme={null}
onStart(callback: () => void): Promise<void> | void;
```

Register a callback to be executed when the bundler starts a new build.

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

plugin({
  name: "onStart example",

  setup(build) {
    build.onStart(() => {
      console.log("Bundle started!");
    });
  },
});
```

The callback can return a Promise. After the bundler initializes, it will wait for all `onStart()` callbacks to complete before proceeding.

For example:

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

<Note>
  `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.
</Note>

### onResolve

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

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

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

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

### onLoad

```ts theme={null}
onLoad(
  args: { filter: RegExp; namespace?: string },
  defer: () => Promise<void>,
  callback: (args: { path: string, importer: string, namespace: string, kind: ImportKind }) => {
    loader?: Loader;
    contents?: string;
    exports?: Record<string, any>;
  },
): void;
```

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:

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

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

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

// import env from "env"
// env.FOO === "bar"
```

This plugin will convert all imports of the form `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 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.

<Accordion title="Example: Track and report unused exports">
  ```ts theme={null}
  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",
        };
      });
    },
  });
  ```
</Accordion>

### onEnd

```ts theme={null}
onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;
```

Register a callback to be executed after bundling completes.

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

plugin({
  name: "onEnd example",
  setup(build) {
    build.onEnd((result) => {
      console.log(`Build ${result.success ? "succeeded" : "failed"}`);
      console.log(`${result.outputs.length} files generated`);
    });
  },
});
```

The callback receives the build result, including outputs and logs.

## Examples

### YAML loader

Here's a simple plugin that adds support for `.yaml` files:

```ts theme={null}
import type { BunPlugin } from "bun";
import { parse } from "yaml";

const yamlPlugin: BunPlugin = {
  name: "YAML loader",
  setup(build) {
    build.onLoad({ filter: /\.ya?ml$/ }, async (args) => {
      const text = await Bun.file(args.path).text();
      const data = parse(text);
      
      return {
        contents: `export default ${JSON.stringify(data)}`,
        loader: "json",
      };
    });
  },
};

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

### Inline SVG as React components

```ts 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: `
          import React from 'react';
          export default function SVG(props) {
            return (
              <svg {...props}>${svg}</svg>
            );
          }
        `,
        loader: "tsx",
      };
    });
  },
};
```

### Environment variable injection

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

const envPlugin: BunPlugin = {
  name: "Environment variables",
  setup(build) {
    build.onResolve({ filter: /^env$/ }, () => {
      return { path: "env", namespace: "env" };
    });

    build.onLoad({ filter: /.*/, namespace: "env" }, () => {
      return {
        contents: `export default ${JSON.stringify(process.env)}`,
        loader: "json",
      };
    });
  },
};

// Usage:
// import env from "env";
// console.log(env.NODE_ENV);
```

### Build-time code generation

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

const buildInfoPlugin: BunPlugin = {
  name: "Build info",
  setup(build) {
    build.onResolve({ filter: /^build-info$/ }, () => {
      return { path: "build-info", namespace: "build-info" };
    });

    build.onLoad({ filter: /.*/, namespace: "build-info" }, () => {
      return {
        contents: `
          export const buildTime = ${Date.now()};
          export const buildId = "${crypto.randomUUID()}";
          export const version = "${process.env.npm_package_version || "unknown"}";
        `,
        loader: "js",
      };
    });
  },
};

// Usage:
// import { buildTime, buildId, version } from "build-info";
// console.log(`Built at ${new Date(buildTime)}`);
```

## Plugin ordering

Plugins are executed in the order they are defined in the `plugins` array:

```ts theme={null}
await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./out",
  plugins: [
    firstPlugin,  // executes first
    secondPlugin, // executes second
    thirdPlugin,  // executes third
  ],
});
```

If multiple plugins register callbacks for the same lifecycle hook and filter:

* For `onResolve`: The first plugin that returns a value wins
* For `onLoad`: The first plugin that returns a value wins
* For `onStart` and `onEnd`: All callbacks execute

## Best practices

### Use specific filters

Use precise regex patterns to avoid unnecessary callback invocations:

```ts theme={null}
// Good: specific pattern
build.onLoad({ filter: /\.scss$/ }, callback);

// Bad: too broad
build.onLoad({ filter: /.*/ }, callback);
```

### Cache expensive operations

Cache results when possible to avoid redundant work:

```ts theme={null}
const cache = new Map();

build.onLoad({ filter: /\.md$/ }, async (args) => {
  if (cache.has(args.path)) {
    return cache.get(args.path);
  }
  
  const result = await expensiveTransform(args.path);
  cache.set(args.path, result);
  return result;
});
```

### Return early

Return `undefined` or `void` from callbacks when no transformation is needed:

```ts theme={null}
build.onLoad({ filter: /.*/ }, (args) => {
  if (!shouldTransform(args.path)) {
    return; // Let other plugins or default loader handle it
  }
  
  return { contents: transform(args.path), loader: "js" };
});
```

### Handle errors gracefully

Catch and log errors instead of letting them crash the build:

```ts theme={null}
build.onLoad({ filter: /\.custom$/ }, async (args) => {
  try {
    return await transform(args.path);
  } catch (error) {
    console.error(`Failed to transform ${args.path}:`, error);
    throw error; // Re-throw to fail the build
  }
});
```
