Хук watchChange и closeWatcher

Хук watchChange вызывается в режиме rollup --watch при изменении файлов, за которыми наблюдает сборщик. Он является частью плагинной системы Rollup и позволяет перехватывать события файловой системы до того, как произойдёт повторная сборка. Основная задача этого хука — дать плагину возможность отреагировать на изменения исходного кода, сбросить внутренние кеши, инициировать побочные эффекты или подготовить данные к следующей компиляции.

Сигнатура хука:

watchChange(id, change)

id — абсолютный путь к изменённому файлу change — объект с метаданными изменения, обычно содержащий тип события (create, update, delete) и дополнительные сведения в зависимости от окружения

Хук относится к категории watcher hooks и не влияет напрямую на граф модулей, но может косвенно воздействовать на него через кеширование и invalidate-логику.

При активированном режиме наблюдения Rollup использует файловую систему или внешние watch-сервисы для отслеживания изменений. Когда происходит событие изменения файла, последовательность выглядит следующим образом:

  1. Файловая система фиксирует изменение
  2. Watcher передаёт событие в Rollup
  3. Rollup определяет, относится ли файл к графу модулей
  4. Вызывается watchChange у всех подключённых плагинов
  5. После обработки хуков инициируется пересборка при необходимости

Важно, что watchChange вызывается до пересборки, что делает его пригодным для подготовки состояния перед новым циклом сборки.

Типичные сценарии использования

Основные применения хука связаны с управлением состоянием плагина:

  • Инвалидация внутреннего кеша при изменении исходников
  • Очистка или обновление промежуточных данных
  • Синхронизация с внешними системами (например, генерация метаданных)
  • Логирование изменений файлов для отладки watch-режима

Пример базового использования:

export default function myPlugin() {
  const cache = new Map();

  return {
    name: 'my-plugin',

    watchChange(id, change) {
      cache.delete(id);

      if (change.event === 'delete') {
        cache.delete(id);
      }
    }
  };
}

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

Особенности поведения

Хук имеет несколько важных особенностей, влияющих на архитектуру плагинов:

  • Вызывается только в watch-режиме
  • Не блокирует процесс сборки, если не возвращает Promise
  • Может быть асинхронным, что позволяет выполнять операции ввода-вывода
  • Не влияет напрямую на результат бандла
  • Может вызываться очень часто при массовых изменениях файлов

Асинхронный вариант:

export default function plugin() {
  return {
    name: 'async-watch',

    async watchChange(id, change) {
      await someExternalSync(id, change);
    }
  };
}

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

Структура объекта change

Хотя Rollup не фиксирует строго единый интерфейс, на практике объект изменения может включать:

  • event — тип изменения (create, update, delete)
  • oldPath — предыдущий путь (при переименовании)
  • timestamp — время события
  • дополнительные поля от watcher backend (например, chokidar)

Пример обработки:

watchChange(id, change) {
  switch (change.event) {
    case 'create':
      console.log('Файл создан:', id);
      break;

    case 'upd ate':
      console.log('Файл изменён:', id);
      break;

    case 'delete':
      console.log('Файл удалён:', id);
      break;
  }
}

Взаимодействие с графом модулей

Хук watchChange не изменяет граф модулей напрямую, однако может использоваться для косвенного влияния на него через кеши и invalidate-механизмы.

В типичной архитектуре Rollup:

  • граф модулей пересобирается при изменениях
  • watchChange используется как предварительный слой реакции
  • дальнейшая логика обработки идёт через load, resolveId, transform

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

Производительность и частые изменения

При большом количестве файлов watchChange становится горячей точкой. Основные проблемы:

  • лавина событий при массовых изменениях (например, генерация файлов)
  • дублирующиеся вызовы на одно и то же изменение
  • задержки при синхронных тяжёлых операциях

Типичная оптимизация — дедупликация:

const pending = new Se t();

export default function plugin() {
  return {
    name: 'optimized-watch',

    watchChange(id) {
      if (pending.has(id)) return;
      pending.add(id);

      setTimeout(() => {
        pending.delete(id);
      }, 50);
    }
  };
}

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


Хук closeWatcher вызывается при завершении работы watch-режима, когда процесс наблюдения за файлами останавливается. Это может происходить при ручном завершении процесса, перезапуске сборки или закрытии CLI-инстанса Rollup.

Сигнатура хука:

closeWatcher()

Хук не принимает аргументов и предназначен исключительно для финализации ресурсов.

Роль closeWatcher в жизненном цикле

closeWatcher является завершающей точкой watcher-пайплайна. Его основная задача — корректное освобождение ресурсов, которые были созданы в процессе наблюдения.

Типичный порядок завершения:

  1. Пользователь останавливает watch-режим
  2. Rollup завершает текущие задачи сборки
  3. Закрывается файловый watcher
  4. Вызывается closeWatcher у всех плагинов
  5. Процесс завершается

Основные сценарии использования

Хук применяется для:

  • закрытия соединений (WebSocket, HTTP long-polling)
  • освобождения файловых дескрипторов
  • остановки таймеров и интервалов
  • завершения фоновых задач
  • записи финальной статистики

Пример:

export default function plugin() {
  let interval;

  return {
    name: 'resource-plugin',

    buildStart() {
      interval = setInterval(() => {
        // фоновые задачи
      }, 1000);
    },

    closeWatcher() {
      clearInterval(interval);
    }
  };
}

Без очистки подобных ресурсов процесс может зависнуть или продолжить потреблять CPU даже после остановки watch-режима.

Важные особенности поведения

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

Асинхронный вариант:

export default function plugin() {
  return {
    name: 'async-cleanup',

    async closeWatcher() {
      await flushLogs();
      await closeConnections();
    }
  };
}

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

Связь watchChange и closeWatcher

Оба хука относятся к жизненному циклу watch-режима, но решают разные задачи:

  • watchChange — реакция на события файловой системы в реальном времени
  • closeWatcher — финализация и освобождение ресурсов

Они часто используются совместно:

export default function plugin() {
  const state = new Map();

  return {
    name: 'combined-plugin',

    watchChange(id, change) {
      state.set(id, change.timestamp);
    },

    closeWatcher() {
      state.clear();
    }
  };
}

Такая связка обеспечивает контролируемое управление состоянием плагина на протяжении всего жизненного цикла watch-режима.

Практические архитектурные паттерны

В сложных плагинах оба хука участвуют в построении реактивной системы:

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

Пример батчинга:

export default function plugin() {
  const queue = new Set();
  let timer;

  function flush() {
    queue.clear();
    timer = null;
  }

  return {
    name: 'batch-plugin',

    watchChange(id) {
      queue.add(id);

      if (!timer) {
        timer = setTimeout(flush, 100);
      }
    },

    closeWatcher() {
      if (timer) clearTimeout(timer);
      flush();
    }
  };
}