Skip to content

Les macros sont un mécanisme pour exécuter des fonctions JavaScript au moment du bundle. La valeur retournée par ces fonctions est directement intégrée dans votre bundle.

À titre d'exemple simple, considérons cette fonction qui retourne un nombre aléatoire.

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

Ceci est juste une fonction régulière dans un fichier régulier, mais nous pouvons l'utiliser comme une macro comme ceci :

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

console.log(`Votre nombre aléatoire est ${random()}`);

NOTE

Les macros sont indiquées en utilisant la syntaxe d'attribut d'importation. Si vous n'avez pas vu cette syntaxe auparavant, c'est une proposition TC39 de stade 3 qui vous permet d'attacher des métadonnées supplémentaires aux instructions d'importation.

Maintenant, nous allons bundler ce fichier avec bun build. Le fichier bundle sera affiché sur stdout.

bash
bun build ./cli.tsx
js
console.log(`Votre nombre aléatoire est ${0.6805550949689833}`);

Comme vous pouvez le voir, le code source de la fonction random n'apparaît nulle part dans le bundle. Au lieu de cela, elle est exécutée pendant le bundling et l'appel de fonction (random()) est remplacé par le résultat de la fonction. Puisque le code source ne sera jamais inclus dans le bundle, les macros peuvent effectuer en toute sécurité des opérations privilégiées comme la lecture depuis une base de données.

Quand utiliser les macros

Si vous avez plusieurs scripts de construction pour de petites choses où vous auriez autrement un script de construction unique, l'exécution de code au moment du bundle peut être plus facile à maintenir. Cela vit avec le reste de votre code, cela s'exécute avec le reste de la construction, c'est automatiquement parallélisé, et si cela échoue, la construction échoue aussi.

Si vous vous retrouvez à exécuter beaucoup de code au moment du bundle, envisagez plutôt d'exécuter un serveur.

Attributs d'importation

Les macros Bun sont des instructions d'importation annotées en utilisant soit :

  • with { type: 'macro' } — un attribut d'importation, une proposition ECMA Script de stade 3
  • assert { type: 'macro' } — une assertion d'importation, une incarnation antérieure des attributs d'importation qui a maintenant été abandonnée (mais est déjà prise en charge par un certain nombre de navigateurs et de runtimes)

Considérations de sécurité

Les macros doivent être explicitement importées avec { type: "macro" } pour être exécutées au moment du bundle. Ces importations n'ont aucun effet si elles ne sont pas appelées, contrairement aux importations JavaScript régulières qui peuvent avoir des effets de bord.

Vous pouvez désactiver complètement les macros en passant l'option --no-macros à Bun. Cela produit une erreur de construction comme ceci :

error: Les macros sont désactivées

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

Pour réduire la surface d'attaque potentielle pour les packages malveillants, les macros ne peuvent pas être invoquées depuis l'intérieur de node_modules/**/*. Si un package tente d'invoquer une macro, vous verrez une erreur comme ceci :

error: Pour des raisons de sécurité, les macros ne peuvent pas être exécutées depuis node_modules.

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

Le code de votre application peut toujours importer des macros depuis node_modules et les invoquer.

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

macro();

Condition d'export "macro"

Lors de la distribution d'une bibliothèque contenant une macro vers npm ou un autre registre de packages, utilisez la condition d'export "macro" pour fournir une version spéciale de votre package exclusivement pour l'environnement de macro.

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

Avec cette configuration, les utilisateurs peuvent consommer votre package au moment de l'exécution ou au moment du bundle en utilisant le même spécificateur d'importation :

ts
import pkg from "my-package"; // importation au moment de l'exécution
import { macro } from "my-package" with { type: "macro" }; // importation de macro

La première importation se résoudra vers ./node_modules/my-package/index.js, tandis que la seconde sera résolue par le bundler de Bun vers ./node_modules/my-package/index.macro.js.

Exécution

Lorsque le transpileur de Bun voit une importation de macro, il appelle la fonction à l'intérieur du transpileur en utilisant le runtime JavaScript de Bun et convertit la valeur de retour de JavaScript en un nœud AST. Ces fonctions JavaScript sont appelées au moment du bundle, pas au moment de l'exécution.

