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: "自定義加載器",
  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 示例",

  setup(build) {
    build.onStart(() => {
      console.log("打包開始!");
    });
  },
});

回調可以返回 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 秒)完成,以及第二個 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() 的第二個參數是回調,為 Bun 找到的每個匹配第一個參數中定義的 filter 和 namespace 的模塊導入運行。

回調接收匹配模塊的路徑作為輸入。回調可以為模塊返回新路徑。Bun 將讀取新路徑的內容並將其解析為模塊。

例如,將所有對 images/ 的導入重定向到 ./public/images/

ts
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

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() 的第二個參數是回調,在 Bun 將模塊內容加載到內存之前為每個匹配模塊運行。

此回調接收匹配模塊的路徑、模塊的導入者(導入模塊的模塊)、模塊的命名空間和模塊的類型作為輸入。

回調可以為模塊返回新的 contents 字符串和新的 loader

例如:

ts
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 回調的執行,直到所有其他模塊加載完成。

這對於返回依賴於其他模塊的模塊內容很有用。

示例:跟蹤和報告未使用的導出

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

原生插件

Bun 打包器如此快速的原因之一是它用原生代碼編寫,並利用多線程並行加載和解析模塊。

然而,用 JavaScript 編寫的插件的一個限制是 JavaScript 本身是單線程的。

原生插件編寫為 NAPI 模塊,可以在多個線程上運行。這允許原生插件比 JavaScript 插件運行得更快。

此外,原生插件可以跳過不必要的工作,如傳遞字符串給 JavaScript 所需的 UTF-8 -> UTF-16 轉換。

以下是原生插件可用的生命周期鉤子:

  • onBeforeParse():在 Bun 打包器解析文件之前在任何線程上調用。

原生插件是 NAPI 模塊,將生命周期鉤子導出為 C ABI 函數。

要創建原生插件,您必須導出與要實現的原生生命周期鉤子簽名匹配的 C ABI 函數。

用 Rust 創建原生插件

原生插件是 NAPI 模塊,將生命周期鉤子導出為 C ABI 函數。

要創建原生插件,您必須導出與要實現的原生生命周期鉤子簽名匹配的 C ABI 函數。

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

然後安裝此 crate:

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");

/// 這裡我們將實現 `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() 中使用它:

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學習網由www.bunjs.com.cn整理維護