執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

外掛

用於擴充套件 Bun 執行環境和打包器的通用外掛 API

Bun 的通用外掛 API 同時擴充套件了執行環境和打包器。

外掛會攔截匯入並執行自定義載入邏輯,例如讀取檔案或轉譯程式碼。它們可以新增對其他檔案型別的支援,例如 .scss。在打包器中,外掛可以實現框架級功能,例如 CSS 提取、宏以及客戶端和伺服器端程式碼共置。

生命週期鉤子

外掛註冊在打包生命週期的不同階段執行的回撥:

  • onStart():打包器開始打包時執行一次
  • onResolve():打包器解析模組之前執行
  • onLoad():打包器載入模組之前執行
  • onBeforeParse():打包器解析檔案之前,在解析器執行緒中執行零複製原生外掛
  • onEnd():打包完成後執行

參考

型別的概覽(完整的型別定義請參閱 Bun 的 bun.d.ts):

bun.d.ts
type PluginBuilder = {
  onStart(callback: () => void | Promise<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; defer: () => Promise<void> }) => {
      loader?: Loader;
      contents?: string;
      exports?: Record<string, any>;
    },
  ) => void;
  onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;
  config: BuildConfig;
};

type Loader =
  | "js"
  | "jsx"
  | "ts"
  | "tsx"
  | "json"
  | "jsonc"
  | "toml"
  | "yaml"
  | "file"
  | "napi"
  | "wasm"
  | "text"
  | "css"
  | "html";

用法

外掛是一個具有 name 屬性和 setup 函式的 JavaScript 物件。

myPlugin.ts
import type { BunPlugin } from "bun";

const myPlugin: BunPlugin = {
  name: "Custom loader",
  setup(build) {
    // 實現邏輯
  },
};

在呼叫 Bun.build 時,將其傳入 plugins 陣列。

index.ts
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

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

註冊一個在打包器開始新一輪打包時執行的回撥。

index.ts
await Bun.build({
  entrypoints: ["./app.ts"],
  plugins: [
    {
      name: "onStart example",
      setup(build) {
        build.onStart(() => {
          console.log("Bundle started!");
        });
      },
    },
  ],
});

回撥可以傳回 Promise。打包流程初始化後,打包器會等待所有 onStart() 回撥完成才繼續。

例如:

index.ts
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "Sleep for 10 seconds",
      setup(build) {
        build.onStart(async () => {
          await Bun.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 秒的休眠以及向 bundle-time.txt 寫入內容。

onResolve

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

為了打包專案,Bun 會遍歷專案中所有模組的相依樹。對於每個匯入的模組,Bun 都必須找到並讀取該模組。“查詢”部分稱為模組“解析”。

onResolve() 外掛生命週期回撥用於設定 Bun 如何解析模組。

onResolve() 的第一個引數是一個包含 filter 和 namespace 屬性的物件。filter 是一個用於匹配匯入字串的正規表示式。兩者結合起來,用於選擇應用自定義解析邏輯的模組。

onResolve() 的第二個引數是一個回撥。對於找到的、符合第一個引數中定義的篩選條件和名稱空間的每個模組匯入,Bun 都會執行該回調。

回撥會接收匹配模組的路徑,並可以為該模組傳回一個新路徑。Bun 會讀取新路徑的內容,並將其解析為模組。

例如,將所有匯入 images/ 的模組重定向到 ./public/images/:

index.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

TypeScript
onLoad(
  args: { filter: RegExp; namespace?: string },
  callback: (args: { path: string; namespace: string; loader: Loader; defer: () => Promise<void> }) => {
    loader?: Loader;
    contents?: string;
    exports?: Record<string, any>;
  },
): void;

Bun 的打包器解析模組後,會讀取並解析模組的內容。

onLoad() 外掛生命週期回撥會在 Bun 讀取並解析模組之前修改模組的內容。

與 onResolve() 類似,onLoad() 的第一個引數用於選擇此次 onLoad() 呼叫所應用的模組。

onLoad() 的第二個引數是一個回撥,在 Bun 將匹配的模組內容載入到記憶體之前,該回調會針對每個匹配的模組執行一次。

回撥會接收匹配模組的路徑、其名稱空間、預設載入器以及一個 defer 函式。

回撥可以傳回該模組新的 contents 字串和新的 loader。

例如:

index.ts
import type { BunPlugin } from "bun";

const envPlugin: BunPlugin = {
  name: "env plugin",
  setup(build) {
    build.onResolve({ filter: /^env$/ }, () => ({ path: "env", namespace: "env" }));
    build.onLoad({ filter: /.*/, namespace: "env" }, 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,在 Bun 載入完所有其他模組後完成。當模組內容相依於其他模組時,請等待該 Promise 完成。

範例:跟蹤和報告未使用匯出
index.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",
      };
    });
  },
});

onEnd

TypeScript
onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;

註冊一個在打包完成後執行的回撥。該回調會接收 BuildOutput 物件,其中包含打包結果,包括輸出檔案和所有打包訊息。

index.ts
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  plugins: [
    {
      name: "onEnd example",
      setup(build) {
        build.onEnd(result => {
          console.log(`Build completed with ${result.outputs.length} files`);
          for (const log of result.logs) {
            console.log(log);
          }
        });
      },
    },
  ],
});

回撥可以傳回一個 Promise。在所有 onEnd() 回撥完成之前,Bun.build() 傳回的 Promise 不會完成。

index.ts
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  plugins: [
    {
      name: "Upload to S3",
      setup(build) {
        build.onEnd(async result => {
          if (!result.success) return;
          for (const output of result.outputs) {
            await uploadToS3(output);
          }
        });
      },
    },
  ],
});

原生外掛

Bun 的打包器使用原生程式碼編寫,並使用多個執行緒並行載入和解析模組。JavaScript 外掛執行在單個執行緒上,因為 JavaScript 本身是單執行緒的。

原生外掛是 NAPI 模組,可將生命週期鉤子作為 C ABI 函式公開。它們可以在多個執行緒上執行,因此速度遠快於 JavaScript 外掛。它們還可以跳過一些工作,例如將字串傳遞給 JavaScript 所需的 UTF-8 到 UTF-16 轉換。

原生外掛可以使用以下生命週期鉤子:

  • onBeforeParse():在 Bun 的打包器解析檔案之前,可在任何執行緒上呼叫

要建立原生外掛,請匯出一個與要實現的原生生命週期鉤子簽名匹配的 C ABI 函式。

用 Rust 建立原生外掛

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

然後安裝該 crate:

terminal
cargo add bun-native-plugin

在 lib.rs 中,使用 bun_native_plugin::bun 過程巨集定義實現原生外掛的函式。

下面是一個實現 onBeforeParse 鉤子的範例:

lib.rs
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow};
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, loader);

  Ok(())
}

在 Bun.build() 中使用它:

index.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

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

onBeforeParse() 回撥會在 Bun 的打包器解析檔案前立即執行。

它接收檔案內容,並可以選擇傳回新的原始碼。