Написание Reporter-плагина

Система плагинов Parcel состоит из нескольких специализированных типов расширений: Transformer, Resolver, Bundler, Optimizer, Packager, Compressor, Runtime и Reporter. Каждый из них отвечает за определённый этап сборочного процесса. Reporter занимает особое место, поскольку не изменяет исходный код, не участвует в упаковке модулей и не влияет на итоговые артефакты напрямую. Его задача заключается в получении информации о ходе сборки и последующем выполнении произвольных действий на основе этих данных.

Reporter-плагин работает как наблюдатель за жизненным циклом Parcel. Он получает поток событий и может:

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

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


Архитектура Reporter-плагинов

Reporter реализуется через пакет @parcel/plugin.

Базовая структура выглядит следующим образом:

const {Reporter} = require('@parcel/plugin');

module.exports = new Reporter({
  report({event, options, logger}) {
    // обработка событий
  }
});

Parcel создаёт экземпляр Reporter и вызывает метод report при возникновении различных событий внутри сборщика.

Параметры метода:

Параметр Назначение
event Информация о текущем событии
options Настройки Parcel
logger Система логирования Parcel

Главным объектом является именно event, поскольку через него Reporter получает сведения о происходящем.


Создание структуры плагина

Типичная структура проекта Reporter-плагина:

parcel-reporter-example/
├── package.json
├── Reporter.js
└── README.md

Файл package.json:

{
  "name": "parcel-reporter-example",
  "version": "1.0.0",
  "main": "Reporter.js",
  "engines": {
    "parcel": "^2.0.0"
  }
}

Основной файл:

const {Reporter} = require('@parcel/plugin');

module.exports = new Reporter({
  report({event}) {
    console.log(event.type);
  }
});

Такой Reporter будет выводить тип каждого события, происходящего во время работы Parcel.


Подключение Reporter-плагина

Parcel использует файл конфигурации .parcelrc.

Пример подключения:

{
  "extends": "@parcel/config-default",
  "reporters": [
    "...",
    "./parcel-reporter-example"
  ]
}

Специальное значение "..." сохраняет стандартные Reporter-плагины Parcel.

Без него встроенные средства отображения прогресса будут отключены.


Система событий

Reporter получает объект события следующего вида:

{
  type: 'buildStart'
}

Поле type определяет разновидность события.

Основные типы:

buildStart
buildProgress
buildSuccess
buildFailure
log
watchStart
watchEnd
watchProgress

Каждое событие содержит собственный набор данных.


Обработка buildStart

Событие возникает при начале сборки.

Пример:

const {Reporter} = require('@parcel/plugin');

module.exports = new Reporter({
  report({event}) {
    if (event.type === 'buildStart') {
      console.log('Сборка началась');
    }
  }
});

Практические сценарии:

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

Например:

let startTime = 0;

module.exports = new Reporter({
  report({event}) {
    if (event.type === 'buildStart') {
      startTime = Date.now();
    }
  }
});

Обработка buildSuccess

После успешного завершения сборки Parcel генерирует событие buildSuccess.

Пример:

module.exports = new Reporter({
  report({event}) {
    if (event.type === 'buildSuccess') {
      console.log('Сборка завершена');
    }
  }
});

Событие содержит объект сборки:

if (event.type === 'buildSuccess') {
  console.log(event.bundleGraph);
}

Через bundleGraph можно получить информацию обо всех созданных пакетах.


Получение времени сборки

Распространённый сценарий — измерение длительности выполнения.

const {Reporter} = require('@parcel/plugin');

let startTime;

module.exports = new Reporter({
  report({event}) {
    if (event.type === 'buildStart') {
      startTime = Date.now();
    }

    if (event.type === 'buildSuccess') {
      const duration = Date.now() - startTime;

      console.log(`Сборка заняла ${duration} мс`);
    }
  }
});

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


Обработка buildFailure

При ошибке возникает событие buildFailure.

