Архитектура плагинной системы Rollup

Архитектура Rollup построена вокруг расширяемого пайплайна, в котором почти вся логика сборки делегируется плагинам. Сам бандлер выступает как координатор этапов: разрешение модулей, их загрузка, трансформация, анализ зависимостей, генерация выходного кода и постобработка. Плагины подключаются как набор объектов с заранее определёнными хуками, через которые они «вклиниваются» в процесс сборки.

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

Структура плагина

Плагин Rollup — это объект, содержащий набор функций-хуков и метаданных:

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

Минимальный пример:

export default function myPlugin() {
  return {
    name: 'my-plugin',
    resolveId(source) {
      return null;
    },
    load(id) {
      return null;
    },
    transform(code, id) {
      return null;
    }
  };
}

Каждый хук имеет строго определённое место в pipeline и влияет на разные стадии обработки модуля.

Классификация хуков

Архитектура плагинов делит хуки на несколько категорий:

1. Build hooks (хуки этапа построения графа)

Эти хуки участвуют в формировании module graph:

  • options
  • buildStart
  • resolveId
  • load
  • transform
  • moduleParsed

Именно они определяют, какие модули попадут в бандл и как они будут интерпретированы.

2. Output hooks (хуки генерации вывода)

Используются при формировании итогового бандла:

  • renderStart
  • banner
  • intro
  • augmentChunkHash
  • renderChunk
  • generateBundle
  • writeBundle
  • closeBundle

Эти хуки работают уже после построения графа и не влияют на структуру зависимостей.

Жизненный цикл плагинов

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

На этапе инициализации вызывается options. Он позволяет модифицировать конфигурацию Rollup до начала сборки.

options(inputOptions) {
  return inputOptions;
}

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

Далее вызывается buildStart, который сигнализирует о начале сборки. Он не влияет на граф напрямую, но может инициировать побочные операции: загрузку данных, кеширование, логирование.

Разрешение модулей: resolveId

Хук resolveId — один из ключевых элементов архитектуры. Он отвечает за преобразование строковых импортов в реальные идентификаторы модулей.

resolveId(source, importer, options) {
  if (source === 'virtual-module') {
    return source;
  }
  return null;
}

Если хук возвращает null, Rollup передаёт управление следующему плагину. Если возвращается строка — это становится финальным ID модуля.

Особенности архитектуры:

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

resolveId формирует основу графа зависимостей, поэтому любые ошибки на этом уровне влияют на всю сборку.

Загрузка модулей: load

После разрешения ID вызывается load. Он отвечает за получение исходного кода модуля.

load(id) {
  if (id === 'virtual-module') {
    return 'export const a = 1;';
  }
  return null;
}

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

  • может возвращать строку кода или null
  • используется для виртуальных модулей
  • может обходить файловую систему
  • часто работает вместе с resolveId

Если load возвращает null, Rollup читает файл с диска.

Трансформация кода: transform

Хук transform — основной механизм изменения кода модулей.

transform(code, id) {
  return {
    code: code.replace('var', 'let'),
    map: null
  };
}

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

Важные характеристики transform:

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

Архитектурно transform формирует «слои» обработки, где каждый плагин может модифицировать результат предыдущего.

moduleParsed и анализ графа

После трансформации модуль анализируется Rollup’ом (AST-парсинг). Затем вызывается moduleParsed.

moduleParsed(moduleInfo) {
  // доступ к импортам и экспортам
}

Этот хук не изменяет код, но предоставляет доступ к структуре модуля:

  • импорты
  • экспорты
  • динамические зависимости
  • AST-метаданные

Он используется для аналитики и построения дополнительных индексов.

Порядок выполнения плагинов

Архитектура Rollup строго определяет порядок:

Для resolveId:

  • вызывается слева направо
  • первый ненулевой результат останавливает цепочку

Для transform:

  • вызывается слева направо
  • результат передаётся следующему плагину

Для output-хуков:

  • часто вызываются в обратном порядке (справа налево)

Такой порядок позволяет комбинировать плагины с предсказуемым поведением.

Контекст выполнения плагинов

Каждый хук получает контекст this, содержащий API Rollup:

  • this.emitFile
  • this.resolve
  • this.getModuleInfo
  • this.addWatchFile
  • this.cache

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

Пример:

transform(code) {
  this.addWatchFile('config.json');
  return null;
}

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

Виртуальные модули

Архитектура плагинов активно использует концепцию виртуальных модулей. Это модули, которые не существуют на диске, но полностью интегрируются в граф.

Обычно реализуются через связку:

  • resolveId возвращает виртуальный ID
  • load возвращает код

Это позволяет:

  • генерировать код на лету
  • инкапсулировать runtime-логику
  • создавать псевдо-файловую структуру

Output-плагины и генерация бандла

После построения графа активируются output-хуки.

renderChunk

Позволяет модифицировать отдельные чанки:

renderChunk(code, chunk) {
  return code;
}

Используется для:

  • минификации
  • инъекций runtime
  • модификации модульной системы

generateBundle

Работает с финальной структурой бандла:

generateBundle(options, bundle) {
  for (const fileName in bundle) {
    const chunk = bundle[fileName];
  }
}

Позволяет:

  • добавлять новые файлы
  • удалять чанки
  • изменять метаданные

Управление порядком плагинов

Плагины могут влиять на порядок выполнения через:

  • позицию в массиве plugins

  • свойство enforce:

    • pre
    • post
  • внутренние зависимости между плагинами

Пример:

export default {
  name: 'plugin-a',
  enforce: 'pre'
}

Архитектурно это создаёт три слоя:

  1. pre-плагины
  2. обычные плагины
  3. post-плагины

Асинхронность и поток выполнения

Практически все хуки поддерживают async:

async transform(code) {
  const data = await fetchSomething();
  return code;
}

Rollup выстраивает внутренний pipeline как цепочку промисов, где каждый этап может ожидать завершения предыдущего.

Это влияет на:

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

Инвалидация и кеширование

Архитектура плагинов учитывает кеш:

  • результаты transform могут кешироваться
  • зависимости отслеживаются через module graph
  • изменения файлов вызывают пересборку

Плагин может управлять кешем через контекст:

  • добавление watch файлов
  • чтение module info
  • влияние на хэш чанков

Взаимодействие с tree-shaking

Плагины напрямую влияют на tree-shaking:

  • изменение AST может скрыть/раскрыть зависимости
  • добавление side effects блокирует удаление кода
  • виртуальные модули могут обходить анализ

Особенно важны:

  • корректная работа moduleSideEffects
  • сохранение статичности импортов
  • отсутствие побочных эффектов в transform

Ошибки и контроль выполнения

Плагины могут:

  • выбрасывать ошибки (прерывание сборки)
  • возвращать предупреждения
  • модифицировать stack trace

Rollup оборачивает выполнение хуков в систему контроля ошибок, связывая их с конкретным модулем.

Архитектурные ограничения системы

Плагинная система Rollup намеренно ограничена:

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

Эти ограничения обеспечивают:

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