Стратегия dual package (ESM + CJS) в Rollup основана на одновременной генерации двух форматов модуля: ECMAScript Modules (ESM) и CommonJS (CJS). Такая схема используется для обеспечения совместимости библиотеки с разными окружениями: современными сборщиками и браузерами, которые предпочитают ESM, и Node.js-экосистемой, где до сих пор широко используется CommonJS.
ESM и CJS имеют фундаментальные различия в механизме загрузки и исполнения модулей.
CommonJS:
require)ESM:
import)Библиотека, выпускаемая только в одном формате, неизбежно ограничивает свою применимость. Dual package позволяет:
Rollup позволяет генерировать несколько выходов из одного
конфигурационного файла через массив output.
Типичная конфигурация:
export default {
input: 'src/index.js',
output: [
{
file: 'dist/index.cjs',
format: 'cjs',
exports: 'auto',
sourcemap: true
},
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true
}
]
};
Ключевой момент — Rollup не требует дублирования входной точки. Один graph модулей используется для построения нескольких вариантов вывода.
На практике dual package почти всегда разделяется по директориям:
dist/
esm/
index.js
cjs/
index.cjs
Это снижает риск конфликтов и упрощает настройку
package.json.
Конфигурация:
export default {
input: 'src/index.js',
output: [
{
dir: 'dist/esm',
format: 'esm',
preserveModules: true,
sourcemap: true
},
{
dir: 'dist/cjs',
format: 'cjs',
exports: 'auto',
preserveModules: true,
sourcemap: true
}
]
};
preserveModules сохраняет структуру исходных файлов при
сборке.
Без него Rollup объединяет весь граф в один или несколько бандлов. С ним:
Особенно важно для библиотек, которые:
Однако preserveModules увеличивает количество файлов в
сборке, что требует аккуратной настройки package.json.
Современный подход — использование поля exports:
{
"name": "my-lib",
"type": "module",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.cjs"
}
}
}
Ключевые особенности:
import указывает на ESM-версиюrequire указывает на CJS-версиюДополнительно можно указать типы:
{
"types": "./dist/types/index.d.ts"
}
Rollup должен корректно обрабатывать различия модулей. Основные сложности:
CommonJS не имеет нативных named exports, поэтому Rollup эмулирует их:
module.exports = {
foo: 1,
bar: 2
};
В ESM:
import { foo } from 'lib';
Rollup использует трансформацию через exports или
interop.
Настройка:
output: {
format: 'cjs',
exports: 'named'
}
или
exports: 'auto'
auto выбирает стратегию в зависимости от структуры
модуля.
В dual package важно исключить зависимости из бандла:
external: ['react', 'lodash']
При необходимости можно использовать функцию:
external: (id) => id.startsWith('react')
Это особенно важно, потому что:
Необходим для преобразования CJS-зависимостей в ESM:
import commonjs from '@rollup/plugin-commonjs';
Без него многие npm-пакеты не будут корректно работать в ESM-сборке.
Обеспечивает резолвинг модулей из node_modules:
import resolve from '@rollup/plugin-node-resolve';
Часто используется в связке с CommonJS.
При использовании TypeScript dual package стратегия усложняется:
import typescript from '@rollup/plugin-typescript';
Типичная проблема — генерация типов только один раз:
tscrollup-plugin-dtsРекомендуемый подход — отдельный этап сборки типов.
Существует два подхода:
Плюсы:
Минусы:
// rollup.config.cjs.js
// rollup.config.esm.js
Плюсы:
Минусы:
Для корректного tree-shaking важно указать:
{
"sideEffects": false
}
или более точно:
{
"sideEffects": [
"*.css"
]
}
Это влияет на ESM-сборку, так как именно она используется большинством bundler’ов для анализа дерева зависимостей.
output: {
format: 'esm'
}
Особенности:
output: {
format: 'cjs'
}
Особенности:
require/module.exportsВозникает при отсутствии external или неправильной конфигурации resolve.
CJS не всегда корректно транслируется в ESM интерфейс.
ESM и CJS могут по-разному обрабатывать:
Node может выбрать не тот entry point при отсутствии
exports.
src/
index.js
utils/
dist/
esm/
cjs/
rollup.config.js
package.json
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
export default {
input: 'src/index.js',
external: ['react'],
plugins: [
resolve(),
commonjs()
],
output: [
{
dir: 'dist/esm',
format: 'esm',
sourcemap: true,
preserveModules: true
},
{
dir: 'dist/cjs',
format: 'cjs',
exports: 'auto',
sourcemap: true,
preserveModules: true
}
]
};
При выпуске библиотеки важно сохранять:
Разные версии сборки не должны расходиться по поведению, иначе пользователи получат непредсказуемые баги в зависимости от способа импорта.
exports mapКритически важно, чтобы оба формата проходили одинаковые тесты, иначе dual package теряет смысл как единая библиотека.
exports mapDual package становится стандартом для публичных библиотек, так как обеспечивает баланс между legacy и современными системами модулей.