Логирование является важной частью разработки плагинов для Rollup. Во время обработки модулей, трансформации исходного кода, генерации чанков и выполнения других этапов сборки возникает необходимость отслеживать внутреннее состояние плагина, фиксировать ошибки, предупреждения и диагностическую информацию.
Грамотно организованное логирование позволяет:
В отличие от обычных приложений, плагины Rollup работают внутри процесса сборки и должны взаимодействовать с системой сообщений самого Rollup.
Самый очевидный способ логирования — использование стандартных
методов объекта console.
Пример:
export default function myPlugin() {
return {
name: 'my-plugin',
buildStart() {
console.log('Сборка началась');
}
};
}
Вывод:
Сборка началась
Для диагностики могут использоваться различные методы:
console.log('Информация');
console.warn('Предупреждение');
console.error('Ошибка');
console.debug('Отладка');
Логирование может выполняться внутри любых хуков:
export default function myPlugin() {
return {
name: 'my-plugin',
resolveId(source) {
console.log('Поиск модуля:', source);
return null;
},
load(id) {
console.log('Загрузка:', id);
return null;
},
transform(code, id) {
console.log('Трансформация:', id);
return null;
}
};
}
Такой подход удобен на ранних этапах разработки, однако имеет ряд недостатков:
Каждый хук получает контекст плагина через this.
Контекст предоставляет специализированные методы:
this.debug(...)
this.info(...)
this.warn(...)
this.error(...)
Эти методы предпочтительнее использования console.
Метод info() используется для вывода информационных
сообщений.
Пример:
export default function myPlugin() {
return {
name: 'my-plugin',
buildStart() {
this.info('Плагин инициализирован');
}
};
}
Информационные сообщения отображаются в соответствии с настройками Rollup и лучше интегрированы в инфраструктуру сборщика.
Передача объекта:
this.info({
message: 'Обработка завершена'
});
Также можно добавлять дополнительные поля:
this.info({
message: 'Модуль обработан',
id: '/src/utils.js'
});
Метод debug() предназначен для диагностической
информации.
Пример:
transform(code, id) {
this.debug(`Трансформация файла ${id}`);
}
Отладочные сообщения позволяют получать подробные сведения о работе плагина без загрязнения стандартного вывода.
Пример регистрации промежуточных данных:
transform(code, id) {
this.debug(`Размер до обработки: ${code.length}`);
}
Полезно при разработке:
this.debug(JSON.stringify(options, null, 4));
Предупреждение сообщает о потенциальной проблеме, но не прерывает сборку.
Пример:
transform(code, id) {
if (!code.includes('use strict')) {
this.warn(`${id} не содержит use strict`);
}
return null;
}
Сборка продолжится:
(!) src/app.js не содержит use strict
Предупреждения особенно полезны при:
Вместо строки можно передавать объект.
Пример:
this.warn({
message: 'Найдена потенциальная проблема',
id
});
Добавление позиции:
this.warn({
message: 'Неиспользуемая конструкция',
id,
pos: 125
});
Добавление координат:
this.warn({
message: 'Ошибка синтаксиса',
id,
loc: {
file: id,
line: 10,
column: 5
}
});
Rollup сможет отобразить точное местоположение проблемы.
Метод error() завершает текущую сборку.
Пример:
buildStart() {
this.error('Конфигурация недействительна');
}
После вызова выполнение прекращается:
transform(code) {
if (!code.includes('export')) {
this.error('Модуль должен содержать export');
}
}
Использование объекта:
this.error({
message: 'Некорректный модуль',
id
});
Указание позиции:
this.error({
message: 'Ошибка трансформации',
id,
loc: {
file: id,
line: 15,
column: 8
}
});
Для анализа работы плагина часто логируют все основные хуки.
Пример:
export default function myPlugin() {
return {
name: 'my-plugin',
options() {
this.info('options');
},
buildStart() {
this.info('buildStart');
},
resolveId(source) {
this.info(`resolveId: ${source}`);
return null;
},
load(id) {
this.info(`load: ${id}`);
return null;
},
transform(code, id) {
this.info(`transform: ${id}`);
return null;
},
generateBundle() {
this.info('generateBundle');
},
closeBundle() {
this.info('closeBundle');
}
};
}
Такой подход позволяет быстро понять порядок вызова хуков.
Часто требуется измерять длительность операций.
Пример:
const timings = new Map();
export default function myPlugin() {
return {
name: 'my-plugin',
load(id) {
timings.set(id, Date.now());
return null;
},
transform(code, id) {
const start = timings.get(id);
if (start) {
const duration = Date.now() - start;
this.info(
`${id} обработан за ${duration} мс`
);
}
return null;
}
};
}
Более точные измерения:
const start = performance.now();
/* операция */
const end = performance.now();
this.debug(
`Время выполнения: ${(end - start).toFixed(2)} мс`
);
Во многих плагинах полезно собирать агрегированную информацию.
Пример:
export default function myPlugin() {
let filesProcessed = 0;
return {
name: 'my-plugin',
transform(code) {
filesProcessed++;
return null;
},
buildEnd() {
this.info(
`Обработано файлов: ${filesProcessed}`
);
}
};
}
Сбор более сложной статистики:
const stats = {
files: 0,
bytes: 0
};
Во время обработки:
stats.files++;
stats.bytes += code.length;
После завершения:
buildEnd() {
this.info(
`Файлов: ${stats.files}, байт: ${stats.bytes}`
);
}
Диагностика часто начинается с проверки входных настроек.
Пример:
export default function myPlugin(options = {}) {
return {
name: 'my-plugin',
buildStart() {
this.debug(
JSON.stringify(options, null, 2)
);
}
};
}
Полезно выводить только необходимые параметры:
this.info(
`Режим работы: ${options.mode}`
);
В производственной среде чрезмерный объём сообщений может мешать.
Часто используется флаг:
export default function myPlugin(options = {}) {
const verbose = options.verbose;
return {
name: 'my-plugin',
transform(code, id) {
if (verbose) {
this.info(`Файл: ${id}`);
}
return null;
}
};
}
Конфигурация:
myPlugin({
verbose: true
});
В крупных плагинах удобно централизовать вывод сообщений.
Пример:
function createLogger(context) {
return {
info(message) {
context.info(message);
},
warn(message) {
context.warn(message);
},
error(message) {
context.error(message);
}
};
}
Использование:
transform(code, id) {
const logger = createLogger(this);
logger.info(`Обработка ${id}`);
return null;
}
При большом количестве плагинов важно явно обозначать источник сообщения.
Пример:
this.info(
'[my-plugin] Начало обработки'
);
Можно создать обёртку:
function log(context, message) {
context.info(
`[my-plugin] ${message}`
);
}
Использование:
log(this, 'Модуль загружен');
Плагин может использовать сторонние библиотеки.
Пример:
try {
processFile(code);
}
catch (error) {
this.error({
message: error.message
});
}
С сохранением дополнительной информации:
catch (error) {
this.error({
message: error.message,
stack: error.stack
});
}
Во время финальной сборки можно отслеживать создаваемые файлы.
Пример:
generateBundle(options, bundle) {
for (const fileName in bundle) {
this.info(
`Создан файл ${fileName}`
);
}
}
Подсчёт размеров:
generateBundle(options, bundle) {
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk') {
this.info(
`${chunk.fileName}: ${chunk.code.length} байт`
);
}
}
}
Использование this.info() вместо
console.log()
Позволяет интегрироваться с инфраструктурой Rollup и получать единообразный вывод.
Использование this.warn() для некритичных
проблем
Предупреждения не должны останавливать сборку.
Использование this.error() только для
действительно критических ситуаций
Любой вызов приводит к прекращению текущей операции сборки.
Минимизация объёма отладочного вывода
Чрезмерное логирование ухудшает читаемость журналов и может замедлять работу при обработке больших проектов.
Добавление контекста в сообщения
Хорошее сообщение содержит:
Пример информативного сообщения:
this.warn(
`Файл ${id} содержит неподдерживаемый синтаксис`
);
Вместо малоинформативного варианта:
this.warn('Ошибка');
Грамотно организованное логирование превращает плагин Rollup из непрозрачного механизма в хорошо диагностируемую систему, позволяющую быстро обнаруживать проблемы, анализировать производительность и поддерживать стабильность процесса сборки даже в крупных проектах.