Любая система модулей сталкивается с необходимостью определить, какой
именно файл соответствует указанному пути в инструкции
import или export. Этот процесс называется
резолвингом модулей (module resolution).
Когда встречается конструкция:
import { format } from './utils.js';
или
import React from 'react';
Rollup должен определить:
От корректности алгоритма поиска зависит возможность сборки проекта, эффективность анализа зависимостей и успешность выполнения tree shaking.
При запуске сборки Rollup начинает с входного файла:
export default {
input: 'src/main.js'
};
После открытия файла анализируются все инструкции импорта:
import './polyfills.js';
import { api } from './services/api.js';
import { render } from './ui/render.js';
Для каждого импорта выполняется отдельный цикл поиска.
Упрощённо процесс выглядит следующим образом:
main.js
├─ polyfills.js
├─ api.js
│ └─ config.js
└─ render.js
├─ button.js
└─ modal.js
Rollup постепенно строит полный граф зависимостей проекта.
Наиболее простой случай — относительный импорт.
Исходный файл:
src/main.js
Импорт:
import './utils.js';
Rollup интерпретирует путь относительно текущего файла.
Если файл расположен здесь:
src/
├─ main.js
└─ utils.js
то будет найден:
src/utils.js
Для конструкции:
import '../config.js';
поиск производится на уровень выше:
project/
├─ config.js
└─ src/
└─ main.js
Результатом станет:
project/config.js
Относительные пути всегда начинаются с:
./
../
Поэтому Rollup может сразу определить, что необходимо искать локальный файл.
В некоторых конфигурациях могут использоваться абсолютные пути:
import '/src/core.js';
Интерпретация зависит от среды выполнения и настроек плагинов.
Сам Rollup не занимается полноценной обработкой веб-путей и обычно ожидает реальные пути файловой системы.
Поэтому абсолютные импорты часто требуют дополнительной настройки
через плагины или параметр resolveId.
Особый случай представляет импорт пакета:
import lodash from 'lodash';
Здесь отсутствуют:
./
../
/
Следовательно, Rollup понимает, что требуется поиск внешнего пакета.
В отличие от Node.js, ядро Rollup не реализует полный алгоритм поиска пакетов самостоятельно. Для этого обычно используется специальный плагин резолвинга.
Наиболее распространённый вариант:
import resolve from '@rollup/plugin-node-resolve';
Без него Rollup не сможет определить расположение большинства npm-пакетов.
Рассмотрим импорт:
import React from 'react';
Физический путь может выглядеть так:
node_modules/
└─ react/
├─ package.json
├─ index.js
└─ cjs/
Rollup видит лишь строку:
'react'
Для преобразования её в конкретный файл требуется алгоритм поиска пакета.
Именно его реализует плагин:
plugins: [
resolve()
]
После подключения Rollup начинает работать по правилам, близким к Node.js.
Предположим, имеется импорт:
import dayjs from 'dayjs';
Алгоритм упрощённо выполняет следующие действия:
node_modules.dayjs.Пример структуры:
node_modules/
└─ dayjs/
├─ package.json
└─ dayjs.min.js
После обнаружения пакета поиск продолжается внутри него.
Основная информация о точке входа находится в файле:
{
"name": "my-library",
"main": "dist/index.js"
}
При обнаружении пакета Rollup анализирует его метаданные.
Если найдено:
{
"main": "dist/index.js"
}
то фактическим модулем станет:
dist/index.js
а не корень каталога.
Современные библиотеки часто содержат несколько сборок.
Например:
{
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js"
}
Здесь:
main предназначен для CommonJS;module содержит ES-модульную версию.Для Rollup предпочтительнее использовать ESM-сборку, поскольку она лучше подходит для tree shaking.
Поэтому при наличии поля:
{
"module": "dist/index.esm.js"
}
обычно выбирается именно оно.
Современный стандарт Node.js ввёл механизм:
{
"exports": {
".": "./dist/index.js",
"./utils": "./dist/utils.js"
}
}
Теперь доступные точки входа определяются явно.
Импорт:
import lib from 'library';
будет преобразован в:
library/dist/index.js
А импорт:
import utils from 'library/utils';
приведёт к:
library/dist/utils.js
Если путь отсутствует в разделе exports, пакет может
оказаться недоступным для импорта, даже если файл физически
существует.
Импорт может выглядеть так:
import helper from './helper';
Файл же называется:
helper.js
Алгоритм резолвинга способен проверять допустимые расширения.
Обычно перебираются варианты:
helper.js
helper.mjs
helper.cjs
helper.json
Конкретный список зависит от настроек и используемых плагинов.
Если импорт указывает на каталог:
import config from './config';
структура проекта:
config/
└─ index.js
то возможна автоматическая подстановка:
config/index.js
Подобный механизм давно используется в экосистеме Node.js и поддерживается большинством инструментов сборки.
После нахождения первого файла процесс не заканчивается.
Например:
import { start } from './app.js';
Файл:
import { api } from './api.js';
Файл:
import { request } from './request.js';
Получается цепочка:
main.js
↓
app.js
↓
api.js
↓
request.js
Каждый найденный модуль запускает новый цикл анализа и поиска зависимостей.
Внутри Rollup создаётся ориентированный граф.
Пример:
main.js
├─ auth.js
│ ├─ api.js
│ └─ storage.js
└─ ui.js
└─ modal.js
Каждая вершина графа представляет модуль.
Каждое ребро показывает связь через импорт.
На основе этого графа затем выполняются:
Поиск файлов может выполняться тысячи раз.
В большом приложении граф способен содержать:
1000+
модулей
Повторный обход файловой системы для каждого импорта привёл бы к серьёзным затратам времени.
Поэтому Rollup сохраняет результаты резолвинга во внутреннем кэше:
"./utils.js"
↓
"/project/src/utils.js"
При повторном обращении используется уже известный путь.
Это существенно ускоряет сборку.
В некоторых проектах используются symlink-ссылки:
packages/
└─ shared
node_modules/
└─ shared -> ../. ./packages/shared
Такой подход распространён в монорепозиториях.
Во время резолвинга Rollup может:
Поведение зависит от конфигурации и настроек резолвера.
Структура монорепозитория может выглядеть так:
packages/
├─ core
├─ ui
└─ utils
Пакет:
import { log } from '@project/utils';
может находиться не в обычном node_modules, а внутри
рабочей области (workspace).
Современные инструменты:
npm workspaces
pnpm
yarn workspaces
создают специальные ссылки между пакетами.
Алгоритм поиска должен корректно учитывать такую структуру.
Иногда модуль не должен включаться в сборку.
Конфигурация:
export default {
external: ['react']
};
Импорт:
import React from 'react';
будет найден и распознан, но содержимое пакета не попадёт в итоговый bundle.
Rollup сохранит импорт во внешнем виде:
import React from 'react';
или преобразует его в формат, соответствующий целевому модульному стандарту.
Одной из важнейших точек расширения является хук:
resolveId
Пример:
export default {
plugins: [
{
name: 'virtual',
resolveId(id) {
if (id === 'config') {
return '\0virtual-config';
}
}
}
]
};
Теперь импорт:
import config from 'config';
будет перенаправлен на виртуальный модуль.
Таким образом можно реализовывать:
Иногда один и тот же импорт может соответствовать нескольким вариантам.
Например:
helper.js
helper.mjs
helper.cjs
При отсутствии явного расширения резолвер обязан выбрать один файл согласно своим правилам приоритета.
Поэтому в крупных проектах часто рекомендуется писать расширения явно:
import helper from './helper.js';
Это снижает вероятность неоднозначностей и делает граф зависимостей более предсказуемым.
Наиболее распространённые проблемы:
Отсутствующий файл:
import './unknown.js';
Ошибка:
Could not resolve "./unknown.js"
Отсутствующий пакет:
import axios from 'axios';
при неустановленной зависимости приводит к ошибке поиска пакета.
Неверный экспорт:
import x from 'library/internal';
может вызвать ошибку, если путь не разрешён через поле
exports.
Качество tree shaking напрямую зависит от успешного определения всех модулей.
После построения полного графа Rollup знает:
какой модуль
↓
какой экспортирует символ
↓
кто этот символ использует
Только после завершения резолвинга становится возможным анализ неиспользуемого кода.
По этой причине алгоритм поиска модулей является одной из фундаментальных частей архитектуры Rollup. Он связывает входной файл, пакеты, локальные модули и плагины в единый граф зависимостей, который затем используется всеми последующими этапами сборки.