Логирование внутри плагинов

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

Грамотно организованное логирование позволяет:

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

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


Простое логирование через console

Самый очевидный способ логирования — использование стандартных методов объекта 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;
        }
    };
}

Такой подход удобен на ранних этапах разработки, однако имеет ряд недостатков:

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

Использование API логирования Rollup

Каждый хук получает контекст плагина через this.

Контекст предоставляет специализированные методы:

this.debug(...)
this.info(...)
this.warn(...)
this.error(...)

Эти методы предпочтительнее использования console.


Информационные сообщения через this.info

Метод info() используется для вывода информационных сообщений.

Пример:

export default function myPlugin() {
    return {
        name: 'my-plugin',

        buildStart() {
            this.info('Плагин инициализирован');
        }
    };
}

Информационные сообщения отображаются в соответствии с настройками Rollup и лучше интегрированы в инфраструктуру сборщика.

Передача объекта:

this.info({
    message: 'Обработка завершена'
});

Также можно добавлять дополнительные поля:

this.info({
    message: 'Модуль обработан',
    id: '/src/utils.js'
});

Отладочные сообщения через this.debug

Метод debug() предназначен для диагностической информации.

Пример:

transform(code, id) {
    this.debug(`Трансформация файла ${id}`);
}

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

Пример регистрации промежуточных данных:

transform(code, id) {
    this.debug(`Размер до обработки: ${code.length}`);
}

Полезно при разработке:

this.debug(JSON.stringify(options, null, 4));

Предупреждения через this.warn

Предупреждение сообщает о потенциальной проблеме, но не прерывает сборку.

Пример:

transform(code, id) {
    if (!code.includes('use strict')) {
        this.warn(`${id} не содержит use strict`);
    }

    return null;
}

Сборка продолжится:

(!) src/app.js не содержит use strict

Предупреждения особенно полезны при:

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

Передача объекта предупреждения

Вместо строки можно передавать объект.

Пример:

this.warn({
    message: 'Найдена потенциальная проблема',
    id
});

Добавление позиции:

this.warn({
    message: 'Неиспользуемая конструкция',
    id,
    pos: 125
});

Добавление координат:

this.warn({
    message: 'Ошибка синтаксиса',
    id,
    loc: {
        file: id,
        line: 10,
        column: 5
    }
});

Rollup сможет отобразить точное местоположение проблемы.


Генерация ошибок через this.error

Метод 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 из непрозрачного механизма в хорошо диагностируемую систему, позволяющую быстро обнаруживать проблемы, анализировать производительность и поддерживать стабильность процесса сборки даже в крупных проектах.