外掛
用於擴充套件 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 的打包器解析檔案前立即執行。
它接收檔案內容,並可以選擇傳回新的原始碼。