插件
用于扩展 Bun 运行时和打包器的通用插件 API
Bun 的通用插件 API 同时扩展了运行时和打包器。
插件会拦截导入并执行自定义加载逻辑,例如读取文件或转译代码。它们可以添加对其他文件类型的支持,例如 .scss。在打包器中,插件可以实现框架级功能,例如 CSS 提取、宏以及客户端和服务器端代码共置。
生命周期钩子
插件注册在打包生命周期的不同阶段运行的回调:
onStart():打包器开始打包时运行一次onResolve():打包器解析模块之前运行onLoad():打包器加载模块之前运行onBeforeParse():打包器解析文件之前,在解析器线程中运行零拷贝原生插件onEnd():打包完成后运行
参考
类型的概览(完整的类型定义请参阅 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; 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 对象。
import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "Custom loader",
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: "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
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/:
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
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。
例如:
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 完成。
示例:跟踪和报告未使用导出
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
onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;
注册一个在打包完成后运行的回调。该回调会接收 BuildOutput 对象,其中包含打包结果,包括输出文件和所有打包消息。
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 不会完成。
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 创建原生插件
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};
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() 中使用它:
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;
onBeforeParse() 回调会在 Bun 的打包器解析文件前立即运行。
它接收文件内容,并可以选择返回新的源代码。