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

插件

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

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

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

生命周期钩子

插件注册在打包生命周期的不同阶段运行的回调:

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

参考

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

bun.d.ts
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; 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 对象。

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

const myPlugin: BunPlugin = {
  name: "Custom loader",
  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: "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`;
        });
      },
    },
  ],
});

在此示例中,Bun 会等待两个 onStart() 回调完成:10 秒的休眠以及向 bundle-time.txt 写入内容。

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 都会运行该回调。

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

例如,将所有导入 images/ 的模块重定向到 ./public/images/:

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

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

与 onResolve() 类似,onLoad() 的第一个参数用于选择此次 onLoad() 调用所应用的模块。

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

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

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

例如:

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

const envPlugin: BunPlugin = {
  name: "env plugin",
  setup(build) {
    build.onResolve({ filter: /^env$/ }, () => ({ path: "env", namespace: "env" }));
    build.onLoad({ filter: /.*/, 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 完成。

示例:跟踪和报告未使用导出
index.ts
import { plugin } from "bun";

plugin({
  name: "track imports",
  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",
      };
    });
  },
});

onEnd

TypeScript
onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;

注册一个在打包完成后运行的回调。该回调会接收 BuildOutput 对象,其中包含打包结果,包括输出文件和所有打包消息。

index.ts
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  plugins: [
    {
      name: "onEnd example",
      setup(build) {
        build.onEnd(result => {
          console.log(`Build completed with ${result.outputs.length} files`);
          for (const log of result.logs) {
            console.log(log);
          }
        });
      },
    },
  ],
});

回调可以返回一个 Promise。在所有 onEnd() 回调完成之前,Bun.build() 返回的 Promise 不会完成。

index.ts
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  plugins: [
    {
      name: "Upload to S3",
      setup(build) {
        build.onEnd(async result => {
          if (!result.success) return;
          for (const output of result.outputs) {
            await uploadToS3(output);
          }
        });
      },
    },
  ],
});

原生插件

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

原生插件是 NAPI 模块,可将生命周期钩子作为 C ABI 函数公开。它们可以在多个线程上运行,因此速度远快于 JavaScript 插件。它们还可以跳过一些工作,例如将字符串传递给 JavaScript 所需的 UTF-8 到 UTF-16 转换。

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

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

要创建原生插件,请导出一个与要实现的原生生命周期钩子签名匹配的 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};
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, loader);

  Ok(())
}

在 Bun.build() 中使用它:

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

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

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