Context API: esbuild.context()

До появления Context API типичный сценарий работы с esbuild выглядел как последовательные вызовы build(). Каждый новый запуск сборки создавал отдельный процесс сборки, заново анализировал граф зависимостей и выполнял все этапы компиляции.

Для задач разработки такой подход не всегда эффективен. Необходимо поддерживать:

  • пересборку проекта при изменении файлов;
  • запуск локального веб-сервера;
  • механизм Live Reload;
  • повторное использование внутреннего состояния сборщика;
  • быстрое выполнение последовательных сборок.

Для решения этих задач в esbuild был реализован Context API, центральным элементом которого является функция esbuild.context().

Она создаёт специальный объект контекста сборки, внутри которого хранится состояние проекта, кэшированные данные и параметры сборки. После создания контекста можно многократно выполнять пересборки, запускать сервер разработки и отслеживать изменения файлов без повторной инициализации всей системы сборки.


Базовый синтаксис

Создание контекста выглядит следующим образом:

import * as esbuild from 'esbuild';

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js'
});

Метод возвращает объект контекста:

const ctx = await esbuild.context(options);

Где:

  • options — обычные параметры сборки esbuild;
  • ctx — экземпляр контекста.

После создания контекста становятся доступны дополнительные методы управления жизненным циклом сборки.


Отличие от build()

Обычная сборка:

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js'
});

Сборка через контекст:

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js'
});

await ctx.rebuild();

Ключевое отличие состоит в том, что:

  • build() выполняет единичную сборку;
  • context() создаёт долговременный объект для многократных операций.

Контекст особенно полезен во время разработки, когда сборка выполняется десятки или сотни раз.


Жизненный цикл контекста

Работа с Context API обычно состоит из четырёх этапов:

  1. Создание контекста.
  2. Выполнение сборки.
  3. Повторные пересборки или запуск режима наблюдения.
  4. Уничтожение контекста.

Схема выглядит следующим образом:

context()
    ↓
rebuild()
    ↓
watch()
    ↓
serve()
    ↓
dispose()

Необязательно использовать все этапы одновременно.


Первичная сборка через rebuild()

После создания контекста сборка автоматически не запускается.

Необходимо вызвать метод:

await ctx.rebuild();

Полный пример:

import * as esbuild from 'esbuild';

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js'
});

await ctx.rebuild();

Такое поведение отличается от build(), где сборка начинается сразу после вызова функции.


Метод rebuild()

Назначение

Метод rebuild() запускает сборку повторно, используя уже существующий контекст.

await ctx.rebuild();

Преимущества:

  • повторное использование графа зависимостей;
  • использование внутренних кэшей;
  • сокращение времени сборки;
  • уменьшение нагрузки на файловую систему.

Многократные пересборки

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js'
});

await ctx.rebuild();
await ctx.rebuild();
await ctx.rebuild();

Каждая последующая сборка обычно выполняется быстрее первой.


Получение результата сборки

const result = await ctx.rebuild();

console.log(result.errors);
console.log(result.warnings);

Объект результата аналогичен возвращаемому значению build().


Режим наблюдения через watch()

Одним из наиболее распространённых сценариев использования Context API является автоматическая пересборка проекта при изменении файлов.

Для этого используется метод:

await ctx.watch();

Пример:

import * as esbuild from 'esbuild';

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js'
});

await ctx.watch();

После запуска наблюдения esbuild:

  1. отслеживает изменения файлов;
  2. автоматически инициирует пересборку;
  3. обновляет выходные файлы.

Как работает watch()

При изменении файла:

src/index.js

происходит следующий процесс:

Изменение файла
      ↓
Обнаружение события
      ↓
Пересборка
      ↓
Обновление output-файлов

При этом разработчику не требуется вручную запускать команду сборки.


Настройка уведомлений через плагины

Сам метод watch() не предоставляет обработчиков событий.

Для логирования часто используются плагины:

const logPlugin = {
  name: 'logger',
  setup(build) {
    build.onEnd(result => {
      console.log(
        `Build completed with ${result.errors.length} errors`
      );
    });
  }
};

Подключение:

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js',
  plugins: [logPlugin]
});

await ctx.watch();

Использование serve()

Контекст может запускать встроенный HTTP-сервер.

Метод:

await ctx.serve();

Пример:

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist'
});

const server = await ctx.serve({
  servedir: 'dist'
});

Результат:

console.log(server.host);
console.log(server.port);

Возвращаемый объект сервера

После запуска сервера возвращается объект:

{
  hosts: [...],
  port: 8000
}

