При разработке сложных экосистем плагинов возникает необходимость
передавать информацию между различными этапами сборки. Один плагин может
анализировать модуль, другой — использовать результаты анализа, третий —
формировать отчёт на основании накопленных данных. Для подобных
сценариев Rollup предоставляет механизм обмена метаданными через
свойство meta.
meta представляет собой специальный объект, связанный с
конкретным модулем. Он позволяет сохранять произвольные данные во время
работы одного плагина и получать их в другом плагине без необходимости
создавать внешние хранилища, глобальные переменные или дополнительные
файлы.
Главная особенность механизма заключается в том, что данные привязываются именно к модулю и становятся частью внутреннего графа зависимостей Rollup.
Во время обработки модулей Rollup хранит сведения о каждом файле:
Метаданные могут записываться плагинами через результат хука
transform.
Простейшая схема взаимодействия выглядит следующим образом:
meta.Наиболее распространённый способ записи информации в
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: {
hasConsole: true
}
Хороший вариант:
meta: {
analyzer: {
hasConsole: true
}
}
Ещё лучше использовать имя плагина:
meta: {
myAnalyzerPlugin: {
hasConsole: true
}
}
Такой подход предотвращает конфликты между различными расширениями.
Основным способом чтения метаданных является метод
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
);
}
}
};
}
Во время генерации сборки будет доступна информация обо всех обработанных модулях.
Метод getModuleInfo() возвращает объект со множеством
свойств.
Упрощённый пример:
{
id: '/src/main.js',
isEntry: true,
importedIds: [],
dynamicallyImportedIds: [],
importers: [],
dynamicImporters: [],
meta: {
analyzer: {
lines: 15
}
}
}
Именно поле meta содержит пользовательские данные,
записанные плагинами.
Одним из наиболее полезных применений 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.
Несколько плагинов могут дополнять метаданные одного и того же модуля.
Первый плагин:
meta: {
pluginA: {
value: 10
}
}
Второй:
meta: {
pluginB: {
value: 20
}
}
В результате:
meta: {
pluginA: {
value: 10
},
pluginB: {
value: 20
}
}
Rollup объединяет пространства имён, сохраняя данные обоих плагинов.
Хотя запись обычно происходит в transform, чтение
возможно практически из любого хука, где доступен контекст плагина.
Пример:
resolveId(source, importer) {
if (!importer) {
return null;
}
const info =
this.getModuleInfo(importer);
if (
info?.meta.analyzer?.hasConsole
) {
console.log(
'Импорт из файла с console.log'
);
}
return null;
}
Таким образом можно влиять на процесс резолвинга на основании результатов предыдущего анализа.
Информация может использоваться и при загрузке виртуальных модулей.
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: {
cache: {
hugeObject: {
...
}
}
}
Метаданные остаются частью графа модулей и занимают память на протяжении сборки.
Для крупных структур предпочтительнее использовать внутренние
Map и кэши плагина.
meta предназначен для данных конкретного модуля.
Нежелательно сохранять в нём сведения уровня всего проекта:
meta: {
totalFiles: 500
}
Для агрегированной информации лучше использовать переменные плагина:
const statistics = new Map();
Плохая практика:
meta: {
typescript: {
something: true
}
}
Если пространство имён принадлежит другому плагину, существует риск нарушения его работы.
Безопаснее создавать собственный раздел:
meta: {
customPlugin: {
something: true
}
}
Крупные системы часто строятся по многоступенчатой схеме.
meta: {
analyzer: {
exports,
imports
}
}
meta: {
validator: {
warnings,
errors
}
}
meta: {
optimizer: {
removableCode
}
}
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 остаётся
надёжным и устойчивым даже в сложных цепочках обработки модулей.