Передача данных между хуками через pluginData

Архитектура плагинов в Esbuild построена вокруг набора хуков, каждый из которых выполняет строго определённую роль в процессе сборки. При этом плагины могут состоять из нескольких хуков, и нередко возникает необходимость передавать контекст или промежуточные данные между ними в пределах одного плагина или даже между разными плагинами, работающими в одном процессе сборки.

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


Модель выполнения хуков и проблема контекста

В процессе работы Esbuild последовательно вызывает хуки:

  • onResolve — разрешение пути модуля
  • onLoad — загрузка содержимого файла
  • onTransform (через onLoad и resolve) — преобразование кода
  • onEnd — завершение сборки

Каждый из этих этапов может быть обработан разными плагинами. Внутри одного плагина часто требуется:

  • передать данные из onResolve в onLoad
  • сохранить информацию о модуле между вызовами
  • избежать повторного вычисления тяжёлых операций

Прямая передача данных невозможна, так как хуки независимы и могут выполняться параллельно.


Назначение pluginData

pluginData представляет собой механизм контекстной передачи произвольного объекта между хуками для конкретного модуля.

Ключевые свойства:

  • передаётся из onResolve в onLoad
  • не глобальный, а локальный для конкретного файла
  • может содержать любые сериализуемые данные
  • не влияет на другие модули

Передача данных из onResolve в onLoad

Основной сценарий использования — сохранение результата анализа пути или метаданных модуля на этапе разрешения.

Пример структуры передачи

import * as esbuild from 'esbuild';

const examplePlugin = {
  name: 'example-plugin',
  setup(build) {

    build.onResolve({ filter: /\.txt$/ }, (args) => {
      return {
        path: args.path,
        namespace: 'example-text',
        pluginData: {
          originalImport: args.importer,
          timestamp: Date.now(),
          isTextFile: true
        }
      };
    });

    build.onLoad({ filter: /.*/, namespace: 'example-text' }, (args) => {
      const data = args.pluginData;

      return {
        contents: `// imported from: ${data.originalImport}\n` +
                  `// loaded at: ${data.timestamp}\n` +
                  `export default "file content";`,
        loader: 'js'
      };
    });
  }
};

Поведение pluginData в цепочке хуков

Esbuild гарантирует, что:

  • объект pluginData, возвращённый из onResolve, передаётся в соответствующий onLoad
  • данные сохраняются только для конкретного пути
  • при повторном разрешении создаётся новый контекст

Важно понимать, что pluginData не является кешем и не хранится между разными файлами автоматически.


Изоляция данных между модулями

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

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

Пример:

build.onResolve({ filter: /\.data$/ }, (args) => {
  return {
    path: args.path,
    namespace: 'data-ns',
    pluginData: {
      id: Math.random()
    }
  };
});

Каждый импорт .data файла будет иметь уникальный id, и он не будет доступен другим модулям.


Ограничения pluginData

Несмотря на гибкость, механизм имеет ряд ограничений:

1. Потеря данных вне цепочки resolve → load

Если onLoad не вызывается для результата onResolve, данные теряются.

2. Отсутствие межплагинной глобальности

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

3. Не гарантируется сохранность сложных структур

Хотя объект может быть произвольным, на практике безопаснее использовать:

  • простые объекты
  • строки
  • числа
  • булевы значения

Использование для оптимизации вычислений

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

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

build.onResolve({ filter: /\.json$/ }, (args) => {
  const stats = fs.statSync(args.path);

  return {
    path: args.path,
    namespace: 'json-ns',
    pluginData: {
      size: stats.size,
      mtime: stats.mtimeMs
    }
  };
});
build.onLoad({ filter: /.*/, namespace: 'json-ns' }, (args) => {
  if (args.pluginData.size > 1024 * 100) {
    return {
      contents: 'export default {};',
      loader: 'js'
    };
  }

  const json = fs.readFileSync(args.path, 'utf8');

  return {
    contents: `export default ${json};`,
    loader: 'js'
  };
});

Здесь pluginData используется для принятия решения до чтения файла.


Передача метаданных трансформации

pluginData часто используется для хранения результатов анализа кода:

  • AST-метаданных
  • результатов парсинга
  • информации о зависимостях
  • маркеров оптимизации

Пример хранения анализа

build.onResolve({ filter: /\.js$/ }, (args) => {
  return {
    path: args.path,
    namespace: 'js-ns',
    pluginData: {
      importedBy: args.importer,
      depth: (args.pluginData?.depth || 0) + 1
    }
  };
});

Здесь создаётся цепочка глубины импорта, передающаяся через каждый уровень resolve.


Комбинация pluginData с namespace

pluginData часто используется вместе с namespace, что позволяет:

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

Пример:

build.onResolve({ filter: /\.svg$/ }, (args) => {
  return {
    path: args.path,
    namespace: 'svg-inline',
    pluginData: {
      inline: true,
      source: args.importer
    }
  };
});

Поведение при цепочке импортов

При каскадных импортax:

A → B → C

каждый переход может формировать собственный pluginData, который:

  • не перезаписывает предыдущий
  • не наследуется автоматически
  • существует только в рамках текущего шага

Это важно учитывать при построении сложных графов зависимостей.


Безопасное использование pluginData

Практика работы с Esbuild показывает несколько устойчивых подходов:

Минимизация объёма данных

pluginData должен содержать только:

  • флаги
  • краткие метаданные
  • результаты предварительных проверок

Избегание вложенных структур

Глубоко вложенные объекты увеличивают риск:

  • потери данных
  • сложности отладки
  • неопределённого поведения при сериализации

Отсутствие побочных эффектов

pluginData не должен использоваться для:

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

Роль pluginData в архитектуре плагинов

Механизм выполняет роль локального канала передачи данных между фазами обработки одного и того же модуля. Он позволяет:

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

Практический паттерн: двухфазная обработка

Наиболее распространённая архитектура:

Фаза 1 — onResolve

  • анализ пути
  • определение namespace
  • вычисление метаданных
  • упаковка контекста в pluginData

Фаза 2 — onLoad

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

Ошибки при использовании pluginData

Потеря данных из-за неправильного namespace

Если onLoad не совпадает по namespace, данные будут недоступны.

Игнорирование передачи pluginData

Если вернуть объект без pluginData из onResolve, данные не попадут в следующий хук.

Попытка использовать как глобальный стор

Приводит к нарушению модели Esbuild и нестабильному поведению сборки.


Итоговая модель поведения

pluginData можно рассматривать как:

  • локальный пакет данных на один модуль
  • одноразовый контекст между resolve и load
  • механизм передачи промежуточных вычислений без глобального состояния

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