Bun

外掛程式

Bun 提供一個通用的外掛程式 API,可用於擴充 執行時期打包器

外掛程式攔截匯入並執行自訂載入邏輯:讀取檔案、編譯程式碼等。它們可用於新增對其他檔案類型的支援,例如 .scss.yaml。在 Bun 的打包器中,外掛程式可用於實作架構層級功能,例如 CSS 萃取、巨集和客戶端伺服器程式碼共置。

用法

外掛程式定義為包含 name 屬性和 setup 函式的簡單 JavaScript 物件。使用 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 載入器

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 檔案。

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 "./sveltePlugin.ts";
import MySvelteComponent from "./component.svelte";

console.log(MySvelteComponent.render());

虛擬模組

此功能目前僅在執行階段使用 Bun.plugin 時可用,且尚未在套件組工具中支援,但你可以使用 onResolveonLoad 模擬行為。

要在執行階段建立虛擬模組,請在 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 讀取和寫入建置設定檔

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}`);
      },
    },
  ],
});

參考

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

type PluginBuilder = {
  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" | "json" | "toml" | "object";

除了 filter 正規表示法之外,onLoad 方法還可以選擇性地接受 namespace。此命名空間將用於為轉譯程式碼中的匯入加上前綴;例如,具有 filter: /\.yaml$/namespace: "yaml:" 的載入器會將來自 ./myfile.yaml 的匯入轉換為 yaml:./myfile.yaml