Bun

外掛程式

Bun 提供通用的外掛程式 API,可用於擴展 runtime 和 bundler

外掛程式攔截 import 並執行自訂載入邏輯:讀取檔案、轉譯程式碼等。它們可以用於增加對其他檔案類型的支援,例如 .scss.yaml。在 Bun 的 bundler 中,外掛程式可以用於實作框架層級的功能,例如 CSS 提取、巨集和客戶端-伺服器程式碼協同配置。

用法

外掛程式被定義為簡單的 JavaScript 物件,包含 name 屬性和 setup 函數。使用 plugin 函數向 Bun 註冊外掛程式。

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

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

plugin(myPlugin);

外掛程式必須在任何其他程式碼執行之前載入!為了實現這一點,請在您的 bunfig.toml 中使用 preload 選項。Bun 會在執行檔案之前自動載入 preload 中指定的文件/模組。

preload = ["./myPlugin.ts"]

bun test 之前預先載入檔案

[test]
preload = ["./myPlugin.ts"]

第三方外掛程式

依照慣例,旨在被使用的第三方外掛程式應該導出一個工廠函數,該函數接受一些配置並返回一個外掛程式物件。

import { plugin } from "bun";
import fooPlugin from "bun-plugin-foo";

plugin(
  fooPlugin({
    // configuration
  }),
);

Bun 的外掛程式 API 鬆散地基於 esbuild。僅實作了 一部分 的 esbuild API,但一些 esbuild 外掛程式在 Bun 中「可以直接使用」,例如官方的 MDX loader

import { plugin } from "bun";
import mdx from "@mdx-js/esbuild";

plugin(mdx());

載入器

外掛程式主要用於使用載入器擴展 Bun 以支援其他檔案類型。讓我們看一個簡單的外掛程式,它實作了 .yaml 檔案的載入器。

yamlPlugin.ts
import { plugin } from "bun";

await plugin({
  name: "YAML",
  async setup(build) {
    const { load } = await import("js-yaml");

    // when a .yaml file is imported...
    build.onLoad({ filter: /\.(yaml|yml)$/ }, async (args) => {

      // read and parse the file
      const text = await Bun.file(args.path).text();
      const exports = load(text) as Record<string, any>;

      // and returns it as a module
      return {
        exports,
        loader: "object", // special loader for JS objects
      };
    });
  },
});

preload 中註冊此檔案

bunfig.toml
preload = ["./yamlPlugin.ts"]

一旦外掛程式被註冊,.yaml.yml 檔案就可以直接被 import。

index.ts
data.yml
index.ts
import data from "./data.yml"

console.log(data);
data.yml
name: Fast X
releaseYear: 2023

請注意,返回的物件具有 loader 屬性。這告訴 Bun 應該使用哪個內部載入器來處理結果。即使我們正在實作 .yaml 的載入器,結果仍然必須可以被 Bun 的內建載入器之一理解。這完全是載入器的層層堆疊。

在這種情況下,我們使用 "object" — 一個內建的載入器(旨在供外掛程式使用),它將一個普通的 JavaScript 物件轉換為等效的 ES 模組。Bun 的任何內建載入器都受到支援;Bun 內部也使用這些相同的載入器來處理各種檔案。下表是一個快速參考;完整文件請參閱 Bundler > Loaders

載入器副檔名輸出
js.mjs .cjs轉譯為 JavaScript 檔案
jsx.js .jsx轉換 JSX 然後轉譯
ts.ts .mts .cts轉換 TypeScript 然後轉譯
tsx.tsx轉換 TypeScript、JSX,然後轉譯
toml.toml使用 Bun 的內建 TOML 解析器解析
json.json使用 Bun 的內建 JSON 解析器解析
napi.node導入原生 Node.js 插件
wasm.wasm導入原生 Node.js 插件
objectnone一種專門為外掛程式設計的特殊載入器,它將一個普通的 JavaScript 物件轉換為等效的 ES 模組。物件中的每個鍵都對應於一個具名導出。

載入 YAML 檔案很有用,但是外掛程式支援的不僅僅是資料載入。讓我們看一個外掛程式,它可以讓 Bun 導入 *.svelte 檔案。

sveltePlugin.ts
import { plugin } from "bun";

await plugin({
  name: "svelte loader",
  async setup(build) {
    const { compile } = await import("svelte/compiler");

    // when a .svelte file is imported...
    build.onLoad({ filter: /\.svelte$/ }, async ({ path }) => {

      // read and compile it with the Svelte compiler
      const file = await Bun.file(path).text();
      const contents = compile(file, {
        filename: path,
        generate: "ssr",
      }).js.code;

      // and return the compiled source code as "js"
      return {
        contents,
        loader: "js",
      };
    });
  },
});

注意:在生產環境的實作中,您會希望快取編譯後的輸出並包含額外的錯誤處理。

build.onLoad 返回的物件在 contents 中包含編譯後的原始碼,並指定 "js" 作為其載入器。這告訴 Bun 將返回的 contents 視為 JavaScript 模組,並使用 Bun 的內建 js 載入器對其進行轉譯。

有了這個外掛程式,Svelte 組件現在可以直接被 import 和使用。

import "./sveltePlugin.ts";
import MySvelteComponent from "./component.svelte";

console.log(MySvelteComponent.render());

虛擬模組

此功能目前僅在 runtime 中通過 Bun.plugin 提供,尚不支援 bundler,但是您可以使用 onResolveonLoad 模擬該行為。

若要在 runtime 中建立虛擬模組,請在 Bun.pluginsetup 函數中使用 builder.module(specifier, callback)

例如

import { plugin } from "bun";

