Skip to content

Bun は、ランタイムとバンドラーの両方を拡張するために使用できるユニバーサルプラグイン API を提供します。

プラグインはインポートをインターセプトし、ファイルの読み取り、コードのトランスパイルなどのカスタム読み込みロジックを実行します。.scss.yaml などの追加ファイルタイプのサポートを追加するために使用できます。Bun のバンドラーのコンテキストでは、プラグインを使用して CSS 抽出、マクロ、クライアント - サーバーコードの共存などのフレームワークレベルの機能を実装できます。

ライフサイクルフック

プラグインは、バンドルのライフサイクルのさまざまな時点で実行されるコールバックを登録できます:

  • onStart():バンダラーがバンドルを開始したら実行
  • onResolve():モジュールが解決される前に実行
  • onLoad():モジュールが読み込まれる前に実行
  • onBeforeParse():ファイルが解析される前にパーサースレッドでゼロコピーネイティブアドオンを実行

リファレンス

型の概要(完全な型定義については Bun の bun.d.ts を参照してください):

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 オブジェクトとして定義されます。

ts
import type { BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "Custom loader",
  setup(build) {
    // 実装
  },
};

このプラグインは、Bun.build を呼び出す際に plugins 配列に渡すことができます。

ts
await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./out",
  plugins: [myPlugin],
});

プラグインのライフサイクル

名前空間

onLoadonResolve はオプションの 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

ts
onStart(callback: () => void): Promise<void> | void;

バンダラーが新しいバンドルを開始するときに実行されるコールバックを登録します。

ts
import { plugin } from "bun";

plugin({
  name: "onStart example",

  setup(build) {
    build.onStart(() => {
      console.log("Bundle started!");
    });
  },
});

コールバックは Promise を返すことができます。バンドルプロセスが初期化された後、バンダラーはすべての onStart() コールバックが完了するまで待機してから続行します。

例えば:

ts
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()(バンドル時間をファイルに書き込み)が完了するまで待機します。

NOTE

`onStart()` コールバック(他のすべてのライフサイクルコールバックと同様)は `build.config` オブジェクトを変更する機能を持ちません。 `build.config` を変更したい場合は、`setup()` 関数内で直接変更する必要があります。

onResolve

ts
onResolve(
  args: { filter: RegExp; namespace?: string },
  callback: (args: { path: string; importer: string }) => {
    path: string;
    namespace?: string;
  } | void,
): void;

プロジェクトをバンドルするために、Bun はプロジェクト内のすべてのモジュールの依存関係ツリーをたどります。インポートされた各モジュールについて、Bun は実際にそのモジュールを見つけて読み取る必要があります。この「見つける」部分は「モジュールの解決」として知られています。

onResolve() プラグインライフサイクルコールバックを使用すると、モジュールがどのように解決されるかを設定できます。

onResolve() の最初の引数は filternamespace プロパティを持つオブジェクトです。filter はインポート文字列に対して実行される正規表現です。これにより、カスタム解決ロジックを適用するモジュールをフィルタリングできます。

onResolve() の 2 番目の引数は、最初の引数で定義されたフィルターと名前空間に一致する各モジュールインポートに対して実行されるコールバックです。

コールバックは入力として一致するモジュールへのパスを受け取ります。コールバックはモジュールの新しいパスを返すことができます。Bun は新しいパスの内容を読み取り、モジュールとして解析します。

例えば、images/ へのすべてのインポートを ./public/images/ にリダイレクトします:

ts
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

ts
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 がモジュールの内容をメモリに読み込む前に、一致する各モジュールに対して実行されるコールバックです。

このコールバックは、一致するモジュールへのパス、モジュールのインポーター(モジュールをインポートしたモジュール)、モジュールの名前空間、モジュールの種類を入力として受け取ります。

コールバックはモジュールの新しい contents 文字列と新しい loader を返すことができます。

例えば:

ts
import { plugin } from "bun";

const envPlugin: BunPlugin = {
  name: "env plugin",
  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 コールバックの実行を遅延できます。

これは、他のモジュールに依存するモジュールの内容を返す場合に役立ちます。

例:未使用のエクスポートの追跡とレポート

ts
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",
      };
    });
  },
});

ネイティブプラグイン

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 関数をエクスポートする必要があります。

bash
bun add -g @napi-rs/cli
napi new

次に、このクレートをインストールします:

bash
cargo add bun-native-plugin

次に、lib.rs ファイル内で、bun_native_plugin::bun proc マクロを使用して、ネイティブプラグインを実装する関数を定義します。

onBeforeParse フックを実装する例を次に示します:

rust
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() で使用するには:

ts
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

ts
onBeforeParse(
  args: { filter: RegExp; namespace?: string },
  callback: { napiModule: NapiModule; symbol: string; external?: unknown },
): void;

このライフサイクルコールバックは、Bun のバンダラーによってファイルが解析される直前に実行されます。

入力として、ファイルの内容を受け取り、オプションで新しいソースコードを返すことができます。

Bun by www.bunjs.com.cn 編集