Генерация манифеста файлов

Манифест файлов в контексте сборки — это структурированный JSON-документ, содержащий соответствие исходных модулей и их результирующих артефактов после бандлинга. В esbuild подобный механизм используется для задач, где требуется точное отслеживание выходных файлов: кеширование, интеграция с CDN, инкрементальные деплои, анализ зависимостей и последующая постобработка сборки.

В отличие от более тяжеловесных сборщиков, esbuild не предоставляет «классического» manifest-режима из коробки в виде отдельного высокоуровневого API-флага, однако аналогичный результат достигается через комбинацию API metafile, плагинов и постобработки результата сборки.


Metafile как основа генерации манифеста

Ключевой механизм, на котором строится генерация манифеста в esbuild, — это опция metafile.

При включении:

import * as esbuild from 'esbuild';

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist',
  metafile: true
});

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

Структура metafile включает:

  • входные точки (entry points)
  • список всех модулей
  • зависимости между модулями
  • выходные файлы
  • байтные размеры
  • связи “вход → выход”

Структура metafile

Фрагмент типичной структуры:

{
  "inputs": {
    "src/index.js": {
      "bytes": 120,
      "imports": ["react", "./app.js"]
    },
    "src/app.js": {
      "bytes": 340,
      "imports": []
    }
  },
  "outputs": {
    "dist/index.js": {
      "imports": ["react"],
      "inputs": ["src/index.js", "src/app.js"],
      "bytes": 5400
    }
  }
}

Ключевые секции:

inputs

  • описывает все входные модули
  • содержит размер, зависимости и тип импорта

outputs

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

Формирование манифеста из metafile

Чтобы превратить metafile в манифест файлов, используется постобработка результата сборки.

Базовая идея заключается в преобразовании секции outputs в плоскую структуру:

import * as esbuild from 'esbuild';
import fs from 'fs';

const result = await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist',
  metafile: true,
  write: true
});

const manifest = {};

for (const [outputFile, data] of Object.entries(result.metafile.outputs)) {
  manifest[outputFile] = {
    inputs: data.inputs,
    bytes: data.bytes,
    imports: data.imports
  };
}

fs.writeFileSync(
  'dist/manifest.json',
  JSON.stringify(manifest, null, 2)
);

Использование абсолютных и относительных путей

esbuild в metafile хранит пути в формате, зависящем от конфигурации сборки:

  • относительные пути — для входов
  • выходные пути — относительно outdir

Это важно учитывать при построении манифеста для production-систем:

Нормализация путей

import path from 'path';

const normalizeManifest = (metafile, outdir) => {
  const result = {};

  for (const [outFile, data] of Object.entries(metafile.outputs)) {
    const normalizedOut = path.relative(outdir, outFile);

    result[normalizedOut] = {
      inputs: data.inputs.map(input =>
        path.normalize(input)
      ),
      bytes: data.bytes
    };
  }

  return result;
};

Манифест для кеширования ассетов

Одна из ключевых задач манифеста — связывание оригинальных файлов с хешированными бандлами.

Пример конфигурации с entryNames:

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist',
  entryNames: '[name]-[hash]',
  assetNames: 'assets/[name]-[hash]',
  metafile: true
});

Результат:

dist/index-a1b2c3.js
dist/manifest.json

Пример манифеста:

{
  "index.js": "index-a1b2c3.js"
}

Генерация манифеста через плагины esbuild

Плагины позволяют автоматически формировать манифест в процессе сборки, без внешнего скрипта.

const manifestPlugin = () => ({
  name: 'manifest-plugin',
  setup(build) {
    let metafile;

    build.onEnd(result => {
      metafile = result.metafile;

      const manifest = {};

      for (const [out, data] of Object.entries(metafile.outputs)) {
        manifest[out] = data.inputs;
      }

      require('fs').writeFileSync(
        'dist/manifest.json',
        JSON.stringify(manifest, null, 2)
      );
    });
  }
});