module.exports = new Reporter({
  report({event}) {
    if (event.type === 'buildFailure') {
      console.error(event.diagnostics);
    }
  }
});

Свойство diagnostics содержит подробную информацию об ошибке.

Структура диагностического объекта может включать:

{
  message: 'Unexpected token',
  origin: 'BabelTransformer',
  stack: '...',
  codeFrames: [...]
}

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


Вывод пользовательских сообщений

Reporter может реагировать на ошибки специальным образом.

module.exports = new Reporter({
  report({event}) {
    if (event.type === 'buildFailure') {
      console.log('Сборка завершилась неудачно');
    }
  }
});

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

const notifier = require('node-notifier');

module.exports = new Reporter({
  report({event}) {
    if (event.type === 'buildFailure') {
      notifier.notify({
        title: 'Parcel',
        message: 'Обнаружена ошибка сборки'
      });
    }
  }
});

Событие buildProgress

Во время работы Parcel создаёт множество промежуточных событий.

if (event.type === 'buildProgress') {
  console.log(event.phase);
}

Поле phase показывает текущий этап обработки.

Например:

resolving
transforming
bundling
packaging
optimizing

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


Создание собственного Progress Bar

Простейший пример:

module.exports = new Reporter({
  report({event}) {
    if (event.type === 'buildProgress') {
      process.stdout.write('.');
    }

    if (event.type === 'buildSuccess') {
      console.log('\nГотово');
    }
  }
});

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

const cliProgress = require('cli-progress');

Reporter может обновлять состояние прогресс-бара в зависимости от этапов сборки.


Работа с логами

Событие log позволяет получать сообщения логирования.

module.exports = new Reporter({
  report({event}) {
    if (event.type === 'log') {
      console.log(event.level);
      console.log(event.message);
    }
  }
});

Возможные уровни:

info
verbose
warn
error
progress
success

Это даёт возможность фильтровать сообщения.


Запись логов в файл

Пример Reporter для сохранения логов.

const fs = require('fs');

module.exports = new Reporter({
  report({event}) {
    if (event.type !== 'log') {
      return;
    }

    fs.appendFileSync(
      'parcel.log',
      `[${event.level}] ${event.message}\n`
    );
  }
});

После каждой сборки формируется полный журнал событий.


Использование logger

Parcel предоставляет собственный объект логирования.

module.exports = new Reporter({
  report({logger}) {
    logger.info({
      message: 'Reporter активирован'
    });
  }
});

Предусмотрены методы:

logger.info(...)
logger.warn(...)
logger.error(...)
logger.verbose(...)
logger.progress(...)

Пример:

logger.warn({
  message: 'Обнаружена потенциальная проблема'
});

Сообщение будет корректно обработано встроенной системой вывода Parcel.


Генерация HTML-отчёта

Одно из наиболее полезных применений Reporter — создание отчётов.

const fs = require('fs');

module.exports = new Reporter({
  report({event}) {
    if (event.type !== 'buildSuccess') {
      return;
    }

    const html = `
      <html>
      <body>
        <h1>Сборка завершена</h1>
      </body>
      </html>
    `;

    fs.writeFileSync('report.html', html);
  }
});

После завершения сборки автоматически создаётся HTML-документ.


Сбор статистики по пакетам

После успешной сборки доступен объект графа пакетов.

if (event.type === 'buildSuccess') {
  const graph = event.bundleGraph;
}

Через него можно обходить все сформированные bundles.

Пример концептуального обхода:

graph.getBundles().forEach(bundle => {
  console.log(bundle.filePath);
});

Полученные данные позволяют:

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

Контроль размера сборки

Reporter может проверять ограничения по размеру.

const fs = require('fs');

module.exports = new Reporter({
  report({event}) {
    if (event.type !== 'buildSuccess') {
      return;
    }

    for (const bundle of event.bundleGraph.getBundles()) {
      const stat = fs.statSync(bundle.filePath);

      if (stat.size > 500000) {
        console.error(
          `${bundle.name} превышает лимит`
        );
      }
    }
  }
});

Подобная проверка особенно полезна в CI/CD.


Интеграция с внешними сервисами

Reporter способен отправлять данные в любые HTTP API.

Пример:

const axios = require('axios');

module.exports = new Reporter({
  async report({event}) {
    if (event.type === 'buildSuccess') {
      await axios.post(
        'https://example.com/build',
        {
          status: 'success',
          date: Date.now()
        }
      );
    }
  }
});

Возможные направления интеграции:

  • системы мониторинга;
  • корпоративные панели аналитики;
  • внутренние сервисы компании;
  • инструменты контроля качества;
  • базы статистики.

Отправка уведомлений в Slack

Практический сценарий автоматизации.

const axios = require('axios');

module.exports = new Reporter({
  async report({event}) {
    if (event.type === 'buildSuccess') {
      await axios.post(
        process.env.SLACK_WEBHOOK,
        {
          text: 'Сборка успешно завершена'
        }
      );
    }
  }
});

Аналогичным образом можно подключать:

  • Discord;
  • Telegram;
  • Microsoft Teams;
  • Mattermost;
  • корпоративные уведомительные системы.

Работа в режиме наблюдения

При использовании parcel watch появляются дополнительные события.

Например:

if (event.type === 'watchStart') {
  console.log('Режим наблюдения активирован');
}

Остановка наблюдения:

if (event.type === 'watchEnd') {
  console.log('Наблюдение завершено');
}

Такие события удобны для управления ресурсами и внешними подключениями.


Асинхронные Reporter-плагины

Метод report может быть асинхронным.

module.exports = new Reporter({
  async report({event}) {
    if (event.type === 'buildSuccess') {
      await saveReport();
    }
  }
});

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

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

Организация кода большого Reporter

По мере роста функциональности рекомендуется выносить обработчики.

Структура:

src/
├── handlers/
│   ├── buildStart.js
│   ├── buildSuccess.js
│   └── buildFailure.js
├── Reporter.js
└── utils/

Основной файл:

const {Reporter} = require('@parcel/plugin');

const buildStart = require('./handlers/buildStart');
const buildSuccess = require('./handlers/buildSuccess');

module.exports = new Reporter({
  async report(context) {
    switch (context.event.type) {
      case 'buildStart':
        return buildStart(context);

      case 'buildSuccess':
        return buildSuccess(context);
    }
  }
});

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


Рекомендации по разработке Reporter-плагинов

Минимизировать влияние на скорость сборки.

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

Избегать блокирующих операций.

Предпочтительнее использовать асинхронные версии API:

await fs.promises.writeFile(...);

вместо

fs.writeFileSync(...);

Обрабатывать исключения.

try {
  await sendData();
} catch (error) {
  console.error(error);
}

Не изменять состояние Parcel напрямую.

Reporter предназначен для наблюдения и анализа, а не для вмешательства в механизм сборки.

Использовать логирование вместо прямого вывода.

logger.info({
  message: 'Операция выполнена'
});

Это обеспечивает корректную интеграцию с внутренней системой сообщений Parcel.


Полноценный пример Reporter-плагина

const {Reporter} = require('@parcel/plugin');
const fs = require('fs/promises');

let startedAt = 0;

module.exports = new Reporter({
  async report({event, logger}) {
    switch (event.type) {
      case 'buildStart':
        startedAt = Date.now();

        logger.info({
          message: 'Сборка началась'
        });

        break;

      case 'buildSuccess':
        const duration =
          Date.now() - startedAt;

        await fs.writeFile(
          'build-report.json',
          JSON.stringify({
            status: 'success',
            duration
          }, null, 2)
        );

        logger.info({
          message:
            `Сборка завершена за ${duration} мс`
        });

        break;

      case 'buildFailure':
        logger.error({
          message: 'Ошибка сборки'
        });

        break;
    }
  }
});

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