Les macros sont exécutées de manière synchrone dans le transpileur pendant la phase de visite — avant les plugins et avant que le transpileur ne génère l'AST. Elles sont exécutées dans l'ordre où elles sont importées. Le transpileur attendra que la macro finisse de s'exécuter avant de continuer. Le transpileur attendra également toute Promise retournée par une macro.

Le bundler de Bun est multi-threadé. En tant que tel, les macros s'exécutent en parallèle à l'intérieur de plusieurs "workers" JavaScript spawnés.

Élimination du code mort

Le bundler effectue l'élimination du code mort après avoir exécuté et intégré les macros. Donc, étant donné la macro suivante :

ts
export function returnFalse() {
  return false;
}

... alors bundler le fichier suivant produira un bundle vide, à condition que l'option de minification de syntaxe soit activée.

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

if (returnFalse()) {
  console.log("Ce code est éliminé");
}

Sérialisabilité

Le transpileur de Bun doit être capable de sérialiser le résultat de la macro afin qu'il puisse être intégré dans l'AST. Toutes les structures de données compatibles JSON sont prises en charge :

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

Les macros peuvent être asynchrones ou retourner des instances de Promise. Le transpileur de Bun attendra automatiquement la Promise et intégrera le résultat.

ts
export async function getText() {
  return "valeur asynchrone";
}

Le transpileur implémente une logique spéciale pour sérialiser des formats de données courants comme Response, Blob, TypedArray.

  • TypedArray : Se résout en une chaîne encodée en base64.
  • Response : Bun lira le Content-Type et sérialisera en conséquence ; par exemple, une Response avec le type application/json sera automatiquement analysée en un objet et text/plain sera intégré en tant que chaîne. Les Responses avec un type non reconnu ou undefined seront encodées en base-64.
  • Blob : Comme pour Response, la sérialisation dépend de la propriété type.

Le résultat de fetch est Promise<Response>, donc il peut être directement retourné.

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

Les fonctions et instances de la plupart des classes (sauf celles mentionnées ci-dessus) ne sont pas sérialisables.

ts
export function getText(url: string) {
  // cela ne fonctionne pas !
  return () => {};
}

Arguments

Les macros peuvent accepter des entrées, mais uniquement dans des cas limités. La valeur doit être statiquement connue. Par exemple, ce qui suit n'est pas autorisé :

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

export function howLong() {
  // la valeur de `foo` ne peut pas être statiquement connue
  const foo = Math.random() ? "foo" : "bar";

  const text = getText(`https://example.com/${foo}`);
  console.log("La page fait ", text.length, " caractères de long");
}

Cependant, si la valeur de foo est connue au moment du bundle (disons, si c'est une constante ou le résultat d'une autre macro), alors c'est autorisé :

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

export function howLong() {
  // cela fonctionne car getFoo() est statiquement connu
  const foo = getFoo();
  const text = getText(`https://example.com/${foo}`);
  console.log("La page fait", text.length, "caractères de long");
}

Cela produit :

js
function howLong() {
  console.log("La page fait", 1322, "caractères de long");
}
export { howLong };

Exemples

Intégrer le hachage du dernier commit git

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

  return stdout.toString();
}

Lorsque nous le buildons, le getGitCommitHash est remplacé par le résultat de l'appel de la fonction :

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

console.log(`Le hachage du commit Git actuel est ${getGitCommitHash()}`);
ts
console.log(`Le hachage du commit Git actuel est 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);

Effectuer des requêtes fetch() au moment du bundle

Dans cet exemple, nous effectuons une requête HTTP sortante en utilisant fetch(), analysons la réponse HTML en utilisant HTMLRewriter, et retournons un objet contenant le titre et les balises meta — tout cela au moment du bundle.

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 fonction extractMetaTags est effacée au moment du bundle et remplacée par le résultat de l'appel de fonction. Cela signifie que la requête fetch se produit au moment du bundle, et le résultat est intégré dans le bundle. De plus, la branche lançant l'erreur est éliminée car elle est inaccessible.

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("Le titre doit être '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 édité par www.bunjs.com.cn