Skip to content

Le macro sono un meccanismo per eseguire funzioni JavaScript a bundle-time. Il valore restituito da queste funzioni è direttamente inlinato nel tuo bundle.

Come esempio giocattolo, considera questa semplice funzione che restituisce un numero casuale.

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

Questa è solo una funzione regolare in un file regolare, ma possiamo usarla come macro così:

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

console.log(`Il tuo numero casuale è ${random()}`);

NOTE

Le macro sono indicate usando la sintassi degli attributi di import. Se non hai mai visto questa sintassi prima, è una proposta TC39 Stage 3 che ti permette di allegare metadati aggiuntivi agli statement di import.

Ora bundleremo questo file con bun build. Il file bundled sarà stampato a stdout.

bash
bun build ./cli.tsx
js
console.log(`Il tuo numero casuale è ${0.6805550949689833}`);

Come puoi vedere, il codice sorgente della funzione random non appare da nessuna parte nel bundle. Invece, è eseguita durante il bundling e la chiamata alla funzione (random()) è rimpiazzata con il risultato della funzione. Poiché il codice sorgente non sarà mai incluso nel bundle, le macro possono eseguire in sicurezza operazioni privilegiate come leggere da un database.

Quando usare le macro

Se hai diversi script di build per piccole cose dove altrimenti avresti uno script di build one-off, l'esecuzione di codice a bundle-time può essere più facile da mantenere. Vive con il resto del tuo codice, esegue con il resto della build, è automaticamente parallelizzata, e se fallisce, anche la build fallisce.

Se ti trovi a eseguire molto codice a bundle-time però, considera di eseguire invece un server.

Attributi di import

Le macro di Bun sono statement di import annotati usando:

  • with { type: 'macro' } — un attributo di import, una proposta ECMA Script Stage 3
  • assert { type: 'macro' } — un'asserzione di import, un'incarnazione precedente degli attributi di import che è ora abbandonata (ma è già supportata da diversi browser e runtime)

Considerazioni di sicurezza

Le macro devono essere esplicitamente importate con { type: "macro" } per essere eseguite a bundle-time. Questi import non hanno effetto se non sono chiamati, a differenza dei normali import JavaScript che possono avere side effects.

Puoi disabilitare completamente le macro passando il flag --no-macros a Bun. Produce un errore di build come questo:

error: Le macro sono disabilitate

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

Per ridurre la potenziale superficie di attacco per pacchetti maligni, le macro non possono essere invocate da dentro node_modules/**/*. Se un pacchetto tenta di invocare una macro, vedrai un errore come questo:

error: Per ragioni di sicurezza, le macro non possono essere eseguite da node_modules.

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

Il tuo codice applicazione può ancora importare macro da node_modules e invocarle.

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

macro();

Condizione di export "macro"

Quando spedisci una libreria contenente una macro a npm o un altro registry di pacchetti, usa la condizione di export "macro" per fornire una versione speciale del tuo pacchetto esclusivamente per l'ambiente macro.

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

Con questa configurazione, gli utenti possono consumare il tuo pacchetto a runtime o a bundle-time usando lo stesso import specifier:

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

Il primo import risolverà a ./node_modules/my-package/index.js, mentre il secondo sarà risolto dal bundler di Bun a ./node_modules/my-package/index.macro.js.

Esecuzione

Quando il transpiler di Bun vede un import macro, chiama la funzione dentro il transpiler usando il runtime JavaScript di Bun e converte il valore di ritorno da JavaScript in un nodo AST. Queste funzioni JavaScript sono chiamate a bundle-time, non a runtime.

Le macro sono eseguite sincronamente nel transpiler durante la fase di visiting—prima dei plugin e prima che il transpiler generi l'AST. Sono eseguite nell'ordine in cui sono importate. Il transpiler aspetterà che la macro finisca di eseguire prima di continuare. Il transpiler awaitterà anche qualsiasi Promise restituita da una macro.

Il bundler di Bun è multi-threaded. Come tale, le macro eseguono in parallelo dentro multiple "workers" JavaScript spawnate.

Dead code elimination

Il bundler esegue dead code elimination dopo aver eseguito e inlinato le macro. Quindi data la seguente macro:

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

...allora bundlare il seguente file produrrà un bundle vuoto, a patto che l'opzione di minificazione syntax sia abilitata.

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

if (returnFalse()) {
  console.log("Questo codice è eliminato");
}

Serializzabilità

Il transpiler di Bun deve essere in grado di serializzare il risultato della macro così che possa essere inlinato nell'AST. Tutte le strutture dati compatibili JSON sono supportate:

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

Le macro possono essere async, o restituire istanze Promise. Il transpiler di Bun awaitterà automaticamente la Promise e inlinerà il risultato.

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

Il transpiler implementa logica speciale per serializzare formati dati comuni come Response, Blob, TypedArray.

  • TypedArray: Risolve a una stringa codificata base64.
  • Response: Bun leggerà il Content-Type e serializzerà di conseguenza; per istanza, una Response con tipo application/json sarà automaticamente parsata in un oggetto e text/plain sarà inlinato come stringa. Response con un tipo non riconosciuto o undefined saranno codificate base-64.
  • Blob: Come con Response, la serializzazione dipende dalla proprietà type.

Il risultato di fetch è Promise<Response>, quindi può essere direttamente restituito.

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

Funzioni e istanze della maggior parte delle classi (eccetto quelle menzionate sopra) non sono serializzabili.

macro.ts
ts
export function getText(url: string) {
  // questo non funziona!
  return () => {};
}

Argomenti

Le macro possono accettare input, ma solo in casi limitati. Il valore deve essere staticamente conosciuto. Per esempio, quanto segue non è permesso:

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

export function howLong() {
  // il valore di `foo` non può essere staticamente conosciuto
  const foo = Math.random() ? "foo" : "bar";

  const text = getText(`https://example.com/${foo}`);
  console.log("La pagina è lunga ", text.length, " caratteri");
}

Tuttavia, se il valore di foo è conosciuto a bundle-time (diciamo, se è una costante o il risultato di un'altra macro) allora è permesso:

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

export function howLong() {
  // questo funziona perché getFoo() è staticamente conosciuto
  const foo = getFoo();
  const text = getText(`https://example.com/${foo}`);
  console.log("La pagina è lunga", text.length, "caratteri");
}

Questo outputta:

js
function howLong() {
  console.log("La pagina è lunga", 1322, "caratteri");
}
export { howLong };

Esempi

Embedda l'hash dell'ultimo commit git

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

  return stdout.toString();
}

Quando lo buildiamo, il getGitCommitHash è rimpiazzato con il risultato della chiamata alla funzione:

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

console.log(`L'hash del commit Git corrente è ${getGitCommitHash()}`);
ts
console.log(`L'hash del commit Git corrente è 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);

Esegui richieste fetch() a bundle-time

In questo esempio, eseguiamo una richiesta HTTP in uscita usando fetch(), parsiamo la risposta HTML usando HTMLRewriter, e restituiamo un oggetto contenente il titolo e i meta tag—tutto a bundle-time.

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 funzione extractMetaTags è cancellata a bundle-time e rimpiazzata con il risultato della chiamata alla funzione. Questo significa che la richiesta fetch avviene a bundle-time, e il risultato è embeddato nel bundle. Inoltre, il branch che lancia l'errore è eliminato poiché è irraggiungibile.

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("Ci si aspettava che il titolo fosse '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 a cura di www.bunjs.com.cn