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()(將打包時間寫入文件)完成。
請注意,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",
};
});
},
});請注意,.defer() 函數目前有一個限制,即每個 onLoad 回調只能調用一次。
原生插件
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 打包器解析文件之前立即運行。
作為輸入,它接收文件的內容,並可以選擇返回新的源代碼。
此回調可以從任何線程調用,因此 napi 模塊實現必須是線程安全的。