外掛
用於擴充套件 Bun 執行環境和打包器的通用外掛 API
Bun 提供了一個通用外掛 API,可同時擴充套件 執行環境 和 打包器。
外掛可以攔截匯入並執行自定義載入邏輯,例如讀取檔案或轉譯程式碼。使用它們可以新增對其他檔案型別的支援,例如 .scss。在 Bun 的打包器中,外掛可以實現框架級功能,例如 CSS 提取、宏以及客戶端與伺服器端程式碼共置。
生命週期鉤子
外掛會註冊在 bundle 生命週期各個階段執行的回撥:
onStart():打包器開始打包後執行一次onResolve():打包器解析模組之前執行onLoad():打包器載入模組之前執行onBeforeParse():在打包器解析檔案之前,於解析器執行緒中執行零複製原生外掛
參考
型別概覽(完整的型別定義請參閱 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; loader: Loader; namespace: string; defer: () => Promise<void> }) => {
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;
註冊一個回撥函式,當打包器開始新的打包任務時執行。
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: "睡眠 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() 回撥(與其他生命週期回撥一樣)不能修改 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() 生命週期回撥用於自定義 Bun 解析模組的方式。
onResolve() 的第一個引數是一個包含 filter 和 namespace 屬性的物件。filter 是一個針對匯入字串執行的正規表示式。兩者共同決定自定義解析邏輯適用於哪些模組。
onResolve() 的第二個引數是一個回撥函式,對於 Bun 找到的、與第一個引數中定義的 filter 和 namespace 匹配的每個模組匯入,該回調都會執行。
回撥會接收匹配模組的 path,並可以為其傳回一個 新路徑。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 },
callback: (args: { path: string; namespace: string; loader: Loader; defer: () => Promise<void> }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
): void;
Bun 的打包器解析模組後,需要讀取並解析模組的內容。
使用 onLoad() 生命週期回撥,可以在 Bun 讀取並解析模組的 contents 之前修改它。
與 onResolve() 一樣,onLoad() 的第一個引數用於篩選此次 onLoad() 呼叫適用的模組。
onLoad() 的第二個引數是一個回撥函式,對於每個匹配的模組,該回調都會在 Bun 將其內容載入到記憶體 之前 執行。
回撥會接收匹配模組的 path、其 namespace、該檔案的預設 loader 以及一個 defer 函式。
回撥可以傳回模組的新 contents 字串和新的 loader。
例如:
import type { BunPlugin } from "bun";
const envPlugin: BunPlugin = {
name: "環境變數外掛",
setup(build) {
build.onResolve({ filter: /^env$/ }, args => {
return { path: args.path, namespace: "env" };
});
build.onLoad({ filter: /env/, 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 就會兌現。當模組內容相依其他模組時,請等待此函式傳回的 Promise。
範例:跟蹤並報告未使用的匯出
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",
};
});
},
});
每個 onLoad 回撥只能呼叫一次 .defer() 函式。
原生外掛
Bun 的打包器速度很快,部分原因在於它使用原生程式碼編寫,並利用多個執行緒並行載入和解析模組。使用 JavaScript 編寫的外掛無法利用這一點,因為 JavaScript 本身是單執行緒的。
原生外掛以 NAPI 模組的形式編寫,可以在多個執行緒上執行,因此比 JavaScript 外掛快得多。它們還可以跳過不必要的工作,例如將字串傳遞給 JavaScript 時所需的 UTF-8 -> UTF-16 轉換。
原生外掛可用的生命週期鉤子如下:
onBeforeParse():在 Bun 的打包器解析檔案之前,於任意執行緒上呼叫。
原生外掛是通過 C ABI 函式暴露生命週期鉤子的 NAPI 模組。要建立一個原生外掛,請匯出一個與要實現的生命週期鉤子簽名匹配的 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, 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 的打包器解析檔案之前立即執行。
它會接收檔案內容,並可以傳回新的原始碼。
Bun 可以從任意執行緒呼叫此回撥,因此 NAPI 模組實現必須是執行緒安全的。