Skip to content

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

Plugins fangen Importe ab und führen benutzerdefinierte Lade-Logik aus: Dateien lesen, Code transpilieren usw. Sie können verwendet werden, um Unterstützung für zusätzliche Dateitypen wie .scss oder .yaml hinzuzufügen. Im Kontext von Buns Bundler können Plugins verwendet werden, um Framework-Funktionen wie CSS-Extraktion, Makros und Client-Server-Code-Kolokation 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 Buns bun.d.ts für die vollständigen Typdefinitionen):

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.

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

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 transformiert ein Loader mit filter: /\.yaml$/ und namespace: "yaml:" einen Import von ./myfile.yaml in yaml:./myfile.yaml.

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.

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:

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

Beachten Sie, dass onStart()-Callbacks (wie jeder andere Lifecycle-Callback) nicht die Möglichkeit haben, das build.config-Objekt zu modifizieren. Wenn Sie build.config ändern 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 bundlen, durchläuft Bun den Abhängigkeitsbaum aller Module in Ihrem Projekt. 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. Dies ermöglicht es Ihnen effektiv 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 und 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, Umleiten aller Importe zu images/ nach ./public/images/:

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 Buns 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 ändern, bevor es von Bun gelesen und geparst wird.

Wie bei onResolve() ermöglicht Ihnen das erste Argument an onLoad() zu filtern, auf welche Module dieser Aufruf von onLoad() 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:

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
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 wurden, 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",
      };
    });
  },
});

Beachten Sie, dass die .defer()-Funktion derzeit die Einschränkung hat, dass sie nur einmal pro onLoad-Callback aufgerufen werden kann.

Native Plugins

Einer der Gründe, warum Buns 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 Plugins, die in JavaScript geschrieben sind, 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 zu laufen als JavaScript-Plugins.

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

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

  • onBeforeParse(): Wird auf einem beliebigen Thread aufgerufen, bevor eine Datei von Buns 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

Installieren Sie dann diese Crate:

bash
cargo add bun-native-plugin

In der lib.rs-Datei verwenden wir 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:

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

/// Definiere 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<()> {
  // Hole den Eingabe-Quellcode.
  let input_source_code = handle.input_source_code()?;

  // Hole 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:

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;

Dieser Lifecycle-Callback wird unmittelbar bevor eine Datei von Buns Bundler geparst wird ausgeführt.

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

Dieser Callback kann von jedem Thread aufgerufen werden, daher muss die Napi-Modul-Implementierung threadsicher sein.

Bun von www.bunjs.com.cn bearbeitet