output.preserveModules для публикации исходной структуры

В стандартном режиме сборки Rollup стремится максимально объединить граф модулей в единый или несколько оптимально разрезанных бандлов. Каждый модуль превращается в часть итогового графа, где границы исходных файлов перестают существовать как физическая структура. Такой подход удобен для приложений, но становится проблемой при публикации библиотек, особенно если важно сохранить модульную архитектуру.

output.preserveModules изменяет это поведение: вместо объединения модулей Rollup сохраняет исходную файловую структуру, превращая каждый модуль в отдельный файл в выходной директории. При этом сохраняются зависимости, имена и относительная структура проекта.

Как работает сохранение модулей

При включении режима:

export default {
  input: 'src/index.js',
  output: {
    dir: 'dist',
    format: 'esm',
    preserveModules: true
  }
}

Rollup перестаёт рассматривать граф модулей как материал для слияния. Вместо этого:

  • каждый исходный файл становится отдельным выходным файлом;
  • структура каталогов из src повторяется в dist;
  • импортные связи остаются через относительные пути;
  • tree-shaking продолжает работать, но на уровне отдельных модулей.

Фактически формируется не бандл, а компиляция файловой системы с применёнными трансформациями Rollup.

Отличие от классического бандлинга

В обычной сборке:

  • все модули схлопываются в один или несколько чанков;
  • внутренние пути исчезают;
  • код становится оптимизированным для загрузки, а не для структуры;
  • отладка исходных файлов зависит от sourcemap.

При preserveModules: true:

  • структура файлов остаётся видимой;
  • каждый модуль экспортируется отдельно;
  • сохраняется гранулярность кода;
  • результат ближе к исходному репозиторию, чем к бандлу.

Это принципиально меняет цель сборки: вместо доставки «готового пакета кода» создаётся «переносимая версия исходной архитектуры».

Где применяется preserveModules

Основной сценарий — публикация библиотек.

Библиотеки с глубокой модульностью

Когда библиотека предоставляет:

  • утилиты (/utils/date, /utils/math);
  • частичные импорты (import x from 'lib/feature');
  • tree-shakable API;

сохранение структуры позволяет пользователю импортировать только нужные части без потери семантики путей.

Design system и UI-kit

Для UI-библиотек важно:

  • разделение компонентов по файлам;
  • возможность точечного импорта;
  • совместимость с bundler-оптимизациями потребителя.

preserveModules делает структуру предсказуемой:

dist/
  button.js
  modal.js
  utils/
    classnames.js

Server-side и ESM-first пакеты

При использовании Node.js ESM:

  • важно сохранять реальные файлы;
  • Node лучше работает с физической структурой модулей;
  • упрощается отладка и трассировка.

Влияние на tree-shaking

Tree-shaking не исчезает, но его характер меняется.

Вместо глобального анализа всего приложения:

  • оптимизация происходит на уровне отдельных файлов;
  • каждый модуль остаётся самостоятельной единицей;
  • удаление неиспользуемого кода зависит от экспортов конкретного файла.

Это означает, что:

  • мелкие утилиты хорошо очищаются;
  • крупные файлы могут сохранять лишний код, если экспорт не атомарный;
  • архитектура исходников становится критически важной.

Взаимодействие с форматом output

На практике preserveModules используется почти исключительно с ESM:

output: {
  dir: 'dist',
  format: 'esm',
  preserveModules: true
}

Причина в том, что:

  • ESM сохраняет статическую структуру импортов;
  • CJS требует runtime-обёрток, что ломает идею сохранения модулей;
  • анализ зависимостей в CJS менее предсказуем.

При попытке использовать с другими форматами могут возникать ограничения или деградация смысла результата.

output.dir вместо output.file

preserveModules несовместим с единым файлом вывода.

Причина проста: невозможно сохранить множество файлов в один output.file.

Поэтому используется:

  • output.dir — обязательный параметр;
  • Rollup создаёт дерево файлов внутри директории.

Это превращает сборку из генерации файла в генерацию каталога.

Влияние на структуру путей

Rollup старается сохранить относительную структуру входных файлов:

src/
  components/
    button.js
  utils/
    format.js

становится:

dist/
  components/
    button.js
  utils/
    format.js

Но поведение может корректироваться через дополнительные настройки.

preserveModulesRoot

Чтобы избежать избыточной вложенности src в dist, используется:

output: {
  dir: 'dist',
  format: 'esm',
  preserveModules: true,
  preserveModulesRoot: 'src'
}

Это позволяет:

  • обрезать корневую директорию;
  • сделать dist зеркалом логической структуры, а не файловой системы разработки;
  • избежать появления лишнего уровня вложенности.

Особенности обработки импортов

При сохранении модулей Rollup переписывает импорты:

  • абсолютные пути превращаются в относительные;
  • сохраняется корректность ссылок между файлами;
  • динамические зависимости могут упрощаться или разделяться.

Например:

import { helper } from '../utils/helper.js'

в итоговой структуре остаётся валидным, но путь может быть пересчитан относительно нового расположения файла.

Ограничения режима

Несмотря на полезность, режим имеет ряд ограничений:

  • сложнее оптимизировать глобально используемые зависимости;
  • увеличивается количество файлов в выходе;
  • возможны дублирования общих вспомогательных модулей;
  • менее эффективен для приложений, чем для библиотек.

Также важно учитывать, что:

  • плагины Rollup могут вести себя иначе при файловом выводе;
  • некоторые оптимизации бандлинга не применяются;
  • код становится ближе к исходному, а не к минимизированному представлению.

Практическая архитектура библиотек

При использовании preserveModules структура проекта становится частью API.

Типичная организация:

src/
  index.js
  core/
  hooks/
  utils/

Именно эта структура:

  • становится публичной;
  • определяет пути импорта у потребителя;
  • влияет на семантику использования библиотеки.

Поэтому проектирование директорий перестаёт быть внутренним вопросом и становится контрактом.

Связь с code splitting

Хотя preserveModules внешне напоминает code splitting, логика противоположная:

  • code splitting создаёт чанки из графа;
  • preserveModules сохраняет граф как набор файлов.

В первом случае результат оптимизирован под загрузку, во втором — под структуру и переиспользование.

Итоговое поведение сборщика

При включённом режиме Rollup выполняет следующий цикл:

  • анализирует граф модулей;
  • применяет трансформации и tree-shaking;
  • не объединяет модули;
  • пишет каждый модуль как отдельный файл;
  • корректирует импорты под новую структуру;
  • сохраняет иерархию каталогов.

В результате получается не бандл, а структурированный набор ESM-модулей, готовый для публикации и точечного импорта.