bundle.write(): запись на диск

Метод bundle.write() в программном API Rollup отвечает за запись уже сгенерированного бандла на файловую систему. Он является частью низкоуровневого workflow, в котором процесс сборки разделён на два этапа: генерацию (generate) и запись (write). Такой подход позволяет контролировать момент физического вывода файлов, отделяя вычисление графа зависимостей и кодогенерацию от операций ввода-вывода.

Метод доступен на экземпляре бандла, возвращаемого функцией rollup(), и применяется после выполнения bundle.generate() или в качестве самостоятельного шага, когда Rollup сам выполняет генерацию и запись в одном вызове.


Общая сигнатура и поведение

bundle.write(outputOptions)

Метод принимает объект outputOptions, который соответствует типу OutputOptions и определяет, как именно будет сформирован итоговый набор файлов: куда записывать, в каком формате, с какими плагинами и настройками оптимизации.

Возвращаемое значение — Promise<RollupOutput>, который резолвится после завершения записи всех файлов и ассетов на диск.


Разница между write() и generate()

Методы generate() и write() имеют общую фазу подготовки бандла, но различаются финальным этапом:

  • generate() Формирует структуру выходного бандла в памяти и возвращает объект с чанками и ассетами, не выполняя запись на диск.

  • write() Выполняет ту же генерацию, но дополнительно записывает результат в файловую систему.

Ключевое отличие заключается в том, что write() включает слой emitFilefinalizeAssetsfs.writeFile через внутренний пайплайн Rollup.


Структура outputOptions

outputOptions управляет всем процессом формирования выходных файлов. Наиболее значимые поля:

  • dir — директория вывода для мульти-чанк сборки
  • file — путь к единственному выходному файлу (используется при single-file bundle)
  • format — формат модуля (esm, cjs, iife, umd, system)
  • sourcemap — генерация sourcemap (true, false, 'inline')
  • entryFileNames — шаблон имени входных чанков
  • chunkFileNames — шаблон имени динамических чанков
  • assetFileNames — шаблон имени ассетов
  • plugins — output-плагины
  • globals — внешние зависимости для IIFE/UMD

Пример базовой конфигурации:

await bundle.write({
  dir: 'dist',
  format: 'esm',
  sourcemap: true,
  entryFileNames: '[name]-[hash].js',
  chunkFileNames: 'chunks/[name]-[hash].js',
  assetFileNames: 'assets/[name]-[hash][extname]'
});

Внутренний процесс выполнения write()

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

1. Подготовка output-конфигурации

Rollup нормализует outputOptions, применяет дефолтные значения и валидирует конфликтующие параметры (например, одновременное использование file и dir в неподходящем контексте).

2. Генерация бандла

Если ранее не был вызван generate(), Rollup выполняет полный процесс построения графа модулей и генерации чанков.

3. Применение output-плагинов

На этапе генерации выходных данных активируются output-хуки плагинов:

  • renderStart
  • renderChunk
  • generateBundle
  • writeBundle

Эти хуки могут модифицировать код чанков или добавлять виртуальные ассеты.

4. Формирование структуры файлов

Rollup определяет:

  • список entry chunks
  • список динамических chunks
  • список asset-файлов (CSS, изображения, текстовые ресурсы)

5. Запись на диск

Файлы записываются через Node.js fs API:

  • создаются директории
  • выполняется writeFile для каждого чанка
  • ассеты сериализуются отдельно
  • sourcemap пишется как .map файл или инлайнится

Работа с одиночным файлом (file) и директориями (dir)

Метод write() строго различает два режима вывода.

Режим file

Используется при генерации одного бандла:

await bundle.write({
  file: 'dist/app.js',
  format: 'iife'
});

Особенности:

  • допускается только один entry chunk
  • динамические импорты могут быть запрещены или инлайнены
  • все зависимости должны быть разрешены в один файл

Режим dir

Используется для code-splitting:

await bundle.write({
  dir: 'dist',
  format: 'esm'
});

Особенности:

  • создаётся множество файлов
  • поддерживаются динамические import()
  • чанки раскладываются по шаблонам именования

Асинхронность и Promise-цепочка

bundle.write() полностью асинхронен и возвращает Promise. Внутри используется последовательность асинхронных операций:

  • построение графа модулей (если требуется)
  • генерация кода чанков
  • сериализация ассетов
  • запись файлов
  • вызов writeBundle

Это позволяет использовать метод в CI/CD пайплайнах без блокировки основного потока Node.js.


Работа с sourcemap

При включённой генерации sourcemap Rollup формирует отдельные .map файлы или инлайн-карты.

await bundle.write({
  dir: 'dist',
  format: 'esm',
  sourcemap: true
});

Поведение:

  • для каждого чанка создаётся соответствующий map
  • source content может быть встроен или ссылаться на исходники
  • плагины могут модифицировать sourcemap через renderChunk

Output-плагины в контексте write()

Плагины получают расширенный контроль над финальной стадией сборки.

generateBundle

Позволяет модифицировать набор файлов до записи:

generateBundle(options, bundle) {
  delete bundle['unused.js'];
}

writeBundle

Срабатывает после записи всех файлов:

writeBundle(options, bundle) {
  console.log('Сборка завершена');
}

Важно: на этом этапе файлы уже записаны, поэтому изменения не влияют на результат.


Обработка ассетов

Ассеты обрабатываются отдельно от JS-чанков.

Типичные ассеты:

  • изображения
  • CSS-файлы
  • шрифты
  • JSON и текстовые ресурсы

Процесс:

  1. ассет регистрируется через emitFile
  2. Rollup определяет имя через assetFileNames
  3. содержимое сериализуется в Buffer или строку
  4. записывается через fs.writeFile

Инкрементальная сборка и write()

В режиме watch API метод write() может вызываться многократно. При этом:

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

Это снижает стоимость повторной сборки в дев-среде.


Типичные ошибки при использовании write()

Конфликт file и dir

Недопустимо смешивать режимы вывода:

await bundle.write({
  file: 'dist/app.js',
  dir: 'dist'
});

Несовместимость формата и code splitting

Некоторые форматы ограничивают мульти-чанк поведение:

  • iife — только один файл
  • umd — ограниченная поддержка split chunks

Отсутствие прав на файловую систему

При записи в защищённые директории возникает ошибка EACCES.


Взаимодействие с bundle.close()

После завершения write() рекомендуется освобождать ресурсы:

await bundle.write(options);
await bundle.close();

close():

  • освобождает файловые watchers
  • очищает внутренний кэш
  • закрывает плагины

Производственные сценарии использования

Метод write() применяется в:

  • сборках production-бандлов
  • CI/CD пайплайнах
  • генерации библиотек
  • SSR-сборках
  • монорепозиториях с разделённым выводом пакетов

В таких сценариях важно учитывать детерминированность имен файлов и стабильность sourcemap.


Влияние плагинов на результат записи

Плагины могут полностью изменить итоговую структуру файлов:

  • добавление виртуальных entry points
  • генерация дополнительных ассетов
  • переименование чанков
  • агрегация CSS
  • инжект runtime-кода

Таким образом write() становится не просто операцией записи, а финальной точкой трансформационного пайплайна сборки.