Поле output.preserveModules управляет стратегией
генерации выходных файлов в Rollup. По умолчанию Rollup старается
объединять модули в минимальное количество бандлов, оптимизируя размер и
уменьшая количество файлов. При включении preserveModules
поведение меняется: структура модулей сохраняется, а каждый исходный
модуль превращается в отдельный выходной файл.
Это особенно важно для библиотек, SDK, монорепозиториев и пакетов, где требуется:
Исходная структура:
src/
├── index.js
├── math/
│ ├── add.js
│ └── sub.js
└── utils/
└── format.js
Конфигурация:
export default {
input: 'src/index.js',
output: {
dir: 'dist',
format: 'es',
preserveModules: true
}
};
После сборки:
dist/
├── index.js
├── math/
│ ├── add.js
│ └── sub.js
└── utils/
└── format.js
Каждый модуль остаётся отдельным файлом.
preserveModulesСтандартная сборка:
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'es'
}
};
Результат:
dist/
└── bundle.js
Все модули объединяются в один файл.
file и
dirПри использовании preserveModules нельзя применять поле
output.file.
Неверно:
output: {
file: 'dist/bundle.js',
preserveModules: true
}
Rollup выдаст ошибку.
Правильно:
output: {
dir: 'dist',
preserveModules: true
}
Причина очевидна: при сохранении модулей генерируется множество файлов, а не один бандл.
Rollup повторяет дерево исходных файлов относительно точки входа.
Пример:
src/
├── components/
│ ├── button.js
│ └── modal.js
└── core/
└── config.js
Выход:
dist/
├── components/
│ ├── button.js
│ └── modal.js
└── core/
└── config.js
Импорты автоматически переписываются:
Исходный код:
import { config } from '../core/config.js';
После сборки:
import { config } from '../core/config.js';
Пути корректируются относительно выходной структуры.
preserveModules особенно полезен при публикации
библиотек.
Пример структуры npm-пакета:
dist/
├── index.js
├── hooks/
│ ├── useFetch.js
│ └── useStorage.js
└── utils/
└── debounce.js
Пользователь может импортировать отдельные части:
import debounce from 'my-lib/utils/debounce.js';
или:
import { useFetch } from 'my-lib/hooks/useFetch.js';
Это снижает размер итогового бандла приложения потребителя.
При обычной сборке Rollup может объединять код таким образом, что отдельные части библиотеки становятся менее изолированными.
preserveModules позволяет сохранить модульные границы,
благодаря чему:
смогут эффективнее удалять неиспользуемый код.
Пример:
export default {
input: [
'src/index.js',
'src/admin.js'
],
output: {
dir: 'dist',
format: 'es',
preserveModules: true
}
};
Rollup создаст структуру, содержащую все зависимости обеих точек входа.
preserveEntrySignaturesПри работе с preserveModules часто используется:
preserveEntrySignatures: 'strict'
Пример:
export default {
input: 'src/index.js',
preserveEntrySignatures: 'strict',
output: {
dir: 'dist',
format: 'es',
preserveModules: true
}
};
Это помогает сохранить экспортные контракты модулей без изменений.
preserveModules работает не только с ES Modules.
Пример:
export default {
input: 'src/index.js',
output: {
dir: 'dist',
format: 'cjs',
preserveModules: true
}
};
Результат:
'use strict';
const add = require('./math/add.js');
Каждый модуль сохраняется отдельно, но синтаксис преобразуется в CommonJS.
preserveModulesRootБез дополнительной настройки Rollup может включать лишние каталоги в итоговую структуру.
Пример:
src/
└── library/
├── index.js
└── utils.js
Конфигурация:
output: {
dir: 'dist',
preserveModules: true
}
Результат:
dist/
└── library/
├── index.js
└── utils.js
Иногда требуется убрать промежуточный каталог
library.
Для этого используется:
output: {
dir: 'dist',
preserveModules: true,
preserveModulesRoot: 'src/library'
}
Теперь результат:
dist/
├── index.js
└── utils.js
Частый сценарий:
import typescript from '@rollup/plugin-typescript';
export default {
input: 'src/index.ts',
plugins: [
typescript()
],
output: {
dir: 'dist',
format: 'es',
preserveModules: true
}
};
Исходная структура:
src/
├── index.ts
├── api/
│ └── client.ts
└── helpers/
└── parse.ts
После сборки:
dist/
├── index.js
├── api/
│ └── client.js
└── helpers/
└── parse.js
Даже при preserveModules Rollup всё равно может
создавать дополнительные shared chunks.
Причина — повторно используемые зависимости.
Пример:
src/
├── a.js
├── b.js
└── shared.js
Если a.js и b.js используют
shared.js, Rollup может вынести код в отдельный чанк.
Иногда требуется абсолютное соответствие структуре исходников.
Для этого используют:
output: {
preserveModules: true,
hoistTransitiveImports: false
}
Однако полностью отключить внутреннюю оптимизацию Rollup невозможно без дополнительных плагинов и архитектурных ограничений.
Исходники:
src/
├── index.js
├── components/
│ ├── button/
│ │ ├── index.js
│ │ └── styles.js
│ └── modal/
│ ├── index.js
│ └── animations.js
├── hooks/
│ ├── useFetch.js
│ └── useTheme.js
└── utils/
├── debounce.js
└── throttle.js
Конфигурация:
export default {
input: 'src/index.js',
output: {
dir: 'dist',
format: 'es',
preserveModules: true,
preserveModulesRoot: 'src'
}
};
Результат:
dist/
├── index.js
├── components/
│ ├── button/
│ │ ├── index.js
│ │ └── styles.js
│ └── modal/
│ ├── index.js
│ └── animations.js
├── hooks/
│ ├── useFetch.js
│ └── useTheme.js
└── utils/
├── debounce.js
└── throttle.js
preserveModules и code splitting решают разные
задачи.
Цель:
Результат:
dist/
├── main.js
├── vendor.js
└── chunk-XYZ.js
Цель:
Результат:
dist/
├── utils/
├── hooks/
└── components/
Часто preserveModules используется вместе с
external.
Пример:
export default {
input: 'src/index.js',
external: ['react'],
output: {
dir: 'dist',
format: 'es',
preserveModules: true
}
};
Rollup не будет включать React в выходные файлы.
Распространённая практика — создание одновременно ESM и CommonJS-версий.
Пример:
export default [
{
input: 'src/index.js',
output: {
dir: 'dist/esm',
format: 'es',
preserveModules: true
}
},
{
input: 'src/index.js',
output: {
dir: 'dist/cjs',
format: 'cjs',
preserveModules: true
}
}
];
Структура:
dist/
├── esm/
│ ├── index.js
│ └── utils/
└── cjs/
├── index.js
└── utils/
preserveModulesВместо одного бандла могут генерироваться сотни файлов.
Это приводит к:
Некоторые старые инструменты плохо работают с глубокой модульной структурой.
Особенно это касается:
Необходимо аккуратно настраивать:
package.json;exports;package.json exportsПример:
{
"exports": {
".": "./dist/index.js",
"./utils/*": "./dist/utils/*.js",
"./hooks/*": "./dist/hooks/*.js"
}
}
Это позволяет официально экспортировать внутренние модули.
Sourcemaps работают корректно:
output: {
dir: 'dist',
format: 'es',
preserveModules: true,
sourcemap: true
}
Результат:
dist/
├── index.js
├── index.js.map
├── utils/
│ ├── helper.js
│ └── helper.js.map
preserveModules использовать не стоитПоле редко применяется в:
В подобных случаях эффективнее стандартный code splitting.
import typescript from '@rollup/plugin-typescript';
export default {
input: 'src/index.ts',
external: [
'react',
'react-dom'
],
plugins: [
typescript({
declaration: true,
declarationDir: 'dist/types'
})
],
output: {
dir: 'dist/esm',
format: 'es',
preserveModules: true,
preserveModulesRoot: 'src',
sourcemap: true
}
};
Такой подход обеспечивает: