esbuild.context(): управление жизненным циклом

Контекст как основная единица управления сборкой

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

Контекст устраняет необходимость вручную управлять:

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

Основная идея заключается в том, что сборка превращается в объект с жизненным циклом, а не в разовую операцию.


Создание контекста и базовая структура

Контекст создаётся через асинхронный вызов:

import * as esbuild from 'esbuild';

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

Возвращаемое значение — объект контекста, содержащий методы управления:

  • ctx.rebuild() — ручной запуск сборки;
  • ctx.watch() — включение режима наблюдения за файлами;
  • ctx.serve() — встроенный dev-сервер;
  • ctx.dispose() — завершение жизненного цикла.

Контекст существует в памяти процесса и удерживает внутренние структуры esbuild до явного освобождения.


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

Жизненный цикл можно разделить на несколько фаз:

1. Инициализация

При вызове esbuild.context() происходит:

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

На этом этапе сборка ещё не выполняется автоматически.


2. Первичная сборка

Первый вызов происходит либо явно через rebuild(), либо автоматически при watch()/serve().

await ctx.rebuild();

В этот момент:

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

3. Состояние ожидания изменений

После первичной сборки контекст может находиться в одном из режимов:

  • пассивный (только rebuild);
  • активный watch;
  • серверный режим serve.

Контекст сохраняет:

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

4. Реактивные пересборки

При изменении файлов (в режиме watch):

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

Ключевая особенность: esbuild не пересобирает всё приложение, а пересчитывает только изменённые ветки графа.


5. Завершение жизненного цикла

Завершение работы выполняется через:

await ctx.dispose();

Это критически важный этап управления ресурсами:

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

Игнорирование dispose() приводит к утечкам ресурсов в долгоживущих процессах (dev-server, CLI watchers, тестовые среды).


Режим watch и управление пересборками

Режим наблюдения включается явно:

await ctx.watch();

После активации:

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

Особенность архитектуры заключается в том, что watch не является «надстройкой» над build, а встроен в контекстный слой.


Поведение при изменениях

При изменении файла происходит следующая цепочка:

  1. Событие файловой системы;
  2. Определение затронутых модулей;
  3. Инвалидация кеша конкретных узлов графа;
  4. Частичная пересборка;
  5. Запись новых выходных файлов.

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


Ручной rebuild как часть жизненного цикла

Метод rebuild() используется в сценариях:

  • интеграция с внешними системами наблюдения;
  • тестовые окружения;
  • CI-процессы с повторными сборками;
  • кастомные dev-серверы.
const result = await ctx.rebuild();

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

Особенность rebuild() в контексте — он использует уже созданный граф зависимостей, а не строит его заново.


Управление состоянием через serve()

Метод serve() объединяет сборку и HTTP-сервер:

await ctx.serve({
  port: 3000,
  servedir: 'public'
});

В этом режиме:

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

Архитектурно это расширение жизненного цикла контекста до уровня runtime-сервиса.


Инкрементальность и кеширование

Контекст esbuild опирается на несколько уровней кеша:

Кеш модулей

Хранит:

  • результаты парсинга;
  • AST-подобные структуры;
  • метаданные импортов.

Кеш трансформаций

Содержит:

  • результаты JSX/TS трансформаций;
  • обработанные плагины;
  • результаты minify (если включено).

Кеш разрешения зависимостей

Фиксирует:

  • пути модулей;
  • алиасы;
  • результат plugin-resolve.

Эти кеши живут внутри контекста и сбрасываются только при dispose().


Взаимодействие с плагинами в жизненном цикле

Плагины в контексте работают в рамках долгоживущего процесса.

const ctx = await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  plugins: [
    {
      name: 'logger',
      setup(build) {
        build.onStart(() => {
          console.log('build started');
        });

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

Особенности:

  • onStart вызывается перед каждой пересборкой;
  • onEnd вызывается после завершения;
  • состояние плагинов сохраняется между rebuild.

Это превращает плагины в часть реактивного жизненного цикла, а не разовой обработки.


Конкурентность и очереди сборок

Контекст сериализует сборки:

  • одновременно выполняется только одна операция build/rebuild;
  • новые события ставятся в очередь;
  • промежуточные состояния не публикуются.

Это исключает race conditions при частых изменениях файлов.


Управление памятью и устойчивость процесса

Контекст хранит значительный объём данных в памяти. Основные источники:

  • кеш модулей;
  • кеш трансформаций;
  • граф зависимостей;
  • plugin state.

Поэтому жизненный цикл должен быть строго ограничен:

  • долгоживущие dev-серверы используют один контекст;
  • тестовые запуски создают и уничтожают контекст на каждый suite;
  • CLI-инструменты обязаны вызывать dispose() после завершения.

Пересоздание контекста

В некоторых сценариях требуется полное обновление конфигурации:

  • изменение entryPoints;
  • смена plugin-цепочки;
  • изменение target/platform.

В таких случаях:

await ctx.dispose();

const newCtx = await esbuild.context({
  entryPoints: ['src/app.js'],
  bundle: true,
  platform: 'browser'
});

Пересоздание дешевле и предсказуемее, чем попытка мутировать существующий контекст.


Типичные ошибки управления жизненным циклом

Утечка контекста

Происходит при отсутствии dispose() в:

  • тестах;
  • скриптах сборки;
  • временных dev-серверах.

Дублирование watch

Запуск watch() без завершения предыдущего контекста приводит к:

  • множественным файловым наблюдателям;
  • повторным rebuild;
  • росту потребления памяти.

Неконтролируемые rebuild-события

Частые изменения файлов без debounce на уровне внешней системы могут перегружать очередь сборки.


Контекст как фундамент архитектуры сборки

esbuild.context() формирует модель, в которой:

  • сборка становится состоянием;
  • файловая система становится источником событий;
  • кеш становится постоянной памятью процесса;
  • rebuild становится реакцией, а не командой.

Такой подход превращает esbuild из инструмента компиляции в управляемую runtime-систему сборки с чётко определённым жизненным циклом.