Практически любой современный проект на JavaScript использует внешние
зависимости. После установки пакетов через npm, pnpm или Yarn в проекте
появляется каталог node_modules, содержащий исходный код
библиотек, вспомогательных утилит и их собственных зависимостей.
Типичная структура проекта выглядит следующим образом:
project/
├── src/
│ ├── main.js
│ └── utils.js
├── package.json
├── package-lock.json
└── node_modules/
Когда приложение импортирует внешний пакет:
import lodash from 'lodash';
файл lodash не находится внутри каталога
src. Он расположен в node_modules, поэтому
Rollup должен понимать, как найти соответствующий пакет и какой файл
использовать в качестве точки входа.
Здесь возникает важная особенность.
Браузер умеет загружать файлы по относительным путям:
import { sum } from './utils.js';
но не умеет самостоятельно интерпретировать имена npm-пакетов:
import lodash from 'lodash';
Строка 'lodash' называется bare import
— импорт без относительного или абсолютного пути.
Для обработки таких импортов Rollup использует специальный механизм резолвинга модулей.
Резолвингом называется процесс определения фактического файла, который соответствует указанному импорту.
Например:
import lodash from 'lodash';
Rollup должен преобразовать это указание в нечто подобное:
node_modules/lodash/lodash.js
или:
node_modules/lodash/index.js
или любой другой файл, определённый самим пакетом.
Алгоритм поиска зависит от:
main;module;exports;Без поддержки резолвинга Rollup способен работать только с локальными файлами.
Например:
import { add } from './math.js';
Такой импорт Rollup найдёт самостоятельно.
Но следующий код вызовет ошибку:
import axios from 'axios';
Поскольку Rollup не знает, где искать пакет axios.
Rollup изначально создавался как инструмент для работы с ES-модулями.
Локальные зависимости имеют очевидное расположение:
import user from './user.js';
import api from '../api.js';
Здесь путь известен заранее.
С пакетами ситуация сложнее.
Допустим, существует импорт:
import React from 'react';
Чтобы найти нужный файл, необходимо:
node_modules/react.package.json.exports.Такой функционал вынесен в отдельный плагин.
Основным инструментом для поиска модулей внутри
node_modules является пакет:
npm install @rollup/plugin-node-resolve --save-dev
Подключение выглядит следующим образом:
import { nodeResolve } from '@rollup/plugin-node-resolve';
export default {
input: 'src/main.js',
output: {
file: 'dist/bundle.js',
format: 'es'
},
plugins: [
nodeResolve()
]
};
После подключения плагина Rollup начинает понимать импорты npm-пакетов:
import lodash from 'lodash';
import axios from 'axios';
import dayjs from 'dayjs';
Без дополнительных настроек большинство популярных библиотек уже будет успешно найдено.
Рассмотрим импорт:
import axios from 'axios';
Плагин выполняет последовательность действий.
Сначала происходит поиск каталога:
node_modules/axios
Если каталог отсутствует, возникает ошибка:
Could not resolve "axios"
Далее открывается файл:
node_modules/axios/package.json
Например:
{
"name": "axios",
"main": "index.js",
"module": "dist/esm/axios.js"
}
Плагин анализирует доступные точки входа.
Если пакет предоставляет ES-модульную сборку:
{
"module": "dist/esm/index.js"
}
то Rollup обычно отдаёт предпочтение ей.
Если ESM-версии нет, может использоваться CommonJS-вариант совместно с другими плагинами.
После завершения поиска импорт превращается в конкретный файл:
node_modules/axios/dist/esm/axios.js
Именно этот модуль участвует в дальнейшем анализе графа зависимостей.
Современные библиотеки часто публикуют две версии кода:
CommonJS
ES Modules
Пример:
{
"main": "dist/index.cjs",
"module": "dist/index.esm.js"
}
Поле:
"module"
указывает на ESM-сборку.
Rollup предпочитает такие файлы, поскольку может:
Поэтому использование module считается оптимальным
вариантом для сборки.
Современные пакеты всё чаще используют поле:
{
"exports": {
".": "./dist/index.js",
"./utils": "./dist/utils.js"
}
}
Оно определяет публичный интерфейс пакета.
Например:
import pkg from 'library';
использует:
".": "./dist/index.js"
А импорт:
import utils from 'library/utils';
использует:
"./utils": "./dist/utils.js"
Плагин node-resolve полностью поддерживает подобную
схему.
Предположим, приложение импортирует:
import axios from 'axios';
Внутри самого Axios присутствуют дополнительные импорты:
import utils from './utils.js';
или:
import followRedirects from 'follow-redirects';
Плагин рекурсивно обрабатывает все найденные зависимости.
В результате формируется полный граф модулей:
main.js
└─ axios
├─ utils
├─ adapters
└─ follow-redirects
Каждый импорт проходит через процедуру резолвинга.
Многие старые библиотеки опубликованы в формате CommonJS.
Например:
const moment = require('moment');
Для работы с такими пакетами обычно используется связка:
npm install \
@rollup/plugin-node-resolve \
@rollup/plugin-commonjs \
--save-dev
Конфигурация:
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
plugins: [
nodeResolve(),
commonjs()
]
};
Последовательность здесь имеет значение.
Сначала:
nodeResolve()
находит пакет.
Затем:
commonjs()
преобразует CommonJS-модули в формат, понятный Rollup.
Некоторые библиотеки публикуют отдельные версии для браузера.
Пример package.json:
{
"main": "index.js",
"browser": "browser.js"
}
Можно указать:
nodeResolve({
browser: true
})
Тогда приоритет получит браузерная версия.
Это особенно полезно при сборке фронтенд-приложений.
Node.js содержит встроенные модули:
fs
path
http
crypto
stream
Например:
import fs from 'fs';
По умолчанию плагин предпочитает встроенные реализации Node.js.
Настройка:
nodeResolve({
preferBuiltins: true
})
является стандартным поведением.
Если необходимо использовать пакет из node_modules, а не
встроенный модуль Node.js:
nodeResolve({
preferBuiltins: false
})
По умолчанию ищутся файлы с типичными расширениями:
.js
.mjs
.json
.node
Список можно расширить:
nodeResolve({
extensions: [
'.js',
'.jsx',
'.ts',
'.tsx'
]
})
После этого становятся возможны импорты без расширений:
import App from './App';
и плагин сможет проверить дополнительные типы файлов.
Иногда необходимо явно определить порядок выбора полей из
package.json.
Пример:
nodeResolve({
mainFields: [
'module',
'main'
]
})
Сначала проверяется:
"module"
затем:
"main"
Можно включать и другие поля:
nodeResolve({
mainFields: [
'browser',
'module',
'main'
]
})
Это позволяет более точно контролировать выбор точки входа.
Монорепозитории часто содержат собственные пакеты.
Структура может выглядеть так:
packages/
├── core
├── ui
└── utils
Импорт:
import { Button } from '@company/ui';
также проходит через механизм node-resolve, если пакет
доступен в графе зависимостей проекта.
Плагин одинаково работает как с внешними библиотеками, так и с локальными пакетами, подключёнными через workspace-механизмы npm, pnpm или Yarn.
Рассмотрим файл:
import lodash from 'lodash';
console.log(lodash.VERSION);
Конфигурация:
export default {
input: 'src/main.js'
};
При сборке появится ошибка:
RollupError:
Could not resolve "lodash"
Rollup понимает наличие импорта, но не знает, где расположен пакет.
После подключения:
plugins: [
nodeResolve()
]
ошибка исчезает, поскольку плагин предоставляет механизм поиска.
Сам по себе node-resolve не удаляет код.
Его задача заключается исключительно в нахождении файлов.
Однако именно благодаря корректному поиску ESM-версий библиотек Rollup получает возможность эффективно выполнять tree shaking.
Например:
import { debounce } from 'lodash-es';
После резолвинга Rollup получает доступ к исходным ES-модулям библиотеки и может включить в бандл только используемые части.
Если же подключается CommonJS-версия:
import _ from 'lodash';
возможности оптимизации становятся заметно ограниченнее.
Наиболее распространённый вариант настройки выглядит следующим образом:
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/main.js',
output: {
dir: 'dist',
format: 'es'
},
plugins: [
nodeResolve({
browser: true
}),
commonjs()
]
};
Такая конфигурация позволяет:
node_modules;exports;