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

# 插件

> 用于扩展 Bun 运行时和打包器的通用插件 API

Bun 提供了一个通用的插件 API，可用于扩展 *运行时* 和 *打包器*。

插件拦截导入并执行自定义加载逻辑：读取文件、转译代码等。它们可用于添加对额外文件类型的支持，比如 `.scss` 或 `.yaml`。在 Bun 的打包器上下文中，插件还可以用来实现框架级功能，如 CSS 提取、宏和客户端-服务器代码共置。

## 生命周期钩子

插件可以注册回调函数，在打包流程的各个阶段运行：

* [`onStart()`](#onstart)：打包器开始打包时运行
* [`onResolve()`](#onresolve)：模块解析前运行
* [`onLoad()`](#onload)：模块加载前运行
* [`onBeforeParse()`](#onbeforeparse)：在文件解析前，在解析线程中零拷贝运行原生插件。

### 参考

类型的粗略概览（完整类型定义请参见 Bun 的 `bun.d.ts`）：

```ts Plugin Types icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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;
  config: BuildConfig;
};

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

## 使用方法

一个插件被定义为一个包含 `name` 属性和 `setup` 函数的 JavaScript 对象。

```tsx myPlugin.ts icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import type { BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "自定义加载器",
  setup(build) {
    // 实现
  },
};
```

此插件可以传入 `Bun.build` 的 `plugins` 数组中。

```ts index.ts icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./out",
  plugins: [myPlugin],
});
```

## 插件生命周期

### 命名空间

`onLoad` 和 `onResolve` 接受一个可选的 `namespace` 字符串。什么是命名空间？

每个模块都有一个命名空间。命名空间用于在转译后的代码中为导入前缀，例如，有一个 `filter: /\.yaml$/` 和 `namespace: "yaml:"` 的加载器，会把 `./myfile.yaml` 的导入转换为 `yaml:./myfile.yaml`。

默认命名空间是 `"file"`，通常不必指定。例如：`import myModule from "./my-module.ts"` 等同于 `import myModule from "file:./my-module.ts"`。

其他常见命名空间包括：

* `"bun"`：Bun 特定模块（如 `"bun:test"`、`"bun:sqlite"`）
* `"node"`：Node.js 模块（如 `"node:fs"`、`"node:path"`）

### `onStart`

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
onStart(callback: () => void): Promise<void> | void;
```

注册回调函数，在打包器启动新打包时运行。

```ts index.ts icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import { plugin } from "bun";

plugin({
  name: "onStart 示例",

  setup(build) {
    build.onStart(() => {
      console.log("打包开始！");
    });
  },
});
```

回调函数可以返回一个 `Promise`。打包流程初始化后，打包器会等待所有 `onStart()` 回调完成后才继续。

例如：

```ts index.ts icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "睡眠 10 秒",
      setup(build) {
        build.onStart(async () => {
          await Bunlog.sleep(10_000);
        });
      },
    },
    {
      name: "记录打包时间到文件",
      setup(build) {
        build.onStart(async () => {
          const now = Date.now();
          await Bun.$`echo ${now} > bundle-time.txt`;
        });
      },
    },
  ],
});
```

在上述示例中，Bun 会等待第一个 `onStart()`（睡眠 10 秒）和第二个 `onStart()`（将打包时间写入文件）都完成后再继续。

注意，`onStart()` 回调（以及其他生命周期回调）不能修改 `build.config` 对象。如果需要修改配置，必须直接在 `setup()` 函数中操作。

### `onResolve`

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
onResolve(
  args: { filter: RegExp; namespace?: string },
  callback: (args: { path: string; importer: string }) => {
    path: string;
    namespace?: string;
  } | void,
): void;
```

为了打包项目，Bun 会遍历项目中所有模块的依赖树。对每个导入的模块，Bun 必须找到并读取该模块。查找模块的这部分称为“解析”模块。

`onResolve()` 插件生命周期回调允许你配置模块的解析方式。

`onResolve()` 的第一个参数是包含 `filter` 和 [`namespace`](#命名空间) 的对象。`filter` 是一个正则表达式，用于匹配导入字符串。实际上，这允许你筛选出自定义解析逻辑适用的模块。

`onResolve()` 的第二个参数是一个回调，在每个匹配 `filter` 和 `namespace` 的模块导入时执行。

回调接收匹配模块的 *路径* 作为输入，可以返回模块的新路径。Bun 会读取此新路径的内容并解析为模块。

例如，把所有以 `images/` 开头的导入重定向到 `./public/images/`：

```ts index.ts icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import { plugin } from "bun";

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

### `onLoad`

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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;
```

在 Bun 的打包器解析完模块后，需要读取其内容并解析。

`onLoad()` 插件生命周期回调允许你在 Bun 读取和解析模块之前修改模块的 *内容*。

与 `onResolve()` 类似，`onLoad()` 的第一个参数用于过滤哪些模块将触发此回调。

`onLoad()` 的第二个参数是每个匹配模块加载前调用的回调。

该回调接收匹配模块的 *路径*、导入该模块的 *importer*、模块的 *namespace* 以及模块的 *kind*。

回调可以返回模块的新 `contents` 字符串和新的 `loader`。

例如：

```ts index.ts icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import { plugin } from "bun";

const envPlugin: BunPlugin = {
  name: "环境变量插件",
  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"
```

此插件将把所有 `import env from "env"` 类型的导入转成导出当前环境变量的 JavaScript 模块。

#### `.defer()`

传给 `onLoad` 回调的参数之一是 `defer` 函数。该函数返回一个 `Promise`，当所有其他模块加载完成后会被 resolve。

这使得你可以延迟 `onLoad` 回调的执行，直到加载完成其他所有模块。

这对于返回依赖其他模块的模块内容非常有用。

##### 示例：跟踪并报告未使用的导出

```ts index.ts icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import { plugin } from "bun";

plugin({
  name: "跟踪导入",
  setup(build) {
    const transpiler = new Bun.Transpiler();

    let trackedImports: Record<string, number> = {};

    // 每个经过此 onLoad 回调的模块
    // 都会记录其导入信息到 `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 }) => {
      // 等待所有文件加载完成，确保每个文件都经过上面的 `onLoad()`
      // 并且导入信息已被跟踪
      await defer();

      // 输出包含导入统计信息的 JSON
      return {
        contents: `export default ${JSON.stringify(trackedImports)}`,
        loader: "json",
      };
    });
  },
});
```

注意 `.defer()` 函数当前限制是每个 `onLoad` 回调中只能调用一次。

## 原生插件

Bun 打包器速度快的原因之一是它使用原生代码实现，并利用多线程并行加载和解析模块。

但是，用 JavaScript 编写的插件有个限制：JavaScript 本身是单线程的。

原生插件是以 [NAPI](/runtime/node-api) 模块的形式编写，可以在多个线程上运行。这使得原生插件运行速度远快于 JavaScript 插件。

此外，原生插件可以跳过将字符串传递给 JavaScript 时必须进行的 UTF-8 -> UTF-16 转换等不必要的工作。

原生插件可用的生命周期钩子包括：

* [`onBeforeParse()`](#onbeforeparse)：在 Bun 的打包器解析文件前于任何线程调用。

原生插件是 NAPI 模块，暴露生命周期钩子为 C ABI 函数。

创建原生插件时，必须导出匹配所实现原生生命周期钩子签名的 C ABI 函数。

### 用 Rust 创建原生插件

原生插件是 NAPI 模块，暴露生命周期钩子为 C ABI 函数。

创建原生插件时，必须导出匹配所实现原生生命周期钩子签名的 C ABI 函数。

```bash terminal icon="terminal" theme={"theme":{"light":"github-light","dark":"dracula"}}
bun add -g @napi-rs/cli
napi new
```

然后安装此 crate：

```bash terminal icon="terminal" theme={"theme":{"light":"github-light","dark":"dracula"}}
cargo add bun-native-plugin
```

现在，在 `lib.rs` 文件中，我们将使用 `bun_native_plugin::bun` 过程宏定义一个函数来实现原生插件。

下面是实现 `onBeforeParse` 钩子的示例：

```rs lib.rs icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/rust.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=7ac541ea996ac441aff4b3857ad7431d" theme={"theme":{"light":"github-light","dark":"dracula"}}
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader};
use napi_derive::napi;

/// 定义插件及其名称
define_bun_plugin!("replace-foo-with-bar");

/// 这里我们实现 `onBeforeParse`，代码将所有出现的
/// `foo` 替换成 `bar`。
///
/// 使用 #[bun] 宏生成部分样板代码。
///
/// 函数参数 (`handle: &mut OnBeforeParse`) 告诉
/// 宏此函数实现了 `onBeforeParse` 钩子。
#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
  // 获取输入源代码。
  let input_source_code = handle.input_source_code()?;

  // 获取文件的 Loader
  let loader = handle.output_loader();

  let output_source_code = input_source_code.replace("foo", "bar");

  handle.set_output_source_code(output_source_code, BunLoader::BUN_LOADER_JSX);

  Ok(())
}
```

使用示例，在 `Bun.build()` 中：

```typescript theme={"theme":{"light":"github-light","dark":"dracula"}}
import myNativeAddon from "./my-native-addon";
Bun.build({
  entrypoints: ["./app.tsx"],
  plugins: [
    {
      name: "my-plugin",

      setup(build) {
        build.onBeforeParse(
          {
            namespace: "file",
            filter: "**/*.tsx",
          },
          {
            napiModule: myNativeAddon,
            symbol: "replace_foo_with_bar",
            // external: myNativeAddon.getSharedState()
          },
        );
      },
    },
  ],
});
```

### `onBeforeParse`

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
onBeforeParse(
  args: { filter: RegExp; namespace?: string },
  callback: { napiModule: NapiModule; symbol: string; external?: unknown },
): void;
```

此生命周期回调在 Bun 打包器解析文件之前立即运行。

作为输入，它接收文件内容，并可返回新的源代码。

此回调可由任意线程调用，因此 napi 模块实现必须是线程安全的。
