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 秒間スリープ)が完了するのを待ち、さらに 2 番目の 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() の 2 番目の引数は、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() の 2 番目の引数は、Bun がモジュールの内容をメモリにロードする 前に 一致する各モジュールに対して実行されるコールバックです。
このコールバックは、一致するモジュールへの パス、モジュールの インポーター(モジュールをインポートしたモジュール)、モジュールの ネームスペース、およびモジュールの kind を入力として受け取ります。
コールバックはモジュールの新しい 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 コールバックに渡される引数の 1 つは 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 コールバックごとに 1 回しか呼び出せないという制限があることに注意してください。
ネイティブプラグイン
Bun のバンドラーが非常に高速である理由の 1 つは、ネイティブコードで記述されており、マルチスレッディングを活用してモジュールを並列にロードおよび解析することです。
ただし、JavaScript で記述されたプラグインの制限の 1 つは、JavaScript 自体がシングルスレッドであることです。
ネイティブプラグインは NAPI モジュールとして記述され、複数のスレッドで実行できます。これにより、ネイティブプラグインは JavaScript プラグインよりもはるかに高速に実行できます。
さらに、ネイティブプラグインは、JavaScript に文字列を渡すために必要な UTF-8 -> UTF-16 変換など、不要な作業をスキップできます。
これらは、ネイティブプラグインで利用可能なライフサイクルフックです:
onBeforeParse(): Bun のバンドラーによってファイルが解析される直前に任意のスレッドで呼び出されます。
ネイティブプラグインは、ライフサイクルフックを C ABI 関数として公開する NAPI モジュールです。
ネイティブプラグインを作成するには、実装したいネイティブライフサイクルフックの署名に一致する C ABI 関数をエクスポートする必要があります。
Rust でのネイティブプラグインの作成
ネイティブプラグインは、ライフサイクルフックを C ABI 関数として公開する NAPI モジュールです。
ネイティブプラグインを作成するには、実装したいネイティブライフサイクルフックの署名に一致する C ABI 関数をエクスポートする必要があります。
bun add -g @napi-rs/cli
napi new次に、このクレートをインストールします:
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");
/// ここで、`foo` のすべての出現を `bar` に置き換えるコードで
/// `onBeforeParse` を実装します。
///
/// #[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 モジュールの実装はスレッドセーフである必要があります。