Skip to content

Bun fornisce un'API plugin universale che può essere usata per estendere sia il runtime che il bundler.

I plugin intercettano gli import ed eseguono logica di caricamento custom: lettura file, transpilazione codice, ecc. Possono essere usati per aggiungere supporto per tipi di file aggiuntivi, come .scss o .yaml. Nel contesto del bundler di Bun, i plugin possono essere usati per implementare feature a livello di framework come estrazione CSS, macro, e co-locazione codice client-server.

Hook del ciclo di vita

I plugin possono registrare callback da eseguire in vari punti del ciclo di vita di un bundle:

  • onStart(): Eseguito una volta che il bundler ha avviato un bundle
  • onResolve(): Eseguito prima che un modulo sia risolto
  • onLoad(): Eseguito prima che un modulo sia caricato
  • onBeforeParse(): Esegue addon nativi zero-copy nel thread del parser prima che un file sia parsato

Riferimento

Una panoramica approssimativa dei tipi (per favore riferisciti a bun.d.ts di Bun per le definizioni complete dei tipi):

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

Utilizzo

Un plugin è definito come un semplice oggetto JavaScript contenente una proprietà name e una funzione setup.

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

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

Questo plugin può essere passato nell'array plugins quando chiami Bun.build.

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

Ciclo di vita del plugin

Namespaces

onLoad e onResolve accettano una stringa namespace opzionale. Cos'è un namespace?

Ogni modulo ha un namespace. I namespace sono usati per prefissare l'import nel codice transpilato; per istanza, un loader con un filter: /\.yaml$/ e namespace: "yaml:" trasformerà un import da ./myfile.yaml in yaml:./myfile.yaml.

Il namespace di default è "file" e non è necessario specificarlo, per istanza: import myModule from "./my-module.ts" è lo stesso di import myModule from "file:./my-module.ts".

Altri namespace comuni sono:

  • "bun": per moduli specifici di Bun (es. "bun:test", "bun:sqlite")
  • "node": per moduli Node.js (es. "node:fs", "node:path")

onStart

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

Registra una callback da eseguire quando il bundler avvia un nuovo bundle.

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

plugin({
  name: "esempio onStart",

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

La callback può restituire una Promise. Dopo che il processo di bundle è stato inizializzato, il bundler aspetta che tutte le callback onStart() siano completate prima di continuare.

Per esempio:

index.ts
ts
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "Dormi per 10 secondi",
      setup(build) {
        build.onStart(async () => {
          await Bun.sleep(10_000);
        });
      },
    },
    {
      name: "Logga il tempo di bundle in un file",
      setup(build) {
        build.onStart(async () => {
          const now = Date.now();
          await Bun.$`echo ${now} > bundle-time.txt`;
        });
      },
    },
  ],
});

Nell'esempio sopra, Bun aspetterà che il primo onStart() (dormendo per 10 secondi) sia completato, così come il secondo onStart() (scrivendo il tempo di bundle in un file).

NOTE

Le callback `onStart()` (come ogni altra callback del ciclo di vita) non hanno la capacità di modificare l'oggetto `build.config`. Se vuoi mutare `build.config`, devi farlo direttamente nella funzione `setup()`.

onResolve

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

Per bundlare il tuo progetto, Bun scende lungo l'albero delle dipendenze di tutti i moduli nel tuo progetto. Per ogni modulo importato, Bun deve effettivamente trovare e leggere quel modulo. La parte del "trovare" è conosciuta come "risolvere" un modulo.

La callback del ciclo di vita del plugin onResolve() ti permette di configurare come un modulo è risolto.

Il primo argomento a onResolve() è un oggetto con una proprietà filter e namespace. Il filter è un'espressione regolare che è eseguita sulla stringa di import. Effettivamente, questi ti permettono di filtrare a quali moduli la tua logica di risoluzione custom si applicherà.

Il secondo argomento a onResolve() è una callback che è eseguita per ogni import di modulo che Bun trova che corrisponde al filter e namespace definiti nel primo argomento.

La callback riceve come input il path al modulo corrispondente. La callback può restituire un nuovo path per il modulo. Bun leggerà i contenuti del nuovo path e lo parserà come un modulo.

Per esempio, reindirizzando tutti gli import a images/ a ./public/images/:

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

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

Dopo che il bundler di Bun ha risolto un modulo, deve leggere i contenuti del modulo e parsarlo.

