Skip to content

Las macros son un mecanismo para ejecutar funciones de JavaScript en tiempo de construcción. Los valores devueltos por estas funciones se incluyen directamente en línea en tu bundle.

Como un ejemplo sencillo, considera esta función simple que devuelve un número aleatorio.

random.ts
ts
export function random() {
  return Math.random();
}

Esta es solo una función regular en un archivo regular, pero podemos usarla como una macro así:

cli.tsx
tsx
import { random } from "./random.ts" with { type: "macro" };

console.log(`Tu número aleatorio es ${random()}`);

NOTE

Las macros se indican usando sintaxis de atributos de import. Si no has visto esta sintaxis antes, es una propuesta TC39 en Stage 3 que te permite adjuntar metadatos adicionales a declaraciones de import.

Ahora empaquetaremos este archivo con bun build. El archivo empaquetado se imprimirá a stdout.

bash
bun build ./cli.tsx
js
console.log(`Tu número aleatorio es ${0.6805550949689833}`);

Como puedes ver, el código fuente de la función random no aparece en ninguna parte del bundle. En su lugar, se ejecuta durante el empaquetado y la llamada a la función (random()) se reemplaza con el resultado de la función. Dado que el código fuente nunca se incluirá en el bundle, las macros pueden realizar de forma segura operaciones privilegiadas como leer desde una base de datos.

Cuándo usar macros

Si tienes varios scripts de construcción para pequeñas cosas donde de otro modo tendrías un script de construcción único, la ejecución de código en tiempo de construcción puede ser más fácil de mantener. Vive con el resto de tu código, se ejecuta con el resto de la construcción, se paraleliza automáticamente, y si falla, la construcción también falla.

Si te encuentras ejecutando mucho código en tiempo de construcción, considera ejecutar un servidor en su lugar.

Atributos de import

Las macros de Bun son declaraciones de import anotadas usando:

  • with { type: 'macro' } — un atributo de import, una propuesta de ECMA Script en Stage 3
  • assert { type: 'macro' } — una aserción de import, una encarnación anterior de atributos de import que ahora ha sido abandonada (pero ya es soportada por varios navegadores y runtimes)

Consideraciones de seguridad

Las macros deben importarse explícitamente con { type: "macro" } para ejecutarse en tiempo de construcción. Estos imports no tienen efecto si no se llaman, a diferencia de los imports regulares de JavaScript que pueden tener efectos secundarios.

Puedes deshabilitar las macros por completo pasando la bandera --no-macros a Bun. Produce un error de construcción como este:

error: Macros are disabled

foo();
^
./hello.js:3:1 53

Para reducir la superficie de ataque potencial para paquetes maliciosos, las macros no pueden invocarse desde dentro de node_modules/**/*. Si un paquete intenta invocar una macro, verás un error como este:

error: For security reasons, macros cannot be run from node_modules.

beEvil();
^
node_modules/evil/index.js:3:1 50

El código de tu aplicación aún puede importar macros desde node_modules e invocarlas.

cli.tsx
ts
import { macro } from "some-package" with { type: "macro" };

macro();

Condición de export "macro"

Al distribuir una librería que contiene una macro a npm u otro registro de paquetes, usa la condición de export "macro" para proporcionar una versión especial de tu paquete exclusivamente para el entorno de macros.

package.json
json
{
  "name": "my-package",
  "exports": {
    "import": "./index.js",
    "require": "./index.js",
    "default": "./index.js",
    "macro": "./index.macro.js"
  }
}

Con esta configuración, los usuarios pueden consumir tu paquete en runtime o en tiempo de construcción usando el mismo especificador de import:

index.ts
ts
import pkg from "my-package"; // import de runtime
import { macro } from "my-package" with { type: "macro" }; // import de macro

El primer import se resolverá a ./node_modules/my-package/index.js, mientras que el segundo será resuelto por el empaquetador de Bun a ./node_modules/my-package/index.macro.js.

Ejecución

Cuando el transpilador de Bun ve un import de macro, llama a la función dentro del transpilador usando el runtime de JavaScript de Bun y convierte el valor de retorno de JavaScript a un nodo AST. Estas funciones de JavaScript se llaman en tiempo de construcción, no en runtime.

Las macros se ejecutan sincrónicamente en el transpilador durante la fase de visita, antes de los plugins y antes de que el transpilador genere el AST. Se ejecutan en el orden en que se importan. El transpilador esperará a que la macro termine de ejecutarse antes de continuar. El transpilador también hará await de cualquier Promise devuelta por una macro.

El empaquetador de Bun es multi-hilo. Como tal, las macros se ejecutan en paralelo dentro de múltiples "workers" de JavaScript generados.

Eliminación de código muerto

El empaquetador realiza eliminación de código muerto después de ejecutar e incluir macros en línea. Así, dada la siguiente macro:

returnFalse.ts
ts
export function returnFalse() {
  return false;
}

...entonces empaquetar el siguiente archivo producirá un bundle vacío, siempre que la opción de minificación de sintaxis esté habilitada.

index.ts
ts
import { returnFalse } from "./returnFalse.ts" with { type: "macro" };

if (returnFalse()) {
  console.log("Este código se elimina");
}

Serialización

El transpilador de Bun necesita poder serializar el resultado de la macro para que pueda incluirse en línea en el AST. Se soportan todas las estructuras de datos compatibles con JSON:

macro.ts
ts
export function getObject() {
  return {
    foo: "bar",
    baz: 123,
    array: [1, 2, { nested: "value" }],
  };
}

Las macros pueden ser async, o devolver instancias de Promise. El transpilador de Bun hará automáticamente await de la Promise e incluirá el resultado en línea.

macro.ts
ts
export async function getText() {
  return "valor async";
}

El transpilador implementa lógica especial para serializar formatos de datos comunes como Response, Blob, TypedArray.

  • TypedArray: Se resuelve a una cadena codificada en base64.
  • Response: Bun leerá el Content-Type y serializará en consecuencia; por ejemplo, una Response con tipo application/json se analizará automáticamente en un objeto y text/plain se incluirá en línea como una cadena. Las Responses con un tipo no reconocido o undefined se codificarán en base-64.
  • Blob: Al igual que con Response, la serialización depende de la propiedad type.

El resultado de fetch es Promise<Response>, por lo que puede devolverse directamente.

macro.ts
ts
export function getObject() {
  return fetch("https://bun.com");
}

Las funciones e instancias de la mayoría de clases (excepto las mencionadas anteriormente) no son serializables.

macro.ts
ts
export function getText(url: string) {
  // ¡esto no funciona!
  return () => {};
}

Argumentos

Las macros pueden aceptar entradas, pero solo en casos limitados. El valor debe conocerse estáticamente. Por ejemplo, lo siguiente no está permitido:

index.ts
ts
import { getText } from "./getText.ts" with { type: "macro" };

export function howLong() {
  // el valor de `foo` no puede conocerse estáticamente
  const foo = Math.random() ? "foo" : "bar";

  const text = getText(`https://example.com/${foo}`);
  console.log("La página tiene ", text.length, " caracteres de largo");
}

Sin embargo, si el valor de foo se conoce en tiempo de construcción (digamos, si es una constante o el resultado de otra macro) entonces está permitido:

index.ts
ts
import { getText } from "./getText.ts" with { type: "macro" };
import { getFoo } from "./getFoo.ts" with { type: "macro" };

export function howLong() {
  // esto funciona porque getFoo() se conoce estáticamente
  const foo = getFoo();
  const text = getText(`https://example.com/${foo}`);
  console.log("La página tiene", text.length, "caracteres de largo");
}

Esto genera:

js
function howLong() {
  console.log("La página tiene", 1322, "caracteres de largo");
}
export { howLong };

Ejemplos

Incrustar hash del último commit de git

getGitCommitHash.ts
ts
export function getGitCommitHash() {
  const { stdout } = Bun.spawnSync({
    cmd: ["git", "rev-parse", "HEAD"],
    stdout: "pipe",
  });

  return stdout.toString();
}

Cuando lo construimos, getGitCommitHash se reemplaza con el resultado de llamar a la función:

ts
import { getGitCommitHash } from "./getGitCommitHash.ts" with { type: "macro" };

console.log(`El hash del commit actual de Git es ${getGitCommitHash()}`);
ts
console.log(`El hash del commit actual de Git es 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);

Hacer peticiones fetch() en tiempo de construcción

En este ejemplo, hacemos una petición HTTP saliente usando fetch(), analizamos la respuesta HTML usando HTMLRewriter, y devolvemos un objeto que contiene el título y las metaetiquetas, todo en tiempo de construcción.

meta.ts
ts
export async function extractMetaTags(url: string) {
  const response = await fetch(url);
  const meta = {
    title: "",
  };
  new HTMLRewriter()
    .on("title", {
      text(element) {
        meta.title += element.text;
      },
    })
    .on("meta", {
      element(element) {
        const name =
          element.getAttribute("name") || element.getAttribute("property") || element.getAttribute("itemprop");

        if (name) meta[name] = element.getAttribute("content");
      },
    })
    .transform(response);

  return meta;
}

La función extractMetaTags se borra en tiempo de construcción y se reemplaza con el resultado de la llamada a la función. Esto significa que la petición fetch ocurre en tiempo de construcción, y el resultado se incrusta en el bundle. Además, la rama que lanza el error se elimina ya que es inalcanzable.

jsx
import { extractMetaTags } from "./meta.ts" with { type: "macro" };

export const Head = () => {
  const headTags = extractMetaTags("https://example.com");

  if (headTags.title !== "Example Domain") {
    throw new Error("Se esperaba que el título fuera 'Example Domain'");
  }

  return (
    <head>
      <title>{headTags.title}</title>
      <meta name="viewport" content={headTags.viewport} />
    </head>
  );
};
jsx
export const Head = () => {
  const headTags = {
    title: "Example Domain",
    viewport: "width=device-width, initial-scale=1",
  };

  return (
    <head>
      <title>{headTags.title}</title>
      <meta name="viewport" content={headTags.viewport} />
    </head>
  );
};

Bun por www.bunjs.com.cn editar