Пример:

const result = await ctx.serve({
  servedir: 'dist'
});

console.log(result.port);

Можно получить порт и использовать его в дополнительных инструментах автоматизации.


Совмещение watch() и serve()

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

import * as esbuild from 'esbuild';

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist'
});

await ctx.watch();

await ctx.serve({
  servedir: 'dist'
});

В результате:

  • сервер раздаёт файлы;
  • esbuild следит за изменениями;
  • проект автоматически пересобирается.

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


Метод dispose()

После завершения работы контекст рекомендуется уничтожать.

Для этого используется метод:

await ctx.dispose();

Пример:

await ctx.dispose();

Метод выполняет:

  • остановку наблюдателей файловой системы;
  • освобождение памяти;
  • завершение внутренних процессов;
  • очистку кэшей контекста.

Почему важно вызывать dispose()

Без корректного завершения могут возникать:

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

Корректный шаблон:

const ctx = await esbuild.context(options);

try {
  await ctx.watch();
}
finally {
  await ctx.dispose();
}

Использование с обработкой сигналов процесса

Для CLI-инструментов часто используется перехват системных сигналов.

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js'
});

await ctx.watch();

process.on('SIGINT', async () => {
  await ctx.dispose();
  process.exit(0);
});

При нажатии Ctrl+C ресурсы будут освобождены корректно.


Комбинирование с плагинами

Контекст полностью совместим с системой плагинов esbuild.

const plugin = {
  name: 'example',
  setup(build) {
    build.onStart(() => {
      console.log('Build started');
    });

    build.onEnd(() => {
      console.log('Build finished');
    });
  }
};

Использование:

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js',
  plugins: [plugin]
});

Все пересборки будут автоматически использовать подключённые плагины.


Context API и производительность

Основное преимущество Context API связано с повторным использованием уже построенного состояния проекта.

Во время первой сборки esbuild:

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

При использовании rebuild() значительная часть этой информации уже находится в памяти.

Упрощённо процесс выглядит так:

Первая сборка
  ↓
Создание графа зависимостей
  ↓
Кэширование

Изменение файла
  ↓
Повторный анализ только изменённых частей
  ↓
Быстрая пересборка

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


Типичный сценарий создания dev-сервера

import * as esbuild from 'esbuild';

async function start() {
  const ctx = await esbuild.context({
    entryPoints: ['src/index.js'],
    bundle: true,
    sourcemap: true,
    outdir: 'dist'
  });

  await ctx.watch();

  const server = await ctx.serve({
    servedir: 'dist'
  });

  console.log(
    `Server running on http://localhost:${server.port}`
  );
}

start();

Такой сценарий обеспечивает:

  • сборку проекта;
  • генерацию Source Maps;
  • отслеживание изменений;
  • автоматическую пересборку;
  • запуск локального сервера.

Ограничения Context API

Невозможность изменения конфигурации

После создания контекста параметры сборки становятся фиксированными.

Нельзя выполнить:

const ctx = await esbuild.context({
  outfile: 'a.js'
});

а затем изменить:

ctx.outfile = 'b.js';

Для новой конфигурации требуется новый контекст.


Один контекст — одна конфигурация

Если необходимо собирать проект в разных режимах:

development
production
library

создаются отдельные контексты:

const devCtx = await esbuild.context(devConfig);

const prodCtx = await esbuild.context(prodConfig);

Сравнение методов

Возможность build() context()
Разовая сборка Да Через rebuild()
Пересборка Нет Да
Watch Mode Нет Да
Встроенный сервер Нет Да
Повторное использование кэша Нет Да
Управление жизненным циклом Ограничено Полное
Подходит для dev-среды Частично Да

Когда использовать build()

Подходит для:

  • CI/CD-пайплайнов;
  • production-сборок;
  • одноразовой компиляции;
  • генерации артефактов перед публикацией.

Пример:

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: true,
  outfile: 'dist/app.js'
});

Когда использовать context()

Подходит для:

  • локальной разработки;
  • автоматических пересборок;
  • инструментов Hot Reload;
  • dev-серверов;
  • собственных CLI-систем;
  • сложных инструментов сборки поверх esbuild.

Пример типичной структуры:

const ctx = await esbuild.context(config);

await ctx.watch();

await ctx.serve({
  servedir: 'dist'
});

В современной экосистеме esbuild именно esbuild.context() является основой для создания производительных сред разработки, поскольку объединяет управление сборкой, отслеживание изменений, встроенный сервер и механизмы повторного использования внутренних кэшей в едином жизненном цикле приложения.