La callback del ciclo di vita del plugin onLoad() ti permette di modificare i contenuti di un modulo prima che sia letto e parsato da Bun.

Come onResolve(), il primo argomento a onLoad() ti permette di filtrare a quali moduli questa invocazione di onLoad() si applicherà.

Il secondo argomento a onLoad() è una callback che è eseguita per ogni modulo corrispondente prima che Bun carichi i contenuti del modulo in memoria.

Questa callback riceve come input il path al modulo corrispondente, l'importer del modulo (il modulo che ha importato il modulo), il namespace del modulo, e il kind del modulo.

La callback può restituire una nuova stringa contents per il modulo così come un nuovo loader.

Per esempio:

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"

Questo plugin trasformerà tutti gli import della forma import env from "env" in un modulo JavaScript che esporta le variabili d'ambiente correnti.

.defer()

Uno degli argomenti passati alla callback onLoad è una funzione defer. Questa funzione restituisce una Promise che è risolta quando tutti gli altri moduli sono stati caricati.

Questo ti permette di ritardare l'esecuzione della callback onLoad fino a quando tutti gli altri moduli sono stati caricati.

Questo è utile per restituire contenuti di un modulo che dipende da altri moduli.

Esempio: tracciare e riportare export inutilizzati

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

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

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

    // Ogni modulo che passa attraverso questa callback onLoad
    // registrerà i suoi import in `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 }) => {
      // Aspetta che tutti i file siano caricati, assicurando
      // che ogni file passi attraverso la funzione `onLoad()` sopra
      // e i loro import siano tracciati
      await defer();

      // Emetti JSON contenente le statistiche di ogni import
      return {
        contents: `export default ${JSON.stringify(trackedImports)}`,
        loader: "json",
      };
    });
  },
});

Plugin nativi

Uno dei motivi per cui il bundler di Bun è così veloce è che è scritto in codice nativo e sfrutta il multi-threading per caricare e parsare i moduli in parallelo.

Tuttavia, una limitazione dei plugin scritti in JavaScript è che JavaScript stesso è single-threaded.

I plugin nativi sono scritti come moduli NAPI e possono essere eseguiti su più thread. Questo permette ai plugin nativi di eseguire molto più velocemente dei plugin JavaScript.

Inoltre, i plugin nativi possono saltare lavoro non necessario come la conversione UTF-8 -> UTF-16 necessaria per passare stringhe a JavaScript.

Questi sono i seguenti hook del ciclo di vita disponibili per i plugin nativi:

  • onBeforeParse(): Chiamato su qualsiasi thread prima che un file sia parsato dal bundler di Bun.

I plugin nativi sono moduli NAPI che espongono hook del ciclo di vita come funzioni C ABI.

Per creare un plugin nativo, devi esportare una funzione C ABI che corrisponde alla firma dell'hook del ciclo di vita nativo che vuoi implementare.

Creare un plugin nativo in Rust

I plugin nativi sono moduli NAPI che espongono hook del ciclo di vita come funzioni C ABI.

Per creare un plugin nativo, devi esportare una funzione C ABI che corrisponde alla firma dell'hook del ciclo di vita nativo che vuoi implementare.

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

Poi installa questo crate:

bash
cargo add bun-native-plugin

Ora, dentro il file lib.rs, useremo la proc macro bun_native_plugin::bun per definire una funzione che implementerà il nostro plugin nativo.

Ecco un esempio che implementa l'hook onBeforeParse:

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

/// Definisci il plugin e il suo nome
define_bun_plugin!("replace-foo-with-bar");

/// Qui implementeremo `onBeforeParse` con codice che rimpiazza tutte le occorrenze di
/// `foo` con `bar`.
///
/// Usiamo la macro #[bun] per generare parte del codice boilerplate.
///
/// L'argomento della funzione (`handle: &mut OnBeforeParse`) dice
/// alla macro che questa funzione implementa l'hook `onBeforeParse`.
#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
  // Recupera il codice sorgente di input.
  let input_source_code = handle.input_source_code()?;

  // Ottieni il Loader per il file
  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(())
}

E per usarlo in Bun.build():

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;

Questa callback del ciclo di vita è eseguita immediatamente prima che un file sia parsato dal bundler di Bun.

Come input, riceve i contenuti del file e può opzionalmente restituire nuovo codice sorgente.

Bun a cura di www.bunjs.com.cn