Bun 提供通用的外掛程式 API,可用於擴展 runtime 和 bundler。
外掛程式攔截 import 並執行自訂載入邏輯:讀取檔案、轉譯程式碼等。它們可以用於增加對其他檔案類型的支援,例如 .scss
或 .yaml
。在 Bun 的 bundler 中,外掛程式可以用於實作框架層級的功能,例如 CSS 提取、巨集和客戶端-伺服器程式碼協同配置。
用法
外掛程式被定義為簡單的 JavaScript 物件,包含 name
屬性和 setup
函數。使用 plugin
函數向 Bun 註冊外掛程式。
import { plugin, type BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "Custom loader",
setup(build) {
// implementation
},
};
plugin(myPlugin);
外掛程式必須在任何其他程式碼執行之前載入!為了實現這一點,請在您的 bunfig.toml
中使用 preload
選項。Bun 會在執行檔案之前自動載入 preload
中指定的文件/模組。
preload = ["./myPlugin.ts"]
在 bun test
之前預先載入檔案
[test]
preload = ["./myPlugin.ts"]
第三方外掛程式
依照慣例,旨在被使用的第三方外掛程式應該導出一個工廠函數,該函數接受一些配置並返回一個外掛程式物件。
import { plugin } from "bun";
import fooPlugin from "bun-plugin-foo";
plugin(
fooPlugin({
// configuration
}),
);
Bun 的外掛程式 API 鬆散地基於 esbuild。僅實作了 一部分 的 esbuild API,但一些 esbuild 外掛程式在 Bun 中「可以直接使用」,例如官方的 MDX loader
import { plugin } from "bun";
import mdx from "@mdx-js/esbuild";
plugin(mdx());
載入器
外掛程式主要用於使用載入器擴展 Bun 以支援其他檔案類型。讓我們看一個簡單的外掛程式,它實作了 .yaml
檔案的載入器。
import { plugin } from "bun";
await plugin({
name: "YAML",
async setup(build) {
const { load } = await import("js-yaml");
// when a .yaml file is imported...
build.onLoad({ filter: /\.(yaml|yml)$/ }, async (args) => {
// read and parse the file
const text = await Bun.file(args.path).text();
const exports = load(text) as Record<string, any>;
// and returns it as a module
return {
exports,
loader: "object", // special loader for JS objects
};
});
},
});
在 preload
中註冊此檔案
preload = ["./yamlPlugin.ts"]
一旦外掛程式被註冊,.yaml
和 .yml
檔案就可以直接被 import。
import data from "./data.yml"
console.log(data);
name: Fast X
releaseYear: 2023
請注意,返回的物件具有 loader
屬性。這告訴 Bun 應該使用哪個內部載入器來處理結果。即使我們正在實作 .yaml
的載入器,結果仍然必須可以被 Bun 的內建載入器之一理解。這完全是載入器的層層堆疊。
在這種情況下,我們使用 "object"
— 一個內建的載入器(旨在供外掛程式使用),它將一個普通的 JavaScript 物件轉換為等效的 ES 模組。Bun 的任何內建載入器都受到支援;Bun 內部也使用這些相同的載入器來處理各種檔案。下表是一個快速參考;完整文件請參閱 Bundler > Loaders。
載入器 | 副檔名 | 輸出 |
---|---|---|
js | .mjs .cjs | 轉譯為 JavaScript 檔案 |
jsx | .js .jsx | 轉換 JSX 然後轉譯 |
ts | .ts .mts .cts | 轉換 TypeScript 然後轉譯 |
tsx | .tsx | 轉換 TypeScript、JSX,然後轉譯 |
toml | .toml | 使用 Bun 的內建 TOML 解析器解析 |
json | .json | 使用 Bun 的內建 JSON 解析器解析 |
napi | .node | 導入原生 Node.js 插件 |
wasm | .wasm | 導入原生 Node.js 插件 |
object | none | 一種專門為外掛程式設計的特殊載入器,它將一個普通的 JavaScript 物件轉換為等效的 ES 模組。物件中的每個鍵都對應於一個具名導出。 |
載入 YAML 檔案很有用,但是外掛程式支援的不僅僅是資料載入。讓我們看一個外掛程式,它可以讓 Bun 導入 *.svelte
檔案。
import { plugin } from "bun";
await plugin({
name: "svelte loader",
async setup(build) {
const { compile } = await import("svelte/compiler");
// when a .svelte file is imported...
build.onLoad({ filter: /\.svelte$/ }, async ({ path }) => {
// read and compile it with the Svelte compiler
const file = await Bun.file(path).text();
const contents = compile(file, {
filename: path,
generate: "ssr",
}).js.code;
// and return the compiled source code as "js"
return {
contents,
loader: "js",
};
});
},
});
注意:在生產環境的實作中,您會希望快取編譯後的輸出並包含額外的錯誤處理。
從 build.onLoad
返回的物件在 contents
中包含編譯後的原始碼,並指定 "js"
作為其載入器。這告訴 Bun 將返回的 contents
視為 JavaScript 模組,並使用 Bun 的內建 js 載入器對其進行轉譯。
有了這個外掛程式,Svelte 組件現在可以直接被 import 和使用。
import "./sveltePlugin.ts";
import MySvelteComponent from "./component.svelte";
console.log(MySvelteComponent.render());
虛擬模組
此功能目前僅在 runtime 中通過 Bun.plugin
提供,尚不支援 bundler,但是您可以使用 onResolve
和 onLoad
模擬該行為。
若要在 runtime 中建立虛擬模組,請在 Bun.plugin
的 setup
函數中使用 builder.module(specifier, callback)
。
例如
import { plugin } from "bun";
plugin({
name: "my-virtual-module",
setup(build) {
build.module(
// The specifier, which can be any string - except a built-in, such as "buffer"
"my-transpiled-virtual-module",
// The callback to run when the module is imported or required for the first time
() => {
return {
contents: "console.log('hello world!')",
loader: "js",
};
},
);
build.module("my-object-virtual-module", () => {
return {
exports: {
foo: "bar",
},
loader: "object",
};
});
},
});
// Sometime later
// All of these work
import "my-transpiled-virtual-module";
require("my-transpiled-virtual-module");
await import("my-transpiled-virtual-module");
require.resolve("my-transpiled-virtual-module");
import { foo } from "my-object-virtual-module";
const object = require("my-object-virtual-module");
await import("my-object-virtual-module");
require.resolve("my-object-virtual-module");
覆寫現有模組
您也可以使用 build.module
覆寫現有模組。
import { plugin } from "bun";
build.module("my-object-virtual-module", () => {
return {
exports: {
foo: "bar",
},
loader: "object",
};
});
require("my-object-virtual-module"); // { foo: "bar" }
await import("my-object-virtual-module"); // { foo: "bar" }
build.module("my-object-virtual-module", () => {
return {
exports: {
baz: "quix",
},
loader: "object",
};
});
require("my-object-virtual-module"); // { baz: "quix" }
await import("my-object-virtual-module"); // { baz: "quix" }
讀取或修改設定
外掛程式可以使用 build.config
讀取和寫入 build config。
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
sourcemap: "external",
plugins: [
{
name: "demo",
setup(build) {
console.log(build.config.sourcemap); // "external"
build.config.minify = true; // enable minification
// `plugins` is readonly
console.log(`Number of plugins: ${build.config.plugins.length}`);
},
},
],
});
注意:外掛程式生命週期回呼 (onStart()
、onResolve()
等) 無法在 setup()
函數中修改 build.config
物件。如果您想變更 build.config
,則必須直接在 setup()
函數中執行
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
sourcemap: "external",
plugins: [
{
name: "demo",
setup(build) {
// ✅ good! modifying it directly in the setup() function
build.config.minify = true;
build.onStart(() => {
// 🚫 uh-oh! this won't work!
build.config.minify = false;
});
},
},
],
});
生命週期鉤子
外掛程式可以註冊回呼函數,以便在 bundle 生命週期的各個階段運行。
onStart()
:在 bundler 啟動 bundle 後運行一次onResolve()
:在模組被解析之前運行onLoad()
:在模組被載入之前運行。
參考
類型的大致概述(完整類型定義請參考 Bun 的 bun.d.ts
)
namespace Bun {
function plugin(plugin: {
name: string;
setup: (build: PluginBuilder) => void;
}): void;
}
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 },
callback: (args: { path: string }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
) => void;
config: BuildConfig;
};
type Loader = "js" | "jsx" | "ts" | "tsx" | "css" | "json" | "toml" | "object";
命名空間
onLoad
和 onResolve
接受一個可選的 namespace
字串。什麼是命名空間?
每個模組都有一個命名空間。命名空間用於在轉譯後的程式碼中為 import 添加前綴;例如,filter: /\.yaml$/
和 namespace: "yaml:"
的載入器會將從 ./myfile.yaml
的 import 轉換為 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;
註冊一個回呼函數,以便在 bundler 啟動新的 bundle 時運行。
import { plugin } from "bun";
plugin({
name: "onStart example",
setup(build) {
build.onStart(() => {
console.log("Bundle started!");
});
},
});
回呼函數可以返回 Promise
。在 bundle 流程初始化後,bundler 會等待所有 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()
(將 bundle 時間寫入檔案)。
請注意,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;
為了 bundle 您的專案,Bun 會遍歷您專案中所有模組的依賴樹。對於每個 import 的模組,Bun 實際上必須找到並讀取該模組。「尋找」部分被稱為「解析」模組。
onResolve()
外掛程式生命週期回呼允許您配置如何解析模組。
onResolve()
的第一個參數是一個物件,包含 filter
和 namespace
屬性。filter
是一個正則表達式,它在 import 字串上運行。實際上,這些允許您過濾您的自訂解析邏輯將應用於哪些模組。
onResolve()
的第二個參數是一個回呼函數,對於 Bun 找到的每個與第一個參數中定義的 filter
和 namespace
相匹配的模組 import 都會運行該回呼函數。
回呼函數接收匹配模組的路徑作為輸入。回呼函數可以返回模組的新路徑。Bun 將讀取新路徑的內容並將其解析為模組。
例如,將所有對 images/
的 import 重定向到 ./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, importer: string, namespace: string, kind: ImportKind }) => {
loader?: Loader;
contents?: string;
exports?: Record<string, any>;
},
): void;
在 Bun 的 bundler 解析模組後,它需要讀取模組的內容並解析它。
onLoad()
外掛程式生命週期回呼允許您在模組被 Bun 讀取和解析之前修改模組的內容。
像 onResolve()
一樣,onLoad()
的第一個參數允許您過濾此 onLoad()
調用將應用於哪些模組。
onLoad()
的第二個參數是一個回呼函數,對於每個匹配的模組,在 Bun 將模組的內容載入到記憶體之前都會運行該回呼函數。
此回呼函數接收匹配模組的路徑、模組的 importer
(import 該模組的模組)、模組的命名空間和模組的種類作為輸入。
回呼函數可以為模組返回新的 contents
字串以及新的 loader
。
例如
import { plugin } from "bun";
plugin({
name: "env plugin",
setup(build) {
build.onLoad({ filter: /env/, namespace: "file" }, args => {
return {
contents: `export default ${JSON.stringify(process.env)}`,
loader: "js",
};
});
},
});
此外掛程式將把所有 import env from "env"
形式的 import 轉換為一個 JavaScript 模組,該模組導出當前環境變數。