Skip to content

Macros são um mecanismo para executar funções JavaScript em tempo de bundle. Os valores retornados destas funções são diretamente inline no seu bundle.

Como um exemplo simples, considere esta função simples que retorna um número aleatório.

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

Esta é apenas uma função regular em um arquivo regular, mas podemos usá-la como uma macro assim:

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

console.log(`Your random number is ${random()}`);

NOTE

Macros são indicadas usando sintaxe de atributo de import. Se você não viu esta sintaxe antes, é uma proposta Stage 3 TC39 que permite anexar metadados adicionais a declarações de import.

Agora vamos fazer bundle deste arquivo com bun build. O arquivo em bundle será impresso no stdout.

bash
bun build ./cli.tsx
js
console.log(`Your random number is ${0.6805550949689833}`);

Como você pode ver, o código fonte da função random não ocorre em lugar nenhum no bundle. Em vez disso, ela é executada durante o bundling e a chamada de função (random()) é substituída pelo resultado da função. Como o código fonte nunca será incluído no bundle, macros podem realizar com segurança operações privilegiadas como ler de um banco de dados.

Quando usar macros

Se você tem vários scripts de build para pequenas coisas onde você teria caso contrário um script de build one-off, execução de código em tempo de bundle pode ser mais fácil de manter. Ele vive com o resto do seu código, executa com o resto do build, é automaticamente paralelizado, e se falhar, o build falha também.

Se você se encontrar executando muito código em tempo de bundle, considere executar um servidor em vez disso.

Atributos de import

Bun Macros são declarações de import anotadas usando:

  • with { type: 'macro' } — um atributo de import, uma proposta Stage 3 ECMA Script
  • assert { type: 'macro' } — uma asserção de import, uma encarnação anterior de atributos de import que foi abandonada (mas já é suportada por vários navegadores e runtimes)

Considerações de segurança

Macros devem ser explicitamente importadas com { type: "macro" } para serem executadas em tempo de bundle. Estes imports não têm efeito se não forem chamados, diferentemente de imports JavaScript regulares que podem ter side effects.

Você pode desabilitar macros inteiramente passando a flag --no-macros para o Bun. Isso produz um erro de build como este:

error: Macros are disabled

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

Para reduzir a superfície de ataque potencial para pacotes maliciosos, macros não podem ser invocadas de dentro de node_modules/**/*. Se um pacote tentar invocar uma macro, você verá um erro como este:

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

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

Seu código de aplicação ainda pode importar macros de node_modules e invocá-las.

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

macro();

Condição de export "macro"

Ao distribuir uma biblioteca contendo uma macro para npm ou outro registry de pacotes, use a condição de export "macro" para fornecer uma versão especial do seu pacote exclusivamente para o ambiente macro.

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

Com esta configuração, usuários podem consumir seu pacote em runtime ou em tempo de bundle usando o mesmo import specifier:

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

O primeiro import resolverá para ./node_modules/my-package/index.js, enquanto o segundo será resolvido pelo bundler do Bun para ./node_modules/my-package/index.macro.js.

Execução

Quando o transpiler do Bun vê um import de macro, ele chama a função dentro do transpiler usando o runtime JavaScript do Bun e converte o valor de retorno de JavaScript para um nó AST. Estas funções JavaScript são chamadas em tempo de bundle, não em runtime.

Macros são executadas sincronamente no transpiler durante a fase de visita—antes dos plugins e antes do transpiler gerar o AST. Elas são executadas na ordem em que são importadas. O transpiler esperará a macro terminar de executar antes de continuar. O transpiler também aguardará qualquer Promise retornada por uma macro.

O bundler do Bun é multi-threaded. Como tal, macros executam em paralelo dentro de múltiplos "workers" JavaScript spawned.

Dead code elimination

O bundler realiza dead code elimination após executar e inline macros. Então dado a seguinte macro:

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

...então fazer bundle do seguinte arquivo produzirá um bundle vazio, desde que a opção de minify syntax esteja habilitada.

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

if (returnFalse()) {
  console.log("This code is eliminated");
}

Serializabilidade

O transpiler do Bun precisa ser capaz de serializar o resultado da macro para que possa ser inline no AST. Todas as estruturas de dados compatíveis com JSON são suportadas:

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

Macros podem ser async ou retornar instâncias de Promise. O transpiler do Bun automaticamente aguardará a Promise e inline o resultado.

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

O transpiler implementa lógica especial para serializar formatos de dados comuns como Response, Blob, TypedArray.

  • TypedArray: Resolve para uma string base64-encoded.
  • Response: O Bun lerá o Content-Type e serializará conforme apropriado; por exemplo, uma Response com tipo application/json será automaticamente analisada para um objeto e text/plain será inline como uma string. Responses com tipo não reconhecido ou undefined serão base-64 encoded.
  • Blob: Como com Response, a serialização depende da propriedade type.

O resultado de fetch é Promise<Response>, então pode ser diretamente retornado.

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

Funções e instâncias da maioria das classes (exceto aquelas mencionadas acima) não são serializáveis.

macro.ts
ts
export function getText(url: string) {
  // isso não funciona!
  return () => {};
}

Argumentos

Macros podem aceitar inputs, mas apenas em casos limitados. O valor deve ser estaticamente conhecido. Por exemplo, o seguinte não é permitido:

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

export function howLong() {
  // o valor de `foo` não pode ser estaticamente conhecido
  const foo = Math.random() ? "foo" : "bar";

  const text = getText(`https://example.com/${foo}`);
  console.log("The page is ", text.length, " characters long");
}

No entanto, se o valor de foo é conhecido em tempo de bundle (digamos, se é uma constante ou resultado de outra macro) então é permitido:

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

export function howLong() {
  // isso funciona porque getFoo() é estaticamente conhecido
  const foo = getFoo();
  const text = getText(`https://example.com/${foo}`);
  console.log("The page is", text.length, "characters long");
}

Isso produz:

js
function howLong() {
  console.log("The page is", 1322, "characters long");
}
export { howLong };

Exemplos

Embed hash do último commit git

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

  return stdout.toString();
}

Quando fazemos build, o getGitCommitHash é substituído pelo resultado de chamar a função:

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

console.log(`The current Git commit hash is ${getGitCommitHash()}`);
ts
console.log(`The current Git commit hash is 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);

Fazer requisições fetch() em tempo de bundle

Neste exemplo, fazemos uma requisição HTTP de saída usando fetch(), analisamos a resposta HTML usando HTMLRewriter, e retornamos um objeto contendo título e meta tags—tudo em tempo de bundle.

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

A função extractMetaTags é apagada em tempo de bundle e substituída pelo resultado da chamada de função. Isso significa que a requisição fetch acontece em tempo de bundle, e o resultado é embed no bundle. Além disso, o branch lançando o erro é eliminado já que é inalcançável.

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("Expected title to be '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 by www.bunjs.com.cn edit