Skip to content

Макросы — это механизм для запуска JavaScript функций во время сборки. Значения возвращаемые этими функциями напрямую встраиваются в ваш бандл.

В качестве простого примера рассмотрим эту простую функцию которая возвращает случайное число.

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

Это просто обычная функция в обычном файле но мы можем использовать ее как макрос вот так:

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

console.log(`Ваше случайное число: ${random()}`);

NOTE

Макросы указываются с помощью синтаксиса атрибута импорта. Если вы раньше не видели этот синтаксис это предложение TC39 Stage 3 которое позволяет вам прикреплять дополнительные метаданные к операторам импорта.

Теперь мы свяжем этот файл с помощью bun build. Связанный файл будет выведен в stdout.

bash
bun build ./cli.tsx
js
console.log(`Ваше случайное число: ${0.6805550949689833}`);

Как вы можете видеть исходный код функции random нигде не встречается в бандле. Вместо этого она выполняется во время связывания и вызов функции (random()) заменяется результатом функции. Поскольку исходный код никогда не будет включен в бандл макросы могут безопасно выполнять привилегированные операции такие как чтение из базы данных.

Когда использовать макросы

Если у вас есть несколько скриптов сборки для небольших вещей где в противном случае у вас был бы одноразовый скрипт сборки выполнение кода во время сборки может быть проще в обслуживании. Оно живет с остальной частью вашего кода запускается с остальной частью сборки автоматически параллелизуется и если оно не удается сборка также не удается.

Если вы обнаружите что запускаете много кода во время сборки рассмотрите вместо этого запуск сервера.

Атрибуты импорта

Макросы Bun — это операторы импорта аннотированные с помощью:

  • with { type: 'macro' } — атрибут импорта предложение ECMA Script Stage 3
  • assert { type: 'macro' } — утверждение импорта более ранняя версия атрибутов импорта которая теперь отвергнута (но уже поддерживается рядом браузеров и сред выполнения)

Соображения безопасности

Макросы должны явно импортироваться с { type: "macro" } для выполнения во время сборки. Эти импорты не имеют эффекта если они не вызваны в отличие от обычных импортов JavaScript которые могут иметь побочные эффекты.

Вы можете полностью отключить макросы передав флаг --no-macros в Bun. Он выдаст ошибку сборки подобную этой:

error: Macros are disabled

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

Для уменьшения потенциальной поверхности атаки для вредоносных пакетов макросы не могут вызываться изнутри node_modules/**/*. Если пакет пытается вызвать макрос вы увидите ошибку подобную этой:

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

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

Код вашего приложения все еще может импортировать макросы из node_modules и вызывать их.

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

macro();

Условие экспорта "macro"

При поставке библиотеки содержащей макрос в npm или другой реестр пакетов используйте условие экспорта "macro" для предоставления специальной версии вашего пакета исключительно для среды макросов.

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

С этой конфигурацией пользователи могут использовать ваш пакет во время выполнения или во время сборки используя один и тот же спецификатор импорта:

ts
import pkg from "my-package"; // импорт времени выполнения
import { macro } from "my-package" with { type: "macro" }; // импорт макроса

Первый импорт будет разрешен в ./node_modules/my-package/index.js в то время как второй будет разрешен бандлером Bun в ./node_modules/my-package/index.macro.js.

Выполнение

Когда транспайлер Bun видит импорт макроса он вызывает функцию внутри транспайлера используя среду выполнения JavaScript от Bun и преобразует возвращаемое значение из JavaScript в узел AST. Эти JavaScript функции вызываются во время сборки а не во время выполнения.

Макросы выполняются синхронно в транспайлере во время фазы посещения — перед плагинами и перед тем как транспайлер генерирует AST. Они выполняются в порядке их импорта. Транспайлер будет ждать завершения выполнения макроса перед продолжением. Транспайлер также будет ожидать любое Promise возвращенное макросом.

Бандлер Bun многопоточный. Таким образом макросы выполняются параллельно внутри нескольких порожденных JavaScript "воркеров".

Устранение мертвого кода

Бандлер выполняет устранение мертвого кода после запуска и встраивания макросов. Таким образом учитывая следующий макрос:

ts
export function returnFalse() {
  return false;
}

...то связывание следующего файла даст пустой бандл при условии что включена опция минификации синтаксиса.

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

if (returnFalse()) {
  console.log("Этот код устранен");
}

Сериализуемость

Транспайлеру Bun нужно иметь возможность сериализовать результат макроса чтобы он мог быть встроен в AST. Поддерживаются все JSON-совместимые структуры данных:

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

Макросы могут быть асинхронными или возвращать экземпляры Promise. Транспайлер Bun автоматически ожидает Promise и встраивает результат.

ts
export async function getText() {
  return "асинхронное значение";
}

Транспайлер реализует специальную логику для сериализации распространенных форматов данных таких как Response Blob TypedArray.

  • TypedArray: Разрешается в base64-кодированную строку.
  • Response: Bun прочитает Content-Type и сериализует соответствующим образом; например Response с типом application/json будет автоматически разобран в объект а text/plain будет встроен как строка. Responses с нераспознанным или неопределенным типом будут base64-кодированы.
  • Blob: Как и с Response сериализация зависит от свойства type.

Результат fetch это Promise<Response> поэтому он может быть напрямую возвращен.

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

Функции и экземпляры большинства классов (кроме упомянутых выше) не сериализуемы.

ts
export function getText(url: string) {
  // это не работает!
  return () => {};
}

Аргументы

Макросы могут принимать входные данные но только в ограниченных случаях. Значение должно быть статически известно. Например следующее не разрешено:

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

export function howLong() {
  // значение `foo` не может быть статически известно
  const foo = Math.random() ? "foo" : "bar";

  const text = getText(`https://example.com/${foo}`);
  console.log("Страница имеет длину ", text.length, " символов");
}

Однако если значение foo известно во время сборки (скажем если это константа или результат другого макроса) то это разрешено:

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

export function howLong() {
  // это работает потому что getFoo() статически известно
  const foo = getFoo();
  const text = getText(`https://example.com/${foo}`);
  console.log("Страница имеет длину", text.length, "символов");
}

Это выводит:

js
function howLong() {
  console.log("Страница имеет длину", 1322, "символов");
}
export { howLong };

Примеры

Встраивание хеша последнего коммита git

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

  return stdout.toString();
}

Когда мы собираем это getGitCommitHash заменяется результатом вызова функции:

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

console.log(`Текущий хеш коммита Git: ${getGitCommitHash()}`);
ts
console.log(`Текущий хеш коммита Git: 3ee3259104e4507cf62c160f0ff5357ec4c7a7f8`);

Выполнение запросов fetch() во время сборки

В этом примере мы делаем исходящий HTTP-запрос используя fetch() разбираем HTML-ответ используя HTMLRewriter и возвращаем объект содержащий title и meta-теги — все во время сборки.

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

Функция extractMetaTags стирается во время сборки и заменяется результатом вызова функции. Это означает что fetch-запрос происходит во время сборки а результат встраивается в бандл. Также ветвь выбрасывающая ошибку устраняется поскольку она недостижима.

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("Ожидалось что title будет '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 от www.bunjs.com.cn