plugin({
  name: "my-virtual-module",

  setup(build) {
    build.module(
      // The specifier, which can be any string - except a built-in, such as "buffer"
      "my-transpiled-virtual-module",
      // The callback to run when the module is imported or required for the first time
      () => {
        return {
          contents: "console.log('hello world!')",
          loader: "js",
        };
      },
    );

    build.module("my-object-virtual-module", () => {
      return {
        exports: {
          foo: "bar",
        },
        loader: "object",
      };
    });
  },
});

// Sometime later
// All of these work
import "my-transpiled-virtual-module";
require("my-transpiled-virtual-module");
await import("my-transpiled-virtual-module");
require.resolve("my-transpiled-virtual-module");

import { foo } from "my-object-virtual-module";
const object = require("my-object-virtual-module");
await import("my-object-virtual-module");
require.resolve("my-object-virtual-module");

覆寫現有模組

您也可以使用 build.module 覆寫現有模組。

import { plugin } from "bun";
build.module("my-object-virtual-module", () => {
  return {
    exports: {
      foo: "bar",
    },
    loader: "object",
  };
});

require("my-object-virtual-module"); // { foo: "bar" }
await import("my-object-virtual-module"); // { foo: "bar" }

build.module("my-object-virtual-module", () => {
  return {
    exports: {
      baz: "quix",
    },
    loader: "object",
  };
});
require("my-object-virtual-module"); // { baz: "quix" }
await import("my-object-virtual-module"); // { baz: "quix" }

讀取或修改設定

外掛程式可以使用 build.config 讀取和寫入 build config。

await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "demo",
      setup(build) {
        console.log(build.config.sourcemap); // "external"

        build.config.minify = true; // enable minification

        // `plugins` is readonly
        console.log(`Number of plugins: ${build.config.plugins.length}`);
      },
    },
  ],
});

注意:外掛程式生命週期回呼 (onStart()onResolve() 等) 無法在 setup() 函數中修改 build.config 物件。如果您想變更 build.config,則必須直接在 setup() 函數中執行

await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "demo",
      setup(build) {
        // ✅ good! modifying it directly in the setup() function
        build.config.minify = true;

        build.onStart(() => {
          // 🚫 uh-oh! this won't work!
          build.config.minify = false;
        });
      },
    },
  ],
});

生命週期鉤子

外掛程式可以註冊回呼函數,以便在 bundle 生命週期的各個階段運行。

  • onStart():在 bundler 啟動 bundle 後運行一次
  • onResolve():在模組被解析之前運行
  • onLoad():在模組被載入之前運行。

參考

類型的大致概述(完整類型定義請參考 Bun 的 bun.d.ts

namespace Bun {
  function plugin(plugin: {
    name: string;
    setup: (build: PluginBuilder) => void;
  }): void;
}

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 },
    callback: (args: { path: string }) => {
      loader?: Loader;
      contents?: string;
      exports?: Record<string, any>;
    },
  ) => void;
  config: BuildConfig;
};

type Loader = "js" | "jsx" | "ts" | "tsx" | "css" | "json" | "toml" | "object";

命名空間

onLoadonResolve 接受一個可選的 namespace 字串。什麼是命名空間?

每個模組都有一個命名空間。命名空間用於在轉譯後的程式碼中為 import 添加前綴;例如,filter: /\.yaml$/namespace: "yaml:" 的載入器會將從 ./myfile.yaml 的 import 轉換為 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;

註冊一個回呼函數,以便在 bundler 啟動新的 bundle 時運行。

import { plugin } from "bun";

plugin({
  name: "onStart example",

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

回呼函數可以返回 Promise。在 bundle 流程初始化後,bundler 會等待所有 onStart() 回呼函數完成後再繼續。

例如

const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "Sleep for 10 seconds",
      setup(build) {
        build.onStart(async () => {
          await Bunlog.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 秒)完成,以及第二個 onStart()(將 bundle 時間寫入檔案)。

請注意,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;

為了 bundle 您的專案,Bun 會遍歷您專案中所有模組的依賴樹。對於每個 import 的模組,Bun 實際上必須找到並讀取該模組。「尋找」部分被稱為「解析」模組。

onResolve() 外掛程式生命週期回呼允許您配置如何解析模組。

onResolve() 的第一個參數是一個物件,包含 filternamespace 屬性。filter 是一個正則表達式,它在 import 字串上運行。實際上,這些允許您過濾您的自訂解析邏輯將應用於哪些模組。

onResolve() 的第二個參數是一個回呼函數,對於 Bun 找到的每個與第一個參數中定義的 filternamespace 相匹配的模組 import 都會運行該回呼函數。

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

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

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

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

在 Bun 的 bundler 解析模組後,它需要讀取模組的內容並解析它。

onLoad() 外掛程式生命週期回呼允許您在模組被 Bun 讀取和解析之前修改模組的內容。

onResolve() 一樣,onLoad() 的第一個參數允許您過濾此 onLoad() 調用將應用於哪些模組。

onLoad() 的第二個參數是一個回呼函數,對於每個匹配的模組,在 Bun 將模組的內容載入到記憶體之前都會運行該回呼函數。

此回呼函數接收匹配模組的路徑、模組的 importer(import 該模組的模組)、模組的命名空間和模組的種類作為輸入。

回呼函數可以為模組返回新的 contents 字串以及新的 loader

例如

import { plugin } from "bun";

plugin({
  name: "env plugin",
  setup(build) {
    build.onLoad({ filter: /env/, namespace: "file" }, args => {
      return {
        contents: `export default ${JSON.stringify(process.env)}`,
        loader: "js",
      };
    });
  },
});

此外掛程式將把所有 import env from "env" 形式的 import 轉換為一個 JavaScript 模組,該模組導出當前環境變數。