Skip to content

Bun bietet eine universelle Plugin-API, die zur Erweiterung sowohl der Runtime als auch des Bundlers verwendet werden kann.

Plugins fangen Importe ab und führen benutzerdefinierte Lade-Logik aus: Dateien lesen, Code transpilieren, etc. Sie können verwendet werden, um Unterstützung für zusätzliche Dateitypen wie .scss oder .yaml hinzuzufügen. Im Kontext von Bun's Bundler können Plugins verwendet werden, um Framework-Level-Funktionen wie CSS-Extraktion, Makros und Client-Server-Code-Co-Location zu implementieren.

Lifecycle-Hooks

Plugins können Callbacks registrieren, die an verschiedenen Punkten im Lifecycle eines Bundles ausgeführt werden:

  • onStart(): Wird ausgeführt, sobald der Bundler ein Bundle gestartet hat
  • onResolve(): Wird ausgeführt, bevor ein Modul aufgelöst wird
  • onLoad(): Wird ausgeführt, bevor ein Modul geladen wird
  • onBeforeParse(): Führt Zero-Copy-Native-Addons im Parser-Thread aus, bevor eine Datei geparst wird

Referenz

Ein grober Überblick über die Typen (bitte beziehen Sie sich auf Bun's bun.d.ts für die vollständigen Typdefinitionen):

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

Verwendung

Ein Plugin wird als einfaches JavaScript-Objekt definiert, das eine name-Eigenschaft und eine setup-Funktion enthält.

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

const myPlugin: BunPlugin = {
  name: "Benutzerdefinierter Loader",
  setup(build) {
    // Implementierung
  },
};

Dieses Plugin kann beim Aufruf von Bun.build an das plugins-Array übergeben werden.

index.ts
ts
await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./out",
  plugins: [myPlugin],
});

Plugin-Lifecycle

Namespaces

onLoad und onResolve akzeptieren einen optionalen namespace-String. Was ist ein Namespace?

Jedes Modul hat einen Namespace. Namespaces werden verwendet, um den Import im transpilierten Code zu präfixen; zum Beispiel wird ein Loader mit einem filter: /\.yaml$/ und namespace: "yaml:" einen Import von ./myfile.yaml in yaml:./myfile.yaml transformieren.

Der Standard-Namespace ist "file" und es ist nicht notwendig, ihn anzugeben, zum Beispiel: import myModule from "./my-module.ts" ist dasselbe wie import myModule from "file:./my-module.ts".

Andere gängige Namespaces sind:

  • "bun": für Bun-spezifische Module (z.B. "bun:test", "bun:sqlite")
  • "node": für Node.js-Module (z.B. "node:fs", "node:path")

onStart

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

Registriert einen Callback, der ausgeführt wird, wenn der Bundler ein neues Bundle startet.

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

plugin({
  name: "onStart Beispiel",

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

Der Callback kann ein Promise zurückgeben. Nachdem der Bundle-Prozess initialisiert wurde, wartet der Bundler, bis alle onStart()-Callbacks abgeschlossen sind, bevor er fortfährt.

Zum Beispiel:

index.ts
ts
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "10 Sekunden schlafen",
      setup(build) {
        build.onStart(async () => {
          await Bun.sleep(10_000);
        });
      },
    },
    {
      name: "Bundle-Zeit in eine Datei protokollieren",
      setup(build) {
        build.onStart(async () => {
          const now = Date.now();
          await Bun.$`echo ${now} > bundle-time.txt`;
        });
      },
    },
  ],
});

Im obigen Beispiel wartet Bun, bis das erste onStart() (10 Sekunden schlafen) abgeschlossen ist, sowie das zweite onStart() (Bundle-Zeit in eine Datei schreiben).

NOTE

`onStart()`-Callbacks (wie jeder andere Lifecycle-Callback auch) haben nicht die Möglichkeit, das `build.config`-Objekt zu modifizieren. Wenn Sie `build.config` mutieren möchten, müssen Sie dies direkt in der `setup()`-Funktion tun.

onResolve

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

Um Ihr Projekt zu bündeln, geht Bun den Abhängigkeitsbaum aller Module in Ihrem Projekt durch. Für jedes importierte Modul muss Bun dieses Modul tatsächlich finden und lesen. Der "finden"-Teil wird als "Auflösen" eines Moduls bezeichnet.

Der onResolve()-Plugin-Lifecycle-Callback ermöglicht es Ihnen zu konfigurieren, wie ein Modul aufgelöst wird.

Das erste Argument an onResolve() ist ein Objekt mit einer filter- und namespace-Eigenschaft. Der filter ist ein regulärer Ausdruck, der auf den Import-String angewendet wird. Effektiv ermöglichen diese Ihnen zu filtern, auf welche Module Ihre benutzerdefinierte Auflösungslogik angewendet wird.

Das zweite Argument an onResolve() ist ein Callback, der für jeden Modul-Import ausgeführt wird, den Bun findet, der mit dem im ersten Argument definierten Filter und Namespace übereinstimmt.

Der Callback erhält als Eingabe den Pfad zum übereinstimmenden Modul. Der Callback kann einen neuen Pfad für das Modul zurückgeben. Bun wird den Inhalt des neuen Pfads lesen und als Modul parsen.

Zum Beispiel, Umleitung aller Importe nach images/ nach ./public/images/:

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