Использование:

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist',
  metafile: true,
  plugins: [manifestPlugin()]
});

Привязка входных точек к выходным файлам

Одной из типичных задач манифеста является отображение:

  • entry point → final bundle

Для этого анализируется entryPoints и outputs:

const buildEntryManifest = (metafile) => {
  const manifest = {};

  for (const [output, data] of Object.entries(metafile.outputs)) {
    for (const input of data.inputs) {
      if (!manifest[input]) {
        manifest[input] = [];
      }
      manifest[input].push(output);
    }
  }

  return manifest;
};

Манифест для code splitting

При использовании динамических импортов esbuild создаёт несколько чанков:

// src/index.js
import('./lazy.js');

Результат сборки:

index.js
lazy.js

Метаданные:

{
  "outputs": {
    "dist/index.js": {
      "inputs": ["src/index.js"]
    },
    "dist/lazy.js": {
      "inputs": ["src/lazy.js"]
    }
  }
}

Манифест с чанками:

{
  "index.js": {
    "type": "entry",
    "imports": []
  },
  "lazy.js": {
    "type": "chunk",
    "imports": []
  }
}

Интеграция с CDN и деплоем

Манифест часто используется для связывания локальных файлов с удалёнными URL.

Пример трансформации:

const CDN_URL = 'https://cdn.example.com/';

const cdnManifest = {};

for (const [file] of Object.entries(manifest)) {
  cdnManifest[file] = CDN_URL + file;
}

Инкрементальные сборки и обновление манифеста

При использовании esbuild в watch-режиме важно обновлять манифест только при изменениях:

await esbuild.context({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist',
  metafile: true,
  watch: {
    onRebuild(error, result) {
      if (!error) {
        updateManifest(result.metafile);
      }
    }
  }
});

Фильтрация метаданных для уменьшения размера манифеста

Полный metafile может быть избыточным. Для production обычно оставляют только:

  • выходной файл
  • размер
  • список исходных файлов
const slimManifest = (metafile) => {
  const result = {};

  for (const [out, data] of Object.entries(metafile.outputs)) {
    result[out] = {
      size: data.bytes,
      inputs: data.inputs
    };
  }

  return result;
};

Использование манифеста в SSR и фреймворках

В серверном рендеринге манифест применяется для:

  • определения правильного JS-бандла
  • подключения CSS
  • предотвращения дублирования загрузки

Пример:

const manifest = JSON.parse(
  fs.readFileSync('dist/manifest.json', 'utf-8')
);

function getBundle(entry) {
  return manifest[entry];
}

Сопоставление CSS и JS в одном манифесте

При сборке стилей через esbuild:

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outdir: 'dist',
  loader: {
    '.css': 'css'
  },
  metafile: true
});

в outputs появляются CSS-файлы, которые также включаются в манифест:

{
  "dist/index.css": {
    "inputs": ["src/style.css"]
  }
}

Обогащение манифеста дополнительными данными

Манифест может включать:

  • хеш содержимого
  • тип файла (js/css/asset)
  • зависимости

Пример расширенного формирования:

import crypto from 'crypto';
import fs from 'fs';

const hashFile = (filePath) => {
  const buffer = fs.readFileSync(filePath);
  return crypto.createHash('sha256').update(buffer).digest('hex').slice(0, 8);
};

const buildManifest = (metafile) => {
  const manifest = {};

  for (const [file, data] of Object.entries(metafile.outputs)) {
    manifest[file] = {
      hash: hashFile(file),
      size: data.bytes,
      inputs: data.inputs
    };
  }

  return manifest;
};

Ограничения подхода через metafile

Несмотря на гибкость, подход имеет ограничения:

  • отсутствует встроенный стабильный формат манифеста
  • требуется ручная обработка структуры
  • нет семантики “entry/chunk/asset” на уровне API
  • нет встроенной поддержки версионирования манифеста

Поэтому манифест в esbuild всегда является производной структурой, создаваемой поверх metafile, а не самостоятельной функцией сборщика.