Резолвинг модулей в Rollup — это процесс определения конечного
идентификатора модуля (module id), по которому Rollup загружает,
кэширует и включает файл в граф зависимостей. В отличие от
рантайм-бандлеров, где резолвинг часто тесно связан с Node.js
require, Rollup строит статический граф и требует более
детального контроля над тем, как именно строка импорта преобразуется в
конкретный файл.
Ключевой момент заключается в том, что импорт в исходном коде не
обязан однозначно соответствовать файловому пути. Строка
import x from 'lib' может означать:
node_modulesИменно поэтому Rollup предоставляет механизм ручного резолвинга через плагины.
На этапе построения графа Rollup встречает импорт:
import utils from './utils.js';
Далее происходит попытка определить реальный файл:
./, ../) резолвятся
относительно текущего модуляreact, lodash) требуют
плагинов (например, node-resolve)Без дополнительных плагинов Rollup не реализует полноценный Node.js resolution algorithm. Это принципиальное отличие архитектуры.
Основной инструмент ручного резолвинга — хук плагина
resolveId.
export default function myResolver() {
return {
name: 'my-resolver',
resolveId(source, importer) {
if (source === 'special-lib') {
return '/src/lib/special/index.js';
}
}
};
}
source — строка импорта
importer — модуль, из которого происходит
импорт
возвращаемое значение:
string → финальный путьnull → передать следующему плагинуfalse → пометить как external (не включать в
бандл)Rollup предоставляет API this.resolve, позволяющее
использовать встроенный механизм резолвинга внутри собственного
плагина.
resolveId(source, importer) {
return this.resolve(source, importer, { skipSelf: true })
.then(resolved => resolved && resolved.id);
}
skipSelf: true предотвращает рекурсивный вызов текущего
плагинаid,
external, moduleSideEffects)Резолвинг в Rollup — это цепочка. Каждый плагин может:
Пример конфликтного поведения:
resolveId(source) {
if (source === 'config') {
return '/a.js';
}
}
и позже:
resolveId(source) {
if (source === 'config') {
return '/b.js';
}
}
Фактически сработает первый плагин, который вернул не
null.
Частая задача — переопределить резолвинг конкретных директорий:
import path from 'path';
export default function aliasPlugin() {
return {
name: 'alias',
resolveId(source, importer) {
if (source.startsWith('@app/')) {
return path.resolve(
process.cwd(),
'src',
source.replace('@app/', '')
);
}
}
};
}
importer обязателен для корректного относительного
резолвингаimporter легко получить некорректные
абсолютные путиОдна из ключевых возможностей ручного резолвинга — создание виртуальных модулей.
const VIRTUAL_PREFIX = 'virtual:';
export default function virtualPlugin() {
return {
name: 'virtual-modules',
resolveId(source) {
if (source.startsWith(VIRTUAL_PREFIX)) {
return source;
}
},
load(id) {
if (id === 'virtual:config') {
return `export default { value: 123 };`;
}
}
};
}
resolveId возвращает идентификатор без физического
файлаload генерирует содержимое на летуРезолвинг напрямую связан с понятием external-модулей.
resolveId(source) {
if (source === 'fs') {
return false;
}
}
require('fs') или
import 'fs'Резолвинг часто зависит от режима сборки:
export default function envResolver(options) {
const isProd = options.env === 'production';
return {
name: 'env-resolver',
resolveId(source) {
if (source === 'env-config') {
return isProd
? '/src/config/prod.js'
: '/src/config/dev.js';
}
}
};
}
Такой подход заменяет необходимость runtime-ветвлений и позволяет полностью удалить лишний код на этапе сборки.
Ручной резолвинг часто приводит к скрытым проблемам:
Rollup не всегда автоматически добавляет .js:
return '/src/utils/index'; // может не резолвиться без плагинов
Плагин @rollup/plugin-node-resolve может перехватывать
импорт раньше кастомного резолвера.
Если разные пути резолвятся в один и тот же файл с разными id:
./utils/index.js
./utils
Rollup может воспринять их как разные модули, если не нормализовать путь.
Для предотвращения дублей важно нормализовать пути:
import path from 'path';
function normalize(id) {
return path.resolve(id);
}
И использовать это в resolveId:
resolveId(source, importer) {
const resolved = path.resolve(path.dirname(importer), source);
return normalize(resolved);
}
В современных проектах значимую роль играет поле exports
в package.json. Rollup сам по себе не всегда интерпретирует
его без дополнительных плагинов.
Ручной резолвинг может перехватывать такие случаи:
resolveId(source) {
if (source === 'my-lib') {
return '/node_modules/my-lib/dist/index.mjs';
}
}
Однако при таком подходе теряется поддержка conditional exports, поэтому обычно требуется делегирование стандартному резолверу:
resolveId(source, importer) {
return this.resolve(source, importer, {
skipSelf: true
});
}
Порядок обработки:
resolveId всех плагиновЕсли хотя бы один плагин возвращает строку, цепочка прерывается.
Rollup кэширует результаты резолвинга для ускорения повторных сборок:
source + importer → один результатОшибки в ручном резолвинге часто приводят к «залипшим» путям из-за кэша.
const aliases = {
'@': '/src',
'~': '/src/shared'
};
resolveId(source) {
for (const key in aliases) {
if (source.startsWith(key)) {
return source.replace(key, aliases[key]);
}
}
}
resolveId(source) {
if (source === 'feature-x') {
return process.env.FEATURE_X === 'true'
? '/src/feature-x/on.js'
: '/src/feature-x/off.js';
}
}
resolveId(source, importer) {
if (source.endsWith('.svg')) {
return this.resolve(source, importer, { skipSelf: true });
}
}
Ручной резолвинг в Rollup требует строгого контроля над:
Любая недетерминированность (например, случайные или time-based решения) приводит к нестабильному графу модулей и неконсистентным бандлам.