Bun 提供了一个通用插件 API,可用于扩展_运行时_和_打包器_。
插件可以拦截导入并执行自定义加载逻辑:读取文件、转译代码等。它们可用于添加对其他文件类型的支持,例如 .scss 或 .yaml。在 Bun 打包器的上下文中,插件可用于实现框架级功能,如 CSS 提取、宏以及客户端 - 服务器代码共存。
生命周期钩子
插件可以注册回调函数,在打包生命周期的各个阶段运行:
onStart():打包器开始打包时运行onResolve():模块解析之前运行onLoad():模块加载之前运行onBeforeParse():在文件解析之前在解析器线程中运行零拷贝原生插件
参考
类型的粗略概述(请参考 Bun 的 bun.d.ts 获取完整类型定义):
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 对象。
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;注册一个回调函数,在打包器开始新打包时运行。
import { plugin } from "bun";
plugin({
name: "onStart 示例",
setup(build) {
build.onStart(() => {
console.log("打包开始!");
});
},
});回调函数可以返回一个 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()(将打包时间写入文件)完成。
请注意,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() 插件生命周期回调允许你配置模块如何被解析。
onResolve() 的第一个参数是一个包含 filter 和 namespace 属性的对象。filter 是一个正则表达式,在导入字符串上运行。实际上,这些允许你过滤自定义解析逻辑将应用于哪些模块。
onResolve() 的第二个参数是一个回调函数,对于 Bun 找到的每个匹配 filter 和 namespace 的模块导入都会运行。
回调函数接收匹配模块的_路径_作为输入。回调函数可以为模块返回一个_新路径_。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 },
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() 调用将应用于哪些模块。
onLoad() 的第二个参数是一个回调函数,对于每个匹配的模块,在 Bun 将模块内容加载到内存_之前_运行。
此回调函数接收匹配模块的_路径_、模块的_导入者_(导入该模块的模块)、模块的_命名空间_和模块的_类型_作为输入。
回调函数可以为模块返回一个新的 contents 字符串和一个新的 loader。
例如:
import { plugin } from "bun";
const envPlugin: BunPlugin = {
name: "env 插件",
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,当所有_其他_模块都已加载时解析。
这允许你延迟 onLoad 回调的执行,直到所有其他模块都已加载。
这对于返回依赖于其他模块的模块内容非常有用。
示例:跟踪和报告未使用的导出
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 模块,可以在多个线程上运行。这使得原生插件比 JavaScript 插件运行得更快。
此外,原生插件可以跳过不必要的工作,例如将字符串传递给 JavaScript 所需的 UTF-8 -> UTF-16 转换。
以下是原生插件可用的生命周期钩子:
onBeforeParse():在 Bun 打包器解析文件之前在任何线程上调用
原生插件是 NAPI 模块,将生命周期钩子导出为 C ABI 函数。
要创建原生插件,你必须导出一个 C ABI 函数,该函数匹配你想要实现的原生生命周期钩子的签名。
使用 Rust 创建原生插件
原生插件是 NAPI 模块,将生命周期钩子导出为 C ABI 函数。
要创建原生插件,你必须导出一个 C ABI 函数,该函数匹配你想要实现的原生生命周期钩子的签名。
bun add -g @napi-rs/cli
napi new然后安装这个 crate:
cargo add bun-native-plugin现在,在 lib.rs 文件中,我们将使用 bun_native_plugin::bun proc 宏来定义一个函数,该函数将实现我们的原生插件。
以下是实现 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 打包器解析文件之前立即运行。
作为输入,它接收文件的内容,并可以选择返回新的源代码。
此回调可以从任何线程调用,因此 napi 模块实现必须是线程安全的。