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()(將打包時間寫入文件)完成。
NOTE
`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",
};
});
},
});原生插件
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 打包器解析文件之前立即運行。
作為輸入,它接收文件的內容,並可以選擇返回新的源代碼。