Плагин Rollup представляет собой программный модуль, который вмешивается в различные этапы сборки: разрешение путей, загрузку файлов, трансформацию кода, генерацию чанков, выпуск ресурсов и другие процессы. Ошибка внутри плагина способна привести к повреждению выходного кода, нарушению tree shaking, появлению некорректных sourcemap или полной остановке сборки.
По этой причине тестирование является обязательной частью разработки плагинов. Даже относительно простой плагин может содержать десятки сценариев поведения:
Качественный набор тестов позволяет безопасно расширять функциональность и предотвращать регрессии.
Обычно тестирование делится на несколько уровней.
Проверяют отдельные функции без запуска Rollup.
Например, если плагин содержит функцию преобразования путей:
export function normalizePath(path) {
return path.replace(/\\/g, '/');
}
Тест может выглядеть следующим образом:
import { expect, test } from 'vitest';
import { normalizePath } from '../src/utils.js';
test('normalizes windows path', () => {
expect(
normalizePath('src\\components\\app.js')
).toBe('src/components/app.js');
});
Подобные тесты выполняются быстро и позволяют проверять бизнес-логику независимо от Rollup.
Проверяют работу плагина внутри настоящего процесса сборки.
Именно такие тесты являются основными для Rollup-плагинов.
Проверяется:
Воспроизводят реальный сценарий использования.
Создается небольшой тестовый проект:
fixture/
├── src/
│ ├── main.js
│ └── utils.js
└── rollup.config.js
После сборки анализируется итоговый результат.
Такие тесты максимально приближены к реальным условиям эксплуатации.
Для современных плагинов чаще всего используются:
Особенно популярным вариантом является Vitest.
Установка:
npm install -D vitest
Минимальный тест:
import { test, expect } from 'vitest';
test('sum', () => {
expect(2 + 2).toBe(4);
});
Основная стратегия тестирования заключается в программном запуске Rollup через API.
Вместо выполнения команды:
rollup -c
сборка запускается напрямую:
import { rollup } from 'rollup';
Пример:
const bundle = await rollup({
input: 'src/main.js',
plugins: []
});
После завершения можно анализировать результат.
Пусть имеется плагин:
export default function bannerPlugin() {
return {
name: 'banner',
transform(code) {
return `/* banner */\n${code}`;
}
};
}
Тест:
import { test, expect } from 'vitest';
import { rollup } from 'rollup';
import bannerPlugin from '../src/plugin.js';
test('adds banner', async () => {
const bundle = await rollup({
input: 'fixtures/input.js',
plugins: [bannerPlugin()]
});
const result = await bundle.generate({
format: 'esm'
});
const code = result.output[0].code;
expect(code.startsWith('/* banner */')).toBe(true);
});
Тест выполняет настоящую сборку и проверяет результат.
Некоторые плагины создают файлы.
Например:
this.emitFile({
type: 'asset',
fileName: 'manifest.json',
source: '{}'
});
Для проверки таких сценариев удобно использовать временные директории.
Node.js предоставляет:
import os from 'node:os';
import fs from 'node:fs/promises';
import path from 'node:path';
Создание каталога:
const tempDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'rollup-test-')
);
После завершения тестов каталог удаляется.
После генерации можно анализировать файлы напрямую.
Пример:
const output = await bundle.generate({
format: 'esm'
});
const chunk = output.output[0];
expect(chunk.code).toContain('console.log');
Проверка имени файла:
expect(chunk.fileName).toBe('main.js');
Проверка экспортов:
expect(chunk.exports).toContain('myFunction');
Один из наиболее важных хуков.
Плагин:
function virtualPlugin() {
return {
name: 'virtual',
resolveId(id) {
if (id === 'virtual:data') {
return id;
}
}
};
}
Тест:
test('resolves virtual module', async () => {
const plugin = virtualPlugin();
const result = plugin.resolveId('virtual:data');
expect(result).toBe('virtual:data');
});
Это пример модульного тестирования.
Интеграционный вариант проверяет полноценную сборку.
Плагин:
function virtualPlugin() {
return {
name: 'virtual',
load(id) {
if (id === 'virtual:data') {
return 'export default 123';
}
}
};
}
Проверка:
expect(
plugin.load('virtual:data')
).toContain('123');
Либо через полноценную сборку и анализ экспорта.
Наиболее распространенный тип тестов.
Плагин:
transform(code) {
return code.replace(
'__VERSION__',
'"1.0.0"'
);
}
Тест:
expect(
transformedCode
).toContain('"1.0.0"');
Дополнительно проверяются:
Если плагин возвращает карту исходников:
return {
code,
map
};
необходимо убедиться, что карта существует.
Пример:
const result = plugin.transform(
sourceCode,
'file.js'
);
expect(result.map).toBeDefined();
При интеграционном тестировании:
const output = await bundle.generate({
sourcemap: true
});
Далее:
expect(
output.output[0].map
).toBeDefined();
Плагин:
this.emitFile({
type: 'asset',
fileName: 'data.json',
source: '{}'
});
После сборки:
const asset = output.output.find(
item => item.type === 'asset'
);
Проверка:
expect(asset.fileName)
.toBe('data.json');
Проверка содержимого:
expect(asset.source)
.toBe('{}');
Плагин может создавать предупреждения.
Пример:
this.warn('deprecated option');
Для тестирования удобно перехватывать обработчик предупреждений:
const warnings = [];
await rollup({
input,
plugins,
onwarn(warning) {
warnings.push(warning);
}
});
Проверка:
expect(warnings.length)
.toBe(1);
Содержимое:
expect(warnings[0].message)
.toContain('deprecated');
Плагин:
this.error('invalid configuration');
Тест:
await expect(
rollup(config)
).rejects.toThrow(
'invalid configuration'
);
Проверка ошибок особенно важна для нестандартных сценариев.
Многие плагины используют виртуальные модули.
Пример:
resolveId(id) {
if (id === 'virtual:env') {
return '\0virtual:env';
}
}
и
load(id) {
if (id === '\0virtual:env') {
return 'export const mode = "test"';
}
}
Тест должен проверять:
Плагин может работать по-разному в зависимости от формата.
Проверка ESM:
await bundle.generate({
format: 'esm'
});
Проверка CommonJS:
await bundle.generate({
format: 'cjs'
});
Проверка IIFE:
await bundle.generate({
format: 'iife',
name: 'App'
});
Каждый формат желательно тестировать отдельно.
Многие проблемы возникают только при использовании нескольких плагинов одновременно.
Пример:
plugins: [
pluginA(),
pluginB()
]
Необходимо проверить:
Крупные плагины обычно содержат каталог fixtures.
Структура:
test/
├── fixtures/
│ ├── basic
│ ├── css
│ ├── virtual
│ ├── assets
│ └── dynamic-import
└── plugin.test.js
Каждая папка представляет отдельный сценарий.
Пример:
fixtures/
└── css
├── input.js
└── style.css
Тест запускает сборку конкретного fixture и сравнивает результат с ожидаемым.
Полезно для крупных трансформаций.
Полученный код сравнивается со снимком.
Пример:
expect(code)
.toMatchSnapshot();
При первом запуске создается файл:
__snapshots__
Последующие тесты сравнивают новый результат со старым.
Преимущества:
Недостаток заключается в том, что чрезмерное использование snapshot-тестов затрудняет понимание причин ошибок.
Для ресурсоемких плагинов полезно контролировать скорость работы.
Пример:
const start = performance.now();
await rollup(config);
const end = performance.now();
Проверка:
expect(end - start)
.toBeLessThan(1000);
Подобные тесты позволяют выявлять деградацию производительности.
Тесты обычно запускаются автоматически через CI-системы.
Типичный процесс:
npm run lint
npm run test
npm run build
Наиболее распространённые платформы:
Автоматический запуск тестов после каждого изменения позволяет обнаруживать проблемы до публикации новой версии плагина.
Хорошая тестовая база обычно обладает следующими характеристиками:
Подобный подход обеспечивает высокую надежность плагинов Rollup, снижает вероятность регрессий и позволяет безопасно развивать функциональность даже при сложной архитектуре сборочного процесса.