Генерация дополнительных файлов через emitFile

Внутренний механизм Rollup предусматривает возможность динамического создания дополнительных файлов на этапе сборки. Для этого используется контекстный метод плагина this.emitFile, который позволяет добавлять как отдельные чанки, так и статические ассеты, не входящие напрямую в граф исходных модулей. Этот механизм является ключевым для генерации побочных артефактов: CSS-файлов, вспомогательных скриптов, иконок, JSON-данных, воркеров и любых производных ресурсов.

В рамках архитектуры Rollup эмиссия файлов интегрирована в фазу генерации бандла и тесно связана с жизненным циклом плагинов.


Контекст выполнения emitFile

Метод this.emitFile доступен только внутри плагинов и только в определённых хуках:

  • buildStart
  • transform
  • resolveId (в ограниченных сценариях)
  • load
  • renderChunk
  • generateBundle

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


Базовая форма использования

emitFile принимает объект с описанием типа создаваемого ресурса:

this.emitFile({
  type: 'asset',
  name: 'styles.css',
  source: 'body { margin: 0; }'
});

или

this.emitFile({
  type: 'chunk',
  id: './src/worker.js'
});

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

const fileId = this.emitFile({
  type: 'asset',
  name: 'data.json',
  source: JSON.stringify({ a: 1 })
});

Типы создаваемых сущностей

asset

Тип asset используется для генерации статических файлов, не входящих в модульный граф.

this.emitFile({
  type: 'asset',
  fileName: 'runtime-info.txt',
  source: 'generated at build time'
});

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

  • не участвует в tree-shaking
  • не имеет зависимости от модулей
  • может содержать любой бинарный или текстовый контент
  • подходит для CSS, JSON, изображений, метаданных

chunk

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

this.emitFile({
  type: 'chunk',
  id: './src/secondary-entry.js'
});

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

  • анализируется как полноценный модуль
  • проходит через все плагины трансформации
  • участвует в code splitting
  • может иметь зависимости и динамический импорт

Получение имени файла

Созданный файл не имеет фиксированного имени в момент вызова emitFile. Rollup назначает имя на этапе generateBundle. Для получения итогового имени используется:

const referenceId = this.emitFile({
  type: 'asset',
  name: 'config.json',
  source: '{}'
});

const fileName = this.getFileName(referenceId);

getFileName доступен только на этапе генерации, например в generateBundle или renderChunk.


Система ссылок и идентификаторов

Каждый вызов emitFile возвращает referenceId — внутренний ключ, по которому Rollup связывает:

  • логическую сущность
  • итоговое имя файла
  • зависимости между чанками

Пример хранения нескольких файлов:

const cssId = this.emitFile({
  type: 'asset',
  name: 'main.css',
  source: 'body{}'
});

const jsonId = this.emitFile({
  type: 'asset',
  name: 'meta.json',
  source: '{"version":1}'
});

Позже эти идентификаторы используются для построения финального манифеста.


Генерация файлов в generateBundle

Наиболее полный контроль над результатами эмиссии достигается в хуке generateBundle.

generateBundle(options, bundle) {
  const id = this.emitFile({
    type: 'asset',
    name: 'manifest.json',
    source: JSON.stringify(bundle, null, 2)
  });

  const fileName = this.getFileName(id);
}

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


Взаимодействие с renderChunk

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

renderChunk(code, chunk) {
  if (chunk.isEntry) {
    this.emitFile({
      type: 'asset',
      name: `${chunk.name}-meta.json`,
      source: JSON.stringify({
        imports: chunk.imports,
        exports: chunk.exports
      })
    });
  }
}

Это позволяет генерировать метаинформацию для каждого входного модуля.


Поведение при code splitting

При использовании code splitting emitFile становится инструментом создания дополнительных точек расширения:

  • дополнительные чанки могут зависеть от исходных
  • ассеты могут быть привязаны к конкретным чанкам
  • Rollup автоматически оптимизирует порядок вывода

Пример динамического создания вспомогательного чанка:

this.emitFile({
  type: 'chunk',
  id: './src/helper.js'
});

Такой подход часто используется для выделения:

  • воркеров
  • ленивых модулей
  • runtime-утилит

Связь с виртуальными модулями

emitFile часто применяется вместе с виртуальными модулями, когда содержимое создаётся на лету:

resolveId(source) {
  if (source === 'virtual:config') {
    return source;
  }
},

load(id) {
  if (id === 'virtual:config') {
    return `
      export const file = import.meta.ROLLUP_FILE_URL_config;
    `;
  }
}

И далее:

this.emitFile({
  type: 'asset',
  name: 'config.json',
  source: '{}'
});

Rollup автоматически связывает import.meta.ROLLUP_FILE_URL_* с реальным файлом.


Встраивание ссылок на emitFile в код

Rollup предоставляет механизм инъекции ссылок на созданные файлы через специальные шаблоны:

  • import.meta.ROLLUP_FILE_URL_<id>

Пример:

const id = this.emitFile({
  type: 'asset',
  name: 'image.png',
  source: buffer
});

В результате можно получить URL:

export const url = import.meta.ROLLUP_FILE_URL_image;

Это используется для:

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

Ошибки и ограничения

При работе с emitFile существуют ограничения:

  • нельзя полагаться на порядок вызовов
  • нельзя использовать результат до фазы генерации
  • getFileName недоступен в buildStart
  • source должен быть строкой или Uint8Array
  • chunk должен ссылаться на существующий модуль

Некорректные вызовы приводят к ошибкам сборки или неконсистентному графу зависимостей.


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

Генерация манифеста сборки

generateBundle(_, bundle) {
  const manifest = {};

  for (const file of Object.keys(bundle)) {
    manifest[file] = bundle[file].type;
  }

  this.emitFile({
    type: 'asset',
    name: 'manifest.json',
    source: JSON.stringify(manifest)
  });
}

Выделение runtime-файла

this.emitFile({
  type: 'asset',
  name: 'runtime.js',
  source: `console.log('runtime loaded');`
});

Генерация нескольких связанных ресурсов

const ids = [
  this.emitFile({ type: 'asset', name: 'a.txt', source: 'A' }),
  this.emitFile({ type: 'asset', name: 'b.txt', source: 'B' })
];

Роль emitFile в архитектуре плагинов

Внутри системы плагинов Rollup emitFile выступает как:

  • точка расширения графа вывода
  • механизм декомпозиции артефактов
  • инструмент генерации побочных ресурсов
  • способ интеграции с внешними форматами (HTML, CSS, JSON)

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