运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

插件

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

Bun 提供了一个通用插件 API,可同时扩展 运行时 和 打包器。

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

生命周期钩子

插件会注册在 bundle 生命周期各个阶段运行的回调:

  • onStart():打包器开始打包后运行一次
  • onResolve():打包器解析模块之前运行
  • onLoad():打包器加载模块之前运行
  • onBeforeParse():在打包器解析文件之前,于解析器线程中运行零拷贝原生插件

参考

类型概览(完整的类型定义请参阅 Bun 的 bun.d.ts):

Plugin
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; loader: Loader; namespace: string; defer: () => Promise<void> }) => {
      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 对象。

myPlugin.ts
import type { BunPlugin } from "bun";

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

调用 Bun.build 时,将插件传入 plugins 数组。

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

TypeScript
onStart(callback: () => void | Promise<void>): void;

注册一个回调函数,当打包器开始新的打包任务时运行。

index.ts
await Bun.build({
  entrypoints: ["./app.ts"],
  plugins: [
    {
      name: "onStart example",
      setup(build) {
        build.onStart(() => {
          console.log("Bundle started!");
        });
      },
    },
  ],
});

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

例如:

index.ts
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 秒,第二个回调将打包时间写入文件。

onStart() 回调(与其他生命周期回调一样)不能修改 build.config 对象。要修改 build.config,请直接在 setup() 函数中进行。

onResolve

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

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

onResolve() 生命周期回调用于自定义 Bun 解析模块的方式。

onResolve() 的第一个参数是一个包含 filter 和 namespace 属性的对象。filter 是一个针对导入字符串运行的正则表达式。两者共同决定自定义解析逻辑适用于哪些模块。

onResolve() 的第二个参数是一个回调函数,对于 Bun 找到的、与第一个参数中定义的 filter 和 namespace 匹配的每个模块导入,该回调都会运行。

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

例如,把所有以 images/ 开头的导入重定向到 ./public/images/:

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

TypeScript
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 读取并解析模块的 contents 之前修改它。

与 onResolve() 一样,onLoad() 的第一个参数用于筛选此次 onLoad() 调用适用的模块。

onLoad() 的第二个参数是一个回调函数,对于每个匹配的模块,该回调都会在 Bun 将其内容加载到内存 之前 运行。

回调会接收匹配模块的 path、其 namespace、该文件的默认 loader 以及一个 defer 函数。

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

例如:

index.ts
import type { BunPlugin } from "bun";

const envPlugin: BunPlugin = {
  name: "环境变量插件",
  setup(build) {
    build.onResolve({ filter: /^env$/ }, args => {
      return { path: args.path, namespace: "env" };
    });
    build.onLoad({ filter: /env/, namespace: "env" }, 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,当 Bun 加载完所有_其他_模块后,该 Promise 就会兑现。当模块内容依赖其他模块时,请等待此函数返回的 Promise。

示例:跟踪并报告未使用的导出
index.ts
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",
      };
    });
  },
});

每个 onLoad 回调只能调用一次 .defer() 函数。

原生插件

Bun 的打包器速度很快,部分原因在于它使用原生代码编写,并利用多个线程并行加载和解析模块。使用 JavaScript 编写的插件无法利用这一点,因为 JavaScript 本身是单线程的。

原生插件以 NAPI 模块的形式编写,可以在多个线程上运行,因此比 JavaScript 插件快得多。它们还可以跳过不必要的工作,例如将字符串传递给 JavaScript 时所需的 UTF-8 -> UTF-16 转换。

原生插件可用的生命周期钩子如下:

  • onBeforeParse():在 Bun 的打包器解析文件之前,于任意线程上调用。

原生插件是通过 C ABI 函数暴露生命周期钩子的 NAPI 模块。要创建一个原生插件,请导出一个与要实现的生命周期钩子签名匹配的 C ABI 函数。

用 Rust 创建原生插件

terminal
bun add -g @napi-rs/cli
napi new

然后安装此 crate:

terminal
cargo add bun-native-plugin

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

下面是实现 onBeforeParse 钩子的示例:

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

TypeScript
onBeforeParse(
  args: { filter: RegExp; namespace?: string },
  callback: { napiModule: NapiModule; symbol: string; external?: unknown },
): void;

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

它会接收文件内容,并可以返回新的源代码。

Bun 可以从任意线程调用此回调,因此 NAPI 模块实现必须是线程安全的。