Виртуальные модули в Rollup представляют собой механизм генерации
модулей «на лету» без наличия физического файла на диске. Они создаются
и обслуживаются плагином через хуки resolveId и
load, позволяя внедрять синтетические зависимости прямо в
граф сборки.
Ключевая идея заключается в том, что Rollup работает не с файлами как таковыми, а с идентификаторами модулей. Если плагин перехватывает запрос на модуль и возвращает код вручную, для сборщика это выглядит как обычный импортируемый файл.
Виртуальный модуль существует только в памяти сборщика. Его жизненный цикл полностью контролируется плагином:
resolveId сообщает Rollup, что модуль существует;load возвращает исходный код;Минимальная реализация строится вокруг двух хуков.
export default function virtualPlugin() {
const VIRTUAL_ID = 'virtual:example-module';
return {
name: 'virtual-module',
resolveId(source) {
if (source === VIRTUAL_ID) {
return VIRTUAL_ID;
}
return null;
},
load(id) {
if (id === VIRTUAL_ID) {
return `
export const message = 'Hello from virtual module';
`;
}
return null;
}
};
}
После подключения такого плагина импорт
virtual:example-module начинает работать как обычный
ES-модуль.
В Rollup нет строгого стандарта именования виртуальных модулей, но на практике используются несколько подходов:
Наиболее распространённый вариант — использование пространственного префикса:
virtual:configvirtual:envvirtual:runtimeТакой подход снижает вероятность конфликта с реальными файлами.
Rollup использует нулевой символ (\0) для внутренних
модулей. Плагины часто применяют его для скрытия виртуальных
зависимостей:
const VIRTUAL_ID = '\0my-virtual-module';
Преимущество подхода — гарантия отсутствия пересечений с пользовательскими путями, так как файловая система не допускает такой символ в именах файлов.
Виртуальные модули могут включать параметры:
virtual:data?type=jsonvirtual:component?name=ButtonВ этом случае resolveId выполняет парсинг строки и
нормализацию идентификатора.
Основная логика виртуального модуля сосредоточена в
load. Он возвращает строку исходного кода, который затем
проходит через стандартный пайплайн Rollup.
Пример динамической генерации:
export default function envPlugin(options = {}) {
const id = 'virtual:env';
return {
name: 'env-plugin',
resolveId(source) {
if (source === id) return id;
return null;
},
load(sourceId) {
if (sourceId === id) {
const env = options.env || {};
return `
export const env = ${JSON.stringify(env)};
`;
}
return null;
}
};
}
Такой модуль позволяет инкапсулировать конфигурацию окружения без отдельного JSON-файла.
Хук resolveId отвечает за идентификацию модуля, а
load — за его содержимое. Их взаимодействие можно
рассматривать как двухфазный процесс:
Если resolveId возвращает null, Rollup
продолжает поиск в файловой системе и других плагинах. Если возвращён
идентификатор — цепочка фиксируется за конкретным модулем.
Важно учитывать, что resolveId может возвращать не
только строку, но и объект с метаданными:
resolveId(source) {
if (source === 'virtual:demo') {
return {
id: '\0virtual:demo',
moduleSideEffects: false
};
}
return null;
}
Это позволяет оптимизировать tree-shaking.
Виртуальные модули часто используются как промежуточный слой между конфигурацией и кодом приложения.
Типовые сценарии:
Пример генерации маршрутов:
export default function routesPlugin(routes) {
const id = 'virtual:routes';
return {
name: 'routes-plugin',
resolveId(source) {
if (source === id) return id;
},
load(sourceId) {
if (sourceId !== id) return null;
const imports = routes.map((r, i) =>
`import * as m${i} from '${r.path}';`
).join('\n');
const exports = `
export const routes = [
${routes.map((r, i) => `{ path: '${r.path}', module: m${i} }`).join(',\n')}
];
`;
return `${imports}\n\n${exports}`;
}
};
}
Виртуальный модуль может импортировать реальные файлы. Rollup обрабатывает такие зависимости как часть графа:
load(id) {
if (id === 'virtual:main') {
return `
import data from './data.json';
export default {
data
};
`;
}
}
Таким образом виртуальный модуль становится точкой входа для сложной структуры зависимостей.
Rollup полагается на стабильность идентификаторов модулей. Если виртуальный модуль генерирует разные идентификаторы между сборками, это приводит к:
Поэтому идентификаторы должны быть детерминированными:
const id = `virtual:component:${name}`;
и не зависеть от случайных значений.
Поскольку виртуальные модули генерируют код вручную, важно учитывать sourcemaps.
Rollup ожидает, что load может вернуть объект:
load(id) {
if (id === 'virtual:example') {
return {
code: 'export const x = 1;',
map: { mappings: '' }
};
}
}
При генерации сложного кода sourcemap становится критическим для отладки, особенно если виртуальный модуль участвует в трансформациях TypeScript или Babel.
Плагины виртуальных модулей часто используют this.error
и this.warn для диагностики:
load(id) {
if (id === 'virtual:config') {
if (!this.meta.config) {
this.warn('Config is not provided, using defaults');
}
return `export const config = {};`;
}
}
Ошибки, возникающие внутри виртуальных модулей, отображаются как ошибки обычных модулей, но их трассировка зависит от корректности sourcemap.
В режиме наблюдения виртуальные модули могут пересоздаваться при изменении внешних данных. Для этого используется кеширование состояния внутри плагина:
let cache;
export default function plugin() {
return {
name: 'virtual-watch',
buildStart() {
cache = null;
},
load(id) {
if (id === 'virtual:data') {
if (!cache) {
cache = computeHeavyData();
}
return `export default ${JSON.stringify(cache)}`;
}
}
};
}
Такой подход снижает нагрузку при частых пересборках.
Rollup строго работает с ES-модулями, поэтому виртуальные модули должны возвращать корректный ESM-код. Недопустимы:
Корректный виртуальный модуль всегда остаётся статически анализируемым.
Плагин может предоставлять сразу несколько виртуальных модулей через одну точку входа:
const modules = {
'virtual:alpha': 'export const a = 1;',
'virtual:beta': 'export const b = 2;'
};
export default function multiVirtual() {
return {
name: 'multi-virtual',
resolveId(source) {
if (modules[source]) return source;
return null;
},
load(id) {
if (modules[id]) return modules[id];
return null;
}
};
}
Такой подход используется в библиотеках, которые предоставляют набор runtime-утилит через Rollup.
Каждая из этих проблем возникает не из-за самого механизма, а из-за отсутствия строгой дисциплины при проектировании плагина.
Виртуальные модули формируют промежуточный слой между статическими файлами и динамической логикой сборки. Они позволяют:
Их использование особенно эффективно в плагинах, которые работают как мини-компиляторы внутри Rollup, расширяя границы стандартного ESM-графа.