Skip to content

Bun 提供了一個通用插件 API,可用於擴展_運行時_和_打包器_。

插件可以攔截導入並執行自定義加載邏輯:讀取文件、轉譯代碼等。它們可用於添加對其他文件類型的支持,例如 .scss.yaml。在 Bun 打包器的上下文中,插件可用於實現框架級功能,如 CSS 提取、宏以及客戶端 - 服務器代碼共存。

生命周期鉤子

插件可以注冊回調函數,在打包生命周期的各個階段運行:

參考

類型的粗略概述(請參考 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 對象。

tsx
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()(將打包時間寫入文件)完成。

請注意,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 找到的每個匹配 filternamespace 的模塊導入都會運行。

回調函數接收匹配模塊的_路徑_作為輸入。回調函數可以為模塊返回一個_新路徑_。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",
      };
    });
  },
});

請注意,.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 函數,該函數匹配你想要實現的原生生命周期鉤子的簽名。

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

然後安裝這個 crate:

bash
cargo add bun-native-plugin

現在,在 lib.rs 文件中,我們將使用 bun_native_plugin::bun proc 宏來定義一個函數,該函數將實現我們的原生插件。

以下是實現 onBeforeParse 鉤子的示例:

rs
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() 中使用它:

typescript
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 打包器解析文件之前立即運行。

作為輸入,它接收文件的內容,並可以選擇返回新的源代碼。

此回調可以從任何線程調用,因此 napi 模塊實現必須是線程安全的。

Bun學習網由www.bunjs.com.cn整理維護