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.
export function random() {
return Math.random();
}Esta es solo una función regular en un archivo regular, pero podemos usarla como una macro así:
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.
bun build ./cli.tsxconsole.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 3assert { 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 53Para 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 50El código de tu aplicación aún puede importar macros desde node_modules e invocarlas.
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.
{
"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:
import pkg from "my-package"; // import de runtime
import { macro } from "my-package" with { type: "macro" }; // import de macroEl 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:
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.
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:
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.
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-Typey serializará en consecuencia; por ejemplo, una Response con tipoapplication/jsonse analizará automáticamente en un objeto ytext/plainse 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.
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.
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:
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:
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:
function howLong() {
console.log("La página tiene", 1322, "caracteres de largo");
}
export { howLong };Ejemplos
Incrustar hash del último commit de git
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:
import { getGitCommitHash } from "./getGitCommitHash.ts" with { type: "macro" };
console.log(`El hash del commit actual de Git es ${getGitCommitHash()}`);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.
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.
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>
);
};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>
);
};