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" | "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:"
的載入器會將從 ./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 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 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()
(將捆綁時間寫入檔案)。
請注意,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
屬性。篩選器是一個正則表達式,在導入字串上執行。實際上,這些讓您可以篩選您的自訂解析邏輯將應用於哪些模組。
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()
外掛程式生命週期回呼 (callback) 允許您在 Bun 讀取和解析模組內容之前修改它。
就像 onResolve()
一樣,onLoad()
的第一個參數允許您篩選此 onLoad()
調用將應用於哪些模組。
onLoad()
的第二個參數是一個回呼 (callback) 函式,它會針對每個匹配的模組在 Bun 將模組內容載入到記憶體之前執行。
此回呼 (callback) 接收的輸入包括匹配模組的路徑、模組的匯入器(匯入該模組的模組)、模組的命名空間和模組的種類。
此回呼 (callback) 可以為模組返回一個新的 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
回呼 (callback) 的參數之一是 defer
函式。此函式返回一個 Promise
,該 Promise 會在所有其他模組都載入完成時解析 (resolved)。
這允許您延遲 onLoad
回呼 (callback) 的執行,直到所有其他模組都載入完成為止。
這對於返回依賴於其他模組的模組內容非常有用。
範例:追蹤和報告未使用的匯出
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
回呼 (callback) 只能調用一次。
原生外掛程式
Bun 的打包器 (bundler) 如此快速的原因之一是它以原生程式碼編寫,並利用多執行緒平行載入和解析模組。
然而,以 JavaScript 編寫的外掛程式的一個限制是 JavaScript 本身是單執行緒的。
原生外掛程式以 NAPI 模組的形式編寫,並且可以在多個執行緒上執行。這使得原生外掛程式比 JavaScript 外掛程式執行速度快得多。
此外,原生外掛程式可以跳過不必要的工作,例如將字串傳遞給 JavaScript 所需的 UTF-8 -> UTF-16 轉換。
以下是原生外掛程式可用的生命週期鉤子 (lifecycle hooks):
onBeforeParse()
:在 Bun 的打包器 (bundler) 解析檔案之前,在任何執行緒上調用。
原生外掛程式是 NAPI 模組,它們將生命週期鉤子 (lifecycle hooks) 作為 C ABI 函式公開。
若要建立原生外掛程式,您必須匯出一個 C ABI 函式,該函式與您要實作的原生生命週期鉤子 (lifecycle hook) 的簽名 (signature) 相符。
在 Rust 中建立原生外掛程式
原生外掛程式是 NAPI 模組,它們將生命週期鉤子 (lifecycle hooks) 作為 C ABI 函式公開。
若要建立原生外掛程式,您必須匯出一個 C ABI 函式,該函式與您要實作的原生生命週期鉤子 (lifecycle hook) 的簽名 (signature) 相符。
bun add -g @napi-rs/cli
napi new
然後安裝這個 crate
cargo add bun-native-plugin
現在,在 lib.rs
檔案中,我們將使用 bun_native_plugin::bun
程序巨集 (proc macro) 來定義一個函式,該函式 將實作我們的原生外掛程式。
這是一個實作 onBeforeParse
鉤子 (hook) 的範例
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;
此生命週期回呼 (lifecycle callback) 在 Bun 的打包器 (bundler) 解析檔案之前立即執行。
作為輸入,它接收檔案的內容,並且可以選擇性地返回新的原始碼。
此回呼 (callback) 可以從任何執行緒調用,因此 napi 模組實作必須是執行緒安全的。