Bun 提供了一个通用的插件 API,可以用来扩展 *runtime* 和 *bundler*。
插件会拦截导入并执行自定义加载逻辑:读取文件、转译代码等。它们可用于添加对其他文件类型的支持,例如 .scss
或 .yaml
。在 Bun 的 bundler 上下文中,插件可用于实现框架级别的功能,例如 CSS 提取、宏和客户端-服务器代码的共置。
生命周期钩子
插件可以注册回调,以便在 bundle 的生命周期的各个点执行。
onStart()
:bundler 启动 bundle 后运行。onResolve()
:在模块解析之前运行。onLoad()
:在模块加载之前运行。onEnd()
:bundle 完成后运行。onBeforeParse()
:在文件被解析之前,在解析器线程中运行零拷贝的本地插件。
Reference
类型的大致概览(请参阅 Bun 的 bun.d.ts
以获取完整的类型定义)
type PluginBuilder = {
onStart(callback: () => void): void;
onEnd(callback: (result: BuildOutput) => 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 },
defer: () => Promise<void>,
callback: (args: { path: string }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
) => void;
config: BuildConfig;
};
type Loader = "js" | "jsx" | "ts" | "tsx" | "css" | "json" | "toml";
用法
插件定义为一个简单的 JavaScript 对象,包含一个 name
属性和一个 setup
函数。
import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "Custom loader",
setup(build) {
// implementation
},
};
此插件可以在调用 Bun.build
时传递到 plugins
数组中。
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./out",
plugins: [myPlugin],
});
插件生命周期
命名空间
onLoad
和 onResolve
接受一个可选的 namespace
字符串。什么是命名空间?
每个模块都有一个命名空间。命名空间用于在转译的代码中为导入添加前缀;例如,一个带有 filter: /\.yaml$/
和 namespace: "yaml:"
的 loader 会将从 ./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;
注册一个回调,在 bundler 启动新 bundle 时运行。
import { plugin } from "bun";
plugin({
name: "onStart example",
setup(build) {
build.onStart(() => {
console.log("Bundle started!");
});
},
});
回调可以返回一个 Promise
。在 bundle 进程初始化后,bundler 会等待所有 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 Bunlog.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 秒)完成,*以及* 第二个 onStart()
(将 bundle 时间写入文件)完成。
请注意,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;
要 bundle 您的项目,Bun 会遍历您项目中所有模块的依赖关系树。对于每个导入的模块,Bun 实际上需要找到并读取该模块。 "寻找" 部分称为 "解析" 模块。
onResolve()
插件生命周期回调允许您配置模块的解析方式。
onResolve()
的第一个参数是一个对象,其中包含 filter
和 namespace
属性。filter 是一个正则表达式,它会在导入字符串上运行。有效地,这些允许您过滤哪些模块将应用您的自定义解析逻辑。
onResolve()
的第二个参数是一个回调,它会为 Bun 找到的与第一个参数中定义的 filter
和 namespace
匹配的每个模块导入运行。
回调接收匹配模块的*路径*作为输入。回调可以返回模块的*新路径*。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 },
defer: () => Promise<void>,
callback: (args: { path: string, importer: string, namespace: string, kind: ImportKind }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
): void;
在 Bun 的 bundler 解析了模块后,它需要读取模块的内容并解析它。
onLoad()
插件生命周期回调允许您在 Bun 读取和解析模块内容之前修改模块的*内容*。
与 onResolve()
类似,onLoad()
的第一个参数允许您过滤此 onLoad()
调用将应用的模块。
onLoad()
的第二个参数是一个回调,它会在 Bun 将模块内容加载到内存*之前*为每个匹配的模块运行。
此回调接收匹配模块的*路径*、模块的*导入者*(导入该模块的模块)、模块的*命名空间*以及模块的*类型*作为输入。
回调可以返回模块的新 contents
字符串以及新的 loader
。
例如
import { plugin } from "bun";
const envPlugin: BunPlugin = {
name: "env plugin",
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
,当所有*其他*模块都已加载时,该 Promise 将解析。
这允许您将 onLoad
回调的执行延迟到所有其他模块加载完成之后。
这对于返回依赖于其他模块的模块内容很有用。
示例:跟踪和报告未使用的导出
import { plugin } from "bun";
plugin({
name: "track imports",
setup(build) {
const transpiler = new Bun.Transpiler();
let trackedImports: Record<string, number> = {};
// Each module that goes through this onLoad callback
// will record its imports in `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 }) => {
// Wait for all files to be loaded, ensuring
// that every file goes through the above `onLoad()` function
// and their imports tracked
await defer();
// Emit JSON containing the stats of each import
return {
contents: `export default ${JSON.stringify(trackedImports)}`,
loader: "json",
};
});
},
});
请注意,.defer()
函数目前有一个限制,即每个 onLoad
回调只能调用一次。
onEnd
onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;
注册一个回调,在 bundler 完成一个 bundle(无论成功与否)时运行。
回调接收 BuildOutput
对象,其中包含
success
:一个布尔值,指示构建是否成功outputs
:生成的构建工件数组logs
:构建消息数组(警告、错误等)
这对于后处理、清理、通知或自定义错误处理很有用。
await Bun.build({
entrypoints: ["./index.ts"],
outdir: "./out",
plugins: [
{
name: "onEnd example",
setup(build) {
build.onEnd(result => {
if (result.success) {
console.log(
`✅ Build succeeded with ${result.outputs.length} outputs`,
);
} else {
console.error(`❌ Build failed with ${result.logs.length} errors`);
}
});
},
},
],
});
onEnd
回调在以下时机被调用:
- 在 build promise 解析或拒绝**之前**
- 在所有 bundling 完成**之后**
- 按照它们被注册的顺序**
多个插件可以注册 onEnd
回调,它们都将按顺序调用。如果 onEnd
回调返回一个 promise,build 将等待它解析后再继续。
Native 插件
Bun 的 bundler 如此之快的原因之一是它使用原生代码编写,并利用多线程并行加载和解析模块。
然而,用 JavaScript 编写的插件的一个限制是 JavaScript 本身是单线程的。
Native 插件是用 NAPI 模块编写的,可以在多个线程上运行。这使得 Native 插件的运行速度比 JavaScript 插件快得多。
此外,Native 插件可以跳过不必要的工作,例如将字符串传递给 JavaScript 所需的 UTF-8 -> UTF-16 转换。
以下是 Native 插件可用的生命周期钩子:
onBeforeParse()
:在 Bun bundler 解析文件之前,在任何线程上调用。
Native 插件是 NAPI 模块,它们将生命周期钩子公开为 C ABI 函数。
要创建 Native 插件,必须导出一个 C ABI 函数,该函数与您要实现的 Native 生命周期钩子的签名匹配。
在 Rust 中创建 Native 插件
Native 插件是 NAPI 模块,它们将生命周期钩子公开为 C ABI 函数。
要创建 Native 插件,必须导出一个 C ABI 函数,该函数与您要实现的 Native 生命周期钩子的签名匹配。
bun add -g @napi-rs/cli
napi new
然后安装此 crate
cargo add bun-native-plugin
现在,在 lib.rs
文件中,我们将使用 bun_native_plugin::bun
proc 宏来定义一个函数,该函数 将实现我们的 Native 插件。
这是一个实现 onBeforeParse
钩子的示例
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader};
use napi_derive::napi;
/// Define the plugin and its name
define_bun_plugin!("replace-foo-with-bar");
/// Here we'll implement `onBeforeParse` with code that replaces all occurrences of
/// `foo` with `bar`.
///
/// We use the #[bun] macro to generate some of the boilerplate code.
///
/// The argument of the function (`handle: &mut OnBeforeParse`) tells
/// the macro that this function implements the `onBeforeParse` hook.
#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
// Fetch the input source code.
let input_source_code = handle.input_source_code()?;
// Get the Loader for the file
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 bundler 解析文件之前立即运行。
作为输入,它接收文件的内容,并可选择返回新的源代码。
此回调可以从任何线程调用,因此 napi 模块实现必须是线程安全的。