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 bundleonResolve(): Eseguito prima che un modulo sia risoltoonLoad(): Eseguito prima che un modulo sia caricatoonBeforeParse(): 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):
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.
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.
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
onStart(callback: () => void): Promise<void> | void;Registra una callback da eseguire quando il bundler avvia un nuovo bundle.
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:
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
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/:
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
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:
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
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.
bun add -g @napi-rs/cli
napi newPoi installa questo crate:
cargo add bun-native-pluginOra, 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:
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():
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
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.