Взаимодействие между плагинами через meta

При разработке сложных экосистем плагинов возникает необходимость передавать информацию между различными этапами сборки. Один плагин может анализировать модуль, другой — использовать результаты анализа, третий — формировать отчёт на основании накопленных данных. Для подобных сценариев Rollup предоставляет механизм обмена метаданными через свойство meta.

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

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


Общая концепция работы

Во время обработки модулей Rollup хранит сведения о каждом файле:

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

Метаданные могут записываться плагинами через результат хука transform.

Простейшая схема взаимодействия выглядит следующим образом:

  1. Первый плагин анализирует код.
  2. Первый плагин записывает результаты в meta.
  3. Rollup сохраняет информацию в графе модулей.
  4. Второй плагин получает доступ к этим данным через API Rollup.
  5. Второй плагин использует полученные сведения для собственной логики.

Добавление метаданных в transform

Наиболее распространённый способ записи информации в meta выполняется через возврат объекта из хука transform.

Пример:

function analyzerPlugin() {
    return {
        name: 'analyzer',

        transform(code, id) {
            const hasConsole = code.includes('console.log');

            return {
                code,
                meta: {
                    analyzer: {
                        hasConsole
                    }
                }
            };
        }
    };
}

После обработки каждого модуля Rollup сохранит объект:

{
    analyzer: {
        hasConsole: true
    }
}

или

{
    analyzer: {
        hasConsole: false
    }
}

в зависимости от содержимого файла.


Пространства имён в meta

Поскольку несколько плагинов могут одновременно записывать данные, рекомендуется использовать собственное пространство имён.

Плохой вариант:

meta: {
    hasConsole: true
}

Хороший вариант:

meta: {
    analyzer: {
        hasConsole: true
    }
}

Ещё лучше использовать имя плагина:

meta: {
    myAnalyzerPlugin: {
        hasConsole: true
    }
}

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


Получение данных через getModuleInfo

Основным способом чтения метаданных является метод this.getModuleInfo().

Предположим, существует плагин:

function analyzerPlugin() {
    return {
        name: 'analyzer',

        transform(code) {
            return {
                code,
                meta: {
                    analyzer: {
                        lines: code.split('\n').length
                    }
                }
            };
        }
    };
}

Другой плагин может получить эти данные:

function reportPlugin() {
    return {
        name: 'report',

        generateBundle() {
            for (const id of this.getModuleIds()) {
                const info = this.getModuleInfo(id);

                console.log(
                    id,
                    info.meta.analyzer.lines
                );
            }
        }
    };
}

Во время генерации сборки будет доступна информация обо всех обработанных модулях.


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

Метод getModuleInfo() возвращает объект со множеством свойств.

Упрощённый пример:

{
    id: '/src/main.js',
    isEntry: true,
    importedIds: [],
    dynamicallyImportedIds: [],
    importers: [],
    dynamicImporters: [],
    meta: {
        analyzer: {
            lines: 15
        }
    }
}

Именно поле meta содержит пользовательские данные, записанные плагинами.


Передача результатов анализа AST

Одним из наиболее полезных применений meta является сохранение результатов дорогостоящего анализа AST.

Предположим, первый плагин ищет все вызовы определённой функции.

function collectorPlugin() {
    return {
        name: 'collector',

        transform(code) {
            const matches = [];

            const regex = /trackEvent\s*\(/g;

            let match;

            while ((match = regex.exec(code))) {
                matches.push(match.index);
            }

            return {
                code,
                meta: {
                    collector: {
                        trackCalls: matches
                    }
                }
            };
        }
    };
}

Второй плагин может использовать уже готовый результат:

function reporterPlugin() {
    return {
        name: 'reporter',

        generateBundle() {
            for (const id of this.getModuleIds()) {
                const info = this.getModuleInfo(id);

                const calls =
                    info.meta.collector?.trackCalls || [];

                if (calls.length) {
                    console.log(id, calls.length);
                }
            }
        }
    };
}

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


Сбор статистики между плагинами

Метаданные позволяют организовывать сложные цепочки анализа.

Первый плагин вычисляет объём кода:

meta: {
    metrics: {
        lines: 120
    }
}

Второй вычисляет сложность:

meta: {
    complexity: {
        score: 34
    }
}

Третий формирует общий отчёт:

generateBundle() {
    for (const id of this.getModuleIds()) {
        const info = this.getModuleInfo(id);

        const lines =
            info.meta.metrics?.lines;

        const complexity =
            info.meta.complexity?.score;

        console.log({
            id,
            lines,
            complexity
        });
    }
}

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


Объединение данных нескольких transform

Несколько плагинов могут дополнять метаданные одного и того же модуля.

Первый плагин:

meta: {
    pluginA: {
        value: 10
    }
}

Второй:

meta: {
    pluginB: {
        value: 20
    }
}

В результате:

meta: {
    pluginA: {
        value: 10
    },
    pluginB: {
        value: 20
    }
}

Rollup объединяет пространства имён, сохраняя данные обоих плагинов.


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

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

Пример:

resolveId(source, importer) {
    if (!importer) {
        return null;
    }

    const info =
        this.getModuleInfo(importer);

    if (
        info?.meta.analyzer?.hasConsole
    ) {
        console.log(
            'Импорт из файла с console.log'
        );
    }

    return null;
}

Таким образом можно влиять на процесс резолвинга на основании результатов предыдущего анализа.


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

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

load(id) {
    if (id !== 'virtual:report') {
        return null;
    }

    let content = '';

    for (const moduleId of this.getModuleIds()) {
        const info =
            this.getModuleInfo(moduleId);

        const count =
            info.meta.metrics?.lines;

        if (count) {
            content += `${moduleId}: ${count}\n`;
        }
    }

    return `export default ${JSON.stringify(content)}`;
}

Виртуальный модуль получает данные, собранные другими плагинами.


Формирование карты зависимостей

Через meta удобно сохранять промежуточные результаты анализа импортов.

transform(code) {
    const imports = [];

    const regex =
        /import\s+.*?from\s+['"](.*?)['"]/g;

    let match;

    while ((match = regex.exec(code))) {
        imports.push(match[1]);
    }

    return {
        code,
        meta: {
            dependencyMap: {
                imports
            }
        }
    };
}

Позже другой плагин способен построить визуализацию графа проекта без повторного обхода исходников.


Проверка качества кода

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

transform(code) {
    const warnings = [];

    if (code.includes('var ')) {
        warnings.push(
            'Использование var'
        );
    }

    return {
        code,
        meta: {
            lint: {
                warnings
            }
        }
    };
}

Второй плагин может генерировать сводный отчёт:

generateBundle() {
    for (const id of this.getModuleIds()) {
        const info =
            this.getModuleInfo(id);

        const warnings =
            info.meta.lint?.warnings || [];

        if (warnings.length) {
            console.log(id);
            console.log(warnings);
        }
    }
}

Ограничения использования meta

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

Не хранить большие объёмы данных

Плохой пример:

meta: {
    cache: {
        hugeObject: {
            ...
        }
    }
}

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

Для крупных структур предпочтительнее использовать внутренние Map и кэши плагина.


Не использовать для глобального состояния

meta предназначен для данных конкретного модуля.

Нежелательно сохранять в нём сведения уровня всего проекта:

meta: {
    totalFiles: 500
}

Для агрегированной информации лучше использовать переменные плагина:

const statistics = new Map();

Не изменять чужие пространства имён

Плохая практика:

meta: {
    typescript: {
        something: true
    }
}

Если пространство имён принадлежит другому плагину, существует риск нарушения его работы.

Безопаснее создавать собственный раздел:

meta: {
    customPlugin: {
        something: true
    }
}

Организация сложного взаимодействия между несколькими плагинами

Крупные системы часто строятся по многоступенчатой схеме.

Этап 1. Анализ

meta: {
    analyzer: {
        exports,
        imports
    }
}

Этап 2. Валидация

meta: {
    validator: {
        warnings,
        errors
    }
}

Этап 3. Оптимизация

meta: {
    optimizer: {
        removableCode
    }
}

Этап 4. Отчётность

generateBundle() {
    const report = [];
}

Каждый плагин использует данные предыдущих этапов, не выполняя лишнюю работу и не создавая прямых зависимостей между реализациями.


Рекомендации по проектированию метаданных

Использовать уникальные пространства имён

meta: {
    myPlugin: {}
}

Хранить только результаты вычислений

meta: {
    analyzer: {
        imports,
        exports
    }
}

Избегать хранения исходного кода

meta: {
    sourceCode: code
}

Документировать структуру данных

meta: {
    analyzer: {
        version: 1,
        imports: [],
        exports: []
    }
}

Считать данные необязательными

При чтении всегда желательно использовать безопасный доступ:

const imports =
    info.meta.analyzer?.imports || [];

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