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

# Plugins

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

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

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

## 生命周期钩子

插件注册在打包生命周期各个阶段运行的回调：

* `onStart()`：打包器开始打包时运行一次
* `onResolve()`：在模块被解析前运行
* `onLoad()`：在模块被加载前运行
* `onBeforeParse()`：在解析器线程中文件被解析前运行零拷贝原生插件
* `onEnd()`：在打包完成后运行

## 参考

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

```ts title="bun.d.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" 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 },
    callback: (args: { path: string; defer: () => Promise<void> }) => {
      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";
```

## 用法

插件是一个包含 `name` 属性和 `setup` 函数的 JavaScript 对象。

```ts title="myPlugin.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import type { BunPlugin } from "bun";

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

在调用 `Bun.build` 时将其传入 `plugins` 数组。

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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={null}
onStart(callback: () => void): Promise<void> | void;
```

注册一个在打包器开始新打包时运行的回调。

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { plugin } from "bun";

plugin({
  name: "onStart 示例",

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

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

例如：

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "休眠 10 秒",
      setup(build) {
        build.onStart(async () => {
          await Bun.sleep(10_000);
        });
      },
    },
    {
      name: "将打包时间记录到文件",
      setup(build) {
        build.onStart(async () => {
          const now = Date.now();
          await Bun.$`echo ${now} > bundle-time.txt`;
        });
      },
    },
  ],
});
```

在这个例子中，Bun 等待两个 `onStart()` 回调都完成：10 秒休眠和写入 `bundle-time.txt`。

<Note>
  `onStart()` 回调（像每个其他生命周期回调一样）不能修改 `build.config` 对象。要修改 `build.config`，直接在 `setup()` 函数中进行。
</Note>

### onResolve

```ts theme={null}
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()` 的第二个参数是一个回调，为每个匹配第一个参数中定义的过滤器和命名空间的模块导入运行。

回调接收匹配模块的路径，并可以返回模块的新路径。Bun 读取新路径的内容并将其解析为模块。

例如，将所有对 `images/` 的导入重定向到 `./public/images/`：

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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={null}
onLoad(
  args: { filter: RegExp; namespace?: string },
  callback: (args: { path: string; namespace: string; loader: Loader; defer: () => Promise<void> }) => {
    loader?: Loader;
    contents?: string;
    exports?: Record<string, any>;
  },
): void;
```

在 Bun 的打包器解析完一个模块后，它会读取并解析模块的内容。

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

与 `onResolve()` 类似，`onLoad()` 的第一个参数选择此 `onLoad()` 调用适用的模块。

`onLoad()` 的第二个参数是一个回调，在每个匹配的模块被 Bun 加载到内存之前运行。

回调接收匹配模块的路径、其命名空间、其默认加载器以及一个 `defer` 函数。

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

例如：

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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，在所有其他模块加载完成后解析。当模块的内容依赖于其他模块时，等待它。

<Accordion title="示例：跟踪和报告未使用的导出">
  ```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
  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",
        };
      });
    },
  });
  ```
</Accordion>

<Warning>每个 `onLoad` 回调只能调用一次 `.defer()` 函数。</Warning>

## 原生插件

Bun 的打包器使用原生代码编写，并使用多线程并行加载和解析模块。JavaScript 插件在单线程上运行，因为 JavaScript 本身是单线程的。

原生插件是以 C ABI 函数形式公开生命周期钩子的 NAPI 模块。它们可以在多线程上运行，因此比 JavaScript 插件快得多，并且跳过了诸如将字符串传递给 JavaScript 所需的 UTF-8 -> UTF-16 转换等工作。

以下生命周期钩子可供原生插件使用：

* `onBeforeParse()`：在任何线程上，在 Bun 打包器解析文件之前调用。

要创建原生插件，导出一个与你想要实现的原生生命周期钩子签名匹配的 C ABI 函数。

### 在 Rust 中创建原生插件

```bash terminal icon="terminal" theme={null}
bun add -g @napi-rs/cli
napi new
```

然后安装这个 crate：

```bash terminal icon="terminal" theme={null}
cargo add bun-native-plugin
```

在 `lib.rs` 中，使用 `bun_native_plugin::bun` 过程宏来定义实现原生插件的函数。

以下是一个实现 `onBeforeParse` 钩子的示例：

```rust title="lib.rs" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/rust.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=a7de74a66b4a1ca0110cecee9337f3dd" theme={null}
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()?;

  // 获取文件的加载器
  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()` 中使用它：

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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={null}
onBeforeParse(
  args: { filter: RegExp; namespace?: string },
  callback: { napiModule: NapiModule; symbol: string; external?: unknown },
): void;
```

`onBeforeParse()` 回调在 Bun 的打包器解析文件之前立即运行。

它接收文件的内容，并可以选择返回新的源代码。

<Info>Bun 可以从任何线程调用此回调，因此 NAPI 模块实现必须是线程安全的。</Info>

### onEnd

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

注册一个在打包完成后运行的回调。回调接收包含构建结果（包括输出文件和任何构建消息）的 [`BuildOutput`](/docs/bundler#outputs) 对象。

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  plugins: [
    {
      name: "onEnd 示例",
      setup(build) {
        build.onEnd(result => {
          console.log(`构建完成，共 ${result.outputs.length} 个文件`);
          for (const log of result.logs) {
            console.log(log);
          }
        });
      },
    },
  ],
});
```

回调可以返回一个 `Promise`。`Bun.build()` 返回的 Promise 在所有 `onEnd()` 回调完成后才会 resolve。

```ts title="index.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  plugins: [
    {
      name: "上传到 S3",
      setup(build) {
        build.onEnd(async result => {
          if (!result.success) return;
          for (const output of result.outputs) {
            await uploadToS3(output);
          }
        });
      },
    },
  ],
});
```