plugin({
  name: "onResolve Beispiel",
  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;

Nachdem Bun's Bundler ein Modul aufgelöst hat, muss es den Inhalt des Moduls lesen und parsen.

Der onLoad()-Plugin-Lifecycle-Callback ermöglicht es Ihnen, den Inhalt eines Moduls zu modifizieren, bevor es von Bun gelesen und geparst wird.

Wie bei onResolve() ermöglicht Ihnen das erste Argument an onLoad() zu filtern, auf welche Module diese onLoad()-Ausführung angewendet wird.

Das zweite Argument an onLoad() ist ein Callback, der für jedes übereinstimmende Modul ausgeführt wird, bevor Bun den Inhalt des Moduls in den Speicher lädt.

Dieser Callback erhält als Eingabe den Pfad zum übereinstimmenden Modul, den Importeur des Moduls (das Modul, das das Modul importiert hat), den Namespace des Moduls und die Art des Moduls.

Der Callback kann einen neuen contents-String für das Modul sowie einen neuen loader zurückgeben.

Zum Beispiel:

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

const envPlugin: BunPlugin = {
  name: "env Plugin",
  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"

Dieses Plugin transformiert alle Importe der Form import env from "env" in ein JavaScript-Modul, das die aktuellen Umgebungsvariablen exportiert.

.defer()

Eines der Argumente, die an den onLoad-Callback übergeben werden, ist eine defer-Funktion. Diese Funktion gibt ein Promise zurück, das aufgelöst wird, wenn alle anderen Module geladen wurden.

Dies ermöglicht es Ihnen, die Ausführung des onLoad-Callbacks zu verzögern, bis alle anderen Module geladen wurden.

Dies ist nützlich, um den Inhalt eines Moduls zurückzugeben, das von anderen Modulen abhängt.

Beispiel: Verfolgen und Melden ungenutzter Exporte

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

plugin({
  name: "Importe verfolgen",
  setup(build) {
    const transpiler = new Bun.Transpiler();

    let trackedImports: Record<string, number> = {};

    // Jedes Modul, das durch diesen onLoad-Callback geht,
    // wird seine Importe in `trackedImports` aufzeichnen
    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 }) => {
      // Warten, bis alle Dateien geladen sind, um sicherzustellen,
      // dass jede Datei durch die obige `onLoad()`-Funktion geht
      // und ihre Importe verfolgt werden
      await defer();

      // JSON ausgeben, das die Statistiken jedes Imports enthält
      return {
        contents: `export default ${JSON.stringify(trackedImports)}`,
        loader: "json",
      };
    });
  },
});

Native Plugins

Einer der Gründe, warum Bun's Bundler so schnell ist, liegt darin, dass er in nativem Code geschrieben ist und Multi-Threading nutzt, um Module parallel zu laden und zu parsen.

Eine Einschränkung von in JavaScript geschriebenen Plugins ist jedoch, dass JavaScript selbst single-threaded ist.

Native Plugins werden als NAPI-Module geschrieben und können auf mehreren Threads ausgeführt werden. Dies ermöglicht es nativen Plugins, viel schneller als JavaScript-Plugins zu laufen.

Darüber hinaus können native Plugins unnötige Arbeit überspringen, wie z.B. die UTF-8 -> UTF-16-Konvertierung, die benötigt wird, um Strings an JavaScript zu übergeben.

Dies sind die folgenden Lifecycle-Hooks, die für native Plugins verfügbar sind:

  • onBeforeParse(): Wird auf jedem Thread aufgerufen, bevor eine Datei von Bun's Bundler geparst wird.

Native Plugins sind NAPI-Module, die Lifecycle-Hooks als C-ABI-Funktionen exponieren.

Um ein natives Plugin zu erstellen, müssen Sie eine C-ABI-Funktion exportieren, die der Signatur des nativen Lifecycle-Hooks entspricht, das Sie implementieren möchten.

Erstellen eines nativen Plugins in Rust

Native Plugins sind NAPI-Module, die Lifecycle-Hooks als C-ABI-Funktionen exponieren.

Um ein natives Plugin zu erstellen, müssen Sie eine C-ABI-Funktion exportieren, die der Signatur des nativen Lifecycle-Hooks entspricht, das Sie implementieren möchten.

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

Dann installieren Sie diese Crate:

bash
cargo add bun-native-plugin

Jetzt verwenden wir in der lib.rs-Datei das bun_native_plugin::bun-Proc-Makro, um eine Funktion zu definieren, die unser natives Plugin implementiert.

Hier ist ein Beispiel, das den onBeforeParse-Hook implementiert:

lib.rs
rust
use bun_native_plugin::{define_bun_plugin, OnBeforeParse, bun, Result, anyhow, BunLoader};
use napi_derive::napi;

/// Definiert das Plugin und seinen Namen
define_bun_plugin!("replace-foo-with-bar");

/// Hier implementieren wir `onBeforeParse` mit Code, der alle Vorkommen von
/// `foo` durch `bar` ersetzt.
///
/// Wir verwenden das #[bun]-Makro, um einen Teil des Boilerplate-Codes zu generieren.
///
/// Das Argument der Funktion (`handle: &mut OnBeforeParse`) teilt
/// dem Makro mit, dass diese Funktion den `onBeforeParse`-Hook implementiert.
#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
  // Holt den Eingabe-Quellcode.
  let input_source_code = handle.input_source_code()?;

  // Holt den Loader für die Datei
  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(())
}

Und um es in Bun.build() zu verwenden:

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

Dieser Lifecycle-Callback wird unmittelbar vor dem Parsen einer Datei durch Bun's Bundler ausgeführt.

Als Eingabe erhält es den Inhalt der Datei und kann optional neuen Quellcode zurückgeben.

Bun von www.bunjs.com.cn bearbeitet