插件
用于扩展 Bun 运行时和打包器的通用插件 API
Bun 提供了一个通用插件 API,可同时扩展 运行时 和 打包器。
插件可以拦截导入并执行自定义加载逻辑,例如读取文件或转译代码。使用它们可以添加对其他文件类型的支持,例如 .scss。在 Bun 的打包器中,插件可以实现框架级功能,例如 CSS 提取、宏以及客户端与服务器端代码共置。
生命周期钩子
插件会注册在 bundle 生命周期各个阶段运行的回调:
onStart():打包器开始打包后运行一次onResolve():打包器解析模块之前运行onLoad():打包器加载模块之前运行onBeforeParse():在打包器解析文件之前,于解析器线程中运行零拷贝原生插件
参考
类型概览(完整的类型定义请参阅 Bun 的 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; 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 对象。
import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "自定义加载器",
setup(build) {
// 实现
},
};
调用 Bun.build 时,将插件传入 plugins 数组。
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
onStart(callback: () => void | Promise<void>): void;
注册一个回调函数,当打包器开始新的打包任务时运行。
await Bun.build({
entrypoints: ["./app.ts"],
plugins: [
{
name: "onStart example",
setup(build) {
build.onStart(() => {
console.log("Bundle started!");
});
},
},
],
});
回调函数可以返回一个 Promise。打包流程初始化后,打包器会等待所有 onStart() 回调完成后才继续。
例如:
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
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/:
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
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。
例如:
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。
示例:跟踪并报告未使用的导出
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 创建原生插件
bun add -g @napi-rs/cli
napi new
然后安装此 crate:
cargo add bun-native-plugin
在 lib.rs 中,使用 bun_native_plugin::bun 过程宏来定义一个实现原生插件的函数。
下面是实现 onBeforeParse 钩子的示例:
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() 中使用:
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
onBeforeParse(
args: { filter: RegExp; namespace?: string },
callback: { napiModule: NapiModule; symbol: string; external?: unknown },
): void;
此生命周期回调会在 Bun 的打包器解析文件之前立即运行。
它会接收文件内容,并可以返回新的源代码。
Bun 可以从任意线程调用此回调,因此 NAPI 模块实现必须是线程安全的。