Skip to content

Bun proporciona una API de plugins universal que se puede usar para extender tanto el runtime como el bundler.

Los plugins interceptan importaciones y realizan lógica de carga personalizada: leer archivos, transpilar código, etc. Se pueden usar para agregar soporte para tipos de archivos adicionales, como .scss o .yaml. En el contexto del bundler de Bun, los plugins se pueden usar para implementar características a nivel de framework como extracción de CSS, macros y co-ubicación de código cliente-servidor.

Hooks del ciclo de vida

Los plugins pueden registrar callbacks para ejecutarse en varios puntos del ciclo de vida de un bundle:

  • onStart(): Se ejecuta una vez que el bundler ha iniciado un bundle
  • onResolve(): Se ejecuta antes de que un módulo sea resuelto
  • onLoad(): Se ejecuta antes de que un módulo sea cargado.
  • onBeforeParse(): Ejecuta addons nativos zero-copy en el hilo del parser antes de que un archivo sea parseado.

Referencia

Una visión general aproximada de los tipos (por favor consulta bun.d.ts de Bun para las definiciones completas de tipos):

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

Uso

Un plugin se define como un objeto de JavaScript simple que contiene una propiedad name y una función setup.

tsx
import type { BunPlugin } from "bun";

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

Este plugin se puede pasar al array plugins al llamar a Bun.build.

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

Ciclo de vida del plugin

Namespaces

onLoad y onResolve aceptan una cadena namespace opcional. ¿Qué es un namespace?

Cada módulo tiene un namespace. Los namespaces se usan para prefijar la importación en el código transpilado; por ejemplo, un loader con filter: /\.yaml$/ y namespace: "yaml:" transformará una importación de ./myfile.yaml a yaml:./myfile.yaml.

El namespace predeterminado es "file" y no es necesario especificarlo, por ejemplo: import myModule from "./my-module.ts" es lo mismo que import myModule from "file:./my-module.ts".

Otros namespaces comunes son:

  • "bun": para módulos específicos de Bun (ej. "bun:test", "bun:sqlite")
  • "node": para módulos de Node.js (ej. "node:fs", "node:path")

onStart

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

Registra un callback para ejecutarse cuando el bundler inicia un nuevo bundle.

ts
import { plugin } from "bun";

plugin({
  name: "onStart example",

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

El callback puede devolver una Promise. Después de que el proceso de bundle se ha inicializado, el bundler espera hasta que todos los callbacks onStart() se hayan completado antes de continuar.

Por ejemplo:

ts
const result = await Bun.build({
  entrypoints: ["./app.ts"],
  outdir: "./dist",
  sourcemap: "external",
  plugins: [
    {
      name: "Dormir por 10 segundos",
      setup(build) {
        build.onStart(async () => {
          await Bun.sleep(10_000);
        });
      },
    },
    {
      name: "Registrar tiempo de bundle en un archivo",
      setup(build) {
        build.onStart(async () => {
          const now = Date.now();
          await Bun.$`echo ${now} > bundle-time.txt`;
        });
      },
    },
  ],
});

En el ejemplo anterior, Bun espera hasta que el primer onStart() (dormir por 10 segundos) se haya completado, así como el segundo onStart() (escribir el tiempo de bundle en un archivo).

Ten en cuenta que los callbacks onStart() (como cualquier otro callback del ciclo de vida) no tienen la capacidad de modificar el objeto build.config. Si quieres modificar build.config, debes hacerlo directamente en la función setup().

onResolve

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

Para hacer bundle de tu proyecto, Bun recorre el árbol de dependencias de todos los módulos en tu proyecto. Para cada módulo importado, Bun realmente tiene que encontrar y leer ese módulo. La parte de "encontrar" se conoce como "resolver" un módulo.

El callback del ciclo de vida onResolve() te permite configurar cómo se resuelve un módulo.

El primer argumento de onResolve() es un objeto con una propiedad filter y namespace. El filter es una expresión regular que se ejecuta en la cadena de importación. Efectivamente, esto te permite filtrar a qué módulos se aplicará tu lógica de resolución personalizada.

El segundo argumento de onResolve() es un callback que se ejecuta para cada importación de módulo que Bun encuentra que coincide con el filter y namespace definidos en el primer argumento.

El callback recibe como entrada la ruta al módulo coincidente. El callback puede devolver una nueva ruta para el módulo. Bun leerá el contenido de la nueva ruta y lo parseará como un módulo.

Por ejemplo, redirigiendo todas las importaciones a images/ a ./public/images/:

ts
import { plugin } from "bun";

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

Después de que el bundler de Bun ha resuelto un módulo, necesita leer el contenido del módulo y parsearlo.

El callback del ciclo de vida onLoad() te permite modificar el contenido de un módulo antes de que sea leído y parseado por Bun.

Como onResolve(), el primer argumento de onLoad() te permite filtrar a qué módulos se aplicará esta invocación de onLoad().

El segundo argumento de onLoad() es un callback que se ejecuta para cada módulo coincidente antes de que Bun cargue el contenido del módulo en memoria.

Este callback recibe como entrada la ruta al módulo coincidente, el importer del módulo (el módulo que importó el módulo), el namespace del módulo, y el kind del módulo.

El callback puede devolver una nueva cadena contents para el módulo así como un nuevo loader.

Por ejemplo:

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"

Este plugin transformará todas las importaciones de la forma import env from "env" en un módulo JavaScript que exporta las variables de entorno actuales.

.defer()

Uno de los argumentos pasados al callback onLoad es una función defer. Esta función devuelve una Promise que se resuelve cuando todos los otros módulos han sido cargados.

Esto te permite retrasar la ejecución del callback onLoad hasta que todos los otros módulos hayan sido cargados.

Esto es útil para devolver el contenido de un módulo que depende de otros módulos.

Ejemplo: rastrear y reportar exportaciones no usadas
ts
import { plugin } from "bun";

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

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

    // Cada módulo que pasa por este callback onLoad
    // registrará sus importaciones en `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 }) => {
      // Esperar a que todos los archivos sean cargados, asegurando
      // que cada archivo pase por la función `onLoad()` anterior
      // y sus importaciones sean rastreadas
      await defer();

      // Emitir JSON conteniendo las estadísticas de cada importación
      return {
        contents: `export default ${JSON.stringify(trackedImports)}`,
        loader: "json",
      };
    });
  },
});

Ten en cuenta que la función .defer() actualmente tiene la limitación de que solo se puede llamar una vez por callback onLoad.

Plugins nativos

Una de las razones por las que el bundler de Bun es tan rápido es que está escrito en código nativo y aprovecha el multi-hilo para cargar y parsear módulos en paralelo.

Sin embargo, una limitación de los plugins escritos en JavaScript es que el propio JavaScript es monohilo.

Los plugins nativos están escritos como módulos NAPI y se pueden ejecutar en múltiples hilos. Esto permite que los plugins nativos se ejecuten mucho más rápido que los plugins de JavaScript.

Además, los plugins nativos pueden omitir trabajo innecesario como la conversión UTF-8 -> UTF-16 necesaria para pasar cadenas a JavaScript.

Estos son los siguientes hooks del ciclo de vida disponibles para plugins nativos:

  • onBeforeParse(): Llamado en cualquier hilo antes de que un archivo sea parseado por el bundler de Bun.

Los plugins nativos son módulos NAPI que exponen hooks del ciclo de vida como funciones C ABI.

Para crear un plugin nativo, debes exportar una función C ABI que coincida con la firma del hook del ciclo de vida nativo que quieres implementar.

Crear un plugin nativo en Rust

Los plugins nativos son módulos NAPI que exponen hooks del ciclo de vida como funciones C ABI.

Para crear un plugin nativo, debes exportar una función C ABI que coincida con la firma del hook del ciclo de vida nativo que quieres implementar.

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

Luego instala este crate:

bash
cargo add bun-native-plugin

Ahora, dentro del archivo lib.rs, usaremos la macro proc bun_native_plugin::bun para definir una función que implementará nuestro plugin nativo.

Aquí hay un ejemplo implementando el hook onBeforeParse:

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

/// Define el plugin y su nombre
define_bun_plugin!("replace-foo-with-bar");

/// Aquí implementaremos `onBeforeParse` con código que reemplaza todas las ocurrencias de
/// `foo` con `bar`.
///
/// Usamos la macro #[bun] para generar parte del código boilerplate.
///
/// El argumento de la función (`handle: &mut OnBeforeParse`) le dice
/// a la macro que esta función implementa el hook `onBeforeParse`.
#[bun]
pub fn replace_foo_with_bar(handle: &mut OnBeforeParse) -> Result<()> {
  // Obtener el código fuente de entrada.
  let input_source_code = handle.input_source_code()?;

  // Obtener el Loader para el archivo
  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(())
}

Y para usarlo en Bun.build():

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;

Este callback del ciclo de vida se ejecuta inmediatamente antes de que un archivo sea parseado por el bundler de Bun.

Como entrada, recibe el contenido del archivo y opcionalmente puede devolver nuevo código fuente.

Este callback se puede llamar desde cualquier hilo, por lo que la implementación del módulo napi debe ser thread-safe.

Bun por www.bunjs.com.cn editar