rollup.watch() через API

rollup.watch() представляет собой программный интерфейс Rollup, предназначенный для организации режима наблюдения за изменениями исходных файлов и автоматической пересборки бандла. В отличие от CLI-режима --watch, использование API предоставляет полный контроль над процессом сборки, управлением инстансом watcher, обработкой событий и интеграцией в собственные инструменты разработки.

При вызове rollup.watch() создаётся объект наблюдателя, который управляет внутренними сборками Rollup и подписывается на файловую систему. Внутри используется механизм отслеживания зависимостей модуля, который строится во время первой сборки графа.

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

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

Это делает watch-режим существенно быстрее по сравнению с полной пересборкой через CLI или отдельные вызовы rollup.rollup().

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

Функция rollup.watch() принимает конфигурацию или массив конфигураций и возвращает объект watcher.

import { watch } from 'rollup';

const watcher = watch({
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm'
  }
});

При таком вызове Rollup начинает отслеживание файлов, указанных в графе импорта, начиная с entry-point. Любое изменение триггерит событие пересборки.

Жизненный цикл watch-процесса

Watcher работает как event-driven система. Он генерирует события, отражающие состояние сборки.

Основные события:

  • START — запуск наблюдения и инициализация графа модулей;
  • BUNDLE_START — начало очередной сборки;
  • BUNDLE_END — завершение сборки;
  • END — завершение текущего цикла обработки событий;
  • ERROR — ошибка во время сборки или наблюдения.

Пример обработки событий:

watcher.on('event', (event) => {
  switch (event.code) {
    case 'START':
      break;

    case 'BUNDLE_START':
      break;

    case 'BUNDLE_END':
      break;

    case 'ERROR':
      console.error(event.error);
      break;
  }
});

Внутренний цикл пересборки

Каждое изменение файла запускает последовательность:

  1. файловая система уведомляет watcher;
  2. определяется изменённый модуль;
  3. invalidation графа зависимостей;
  4. пересборка затронутой части;
  5. генерация нового output;
  6. эмиссия событий BUNDLE_START и BUNDLE_END.

Важно, что Rollup не всегда пересобирает весь бандл. Если изменение локализовано (например, внутри leaf-модуля), пересборка может быть частичной.

Инвалидация кэша модулей

Watcher активно использует механизм кеширования модулей. Каждый модуль хранит:

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

При изменении файла происходит инвалидация:

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

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

Режимы output в watch

Watcher может работать с одним или несколькими output-конфигурациями. В случае массива конфигураций:

watch([
  {
    input: 'src/a.js',
    output: {
      file: 'dist/a.js',
      format: 'esm'
    }
  },
  {
    input: 'src/b.js',
    output: {
      file: 'dist/b.js',
      format: 'cjs'
    }
  }
]);

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

Объект watcher и управление жизненным циклом

Функция rollup.watch() возвращает объект watcher, который позволяет управлять процессом:

  • остановка наблюдения;
  • подписка на события;
  • контроль состояния.

Основной метод управления:

watcher.close();

После вызова close():

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

Обработка ошибок в watch-режиме

Ошибки в watch не останавливают процесс наблюдения. Вместо этого они передаются через событие ERROR.

watcher.on('event', (event) => {
  if (event.code === 'ERROR') {
    console.error(event.error);
  }
});

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

Типы ошибок:

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

Debounce и батчинг изменений

При множественных изменениях файлов Rollup использует batching. Это означает, что несколько файловых событий объединяются в одну пересборку.

Механизм работает следующим образом:

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

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

Интеграция с плагинами

Плагины Rollup активно участвуют в watch-режиме. Каждый из них может:

  • реагировать на изменения через transform;
  • добавлять дополнительные зависимости;
  • инвалидировать модули через this.addWatchFile.

Пример:

export default function myPlugin() {
  return {
    transform(code, id) {
      this.addWatchFile(id);
      return null;
    }
  };
}

Добавление файлов через addWatchFile расширяет список наблюдаемых ресурсов, включая не-JS файлы.

Файловая система и стратеги отслеживания

Rollup использует нативные file watchers (например, fs.watch или chokidar в зависимости от окружения). Поведение может отличаться:

  • на Windows — событийная модель с возможными дубликатами;
  • на Linux — inotify с высокой точностью;
  • на macOS — FSEvents.

Rollup нормализует эти различия, обеспечивая единое поведение API.

Watch и кеширование transform pipeline

Одной из ключевых оптимизаций является кеширование результатов transform-плагинов. Если модуль не изменился, его трансформации повторно не выполняются.

Кеш зависит от:

  • содержимого файла;
  • параметров плагина;
  • зависимостей transform-функции.

Это существенно ускоряет пересборку в крупных проектах.

Взаимодействие с incremental rebuild

Watch-режим можно рассматривать как реализацию incremental build поверх Rollup. Основные принципы:

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

В отличие от полного rollup.rollup() вызова, watcher работает с долгоживущим состоянием.

Поток событий в сложных сценариях

В реальных проектах последовательность событий может выглядеть так:

  • START;
  • BUNDLE_START;
  • BUNDLE_END;
  • END;
  • изменение файла;
  • BUNDLE_START;
  • BUNDLE_END;
  • END;
  • ERROR (если присутствует ошибка);
  • BUNDLE_START после исправления;
  • BUNDLE_END;
  • END.

Эта модель позволяет интегрировать watcher в системы hot-reload, dev server и CI-инструменты.

Использование в кастомных dev-серверах

Watcher часто применяется как ядро для разработки серверов:

  • отслеживание исходников;
  • пересборка бандла;
  • передача результата в HTTP-сервер;
  • интеграция с WebSocket для HMR-логики.

Типичная схема:

const watcher = watch(config);

watcher.on('event', async (event) => {
  if (event.code === 'BUNDLE_END') {
    const { output } = event;
  }
});

Ограничения watch-режима

Несмотря на гибкость, watch имеет ограничения:

  • не всегда точно отражает изменения внешних систем (например, генерация файлов вне Rollup);
  • зависит от стабильности file system events;
  • может иметь задержки при больших графах;
  • требует аккуратного управления плагинами с побочными эффектами.

Эти ограничения компенсируются архитектурой incremental rebuild и кешированием, но требуют учёта при проектировании сборочной системы.