Манифест файлов в контексте сборки — это структурированный
JSON-документ, содержащий соответствие исходных модулей и их
результирующих артефактов после бандлинга. В esbuild
подобный механизм используется для задач, где требуется точное
отслеживание выходных файлов: кеширование, интеграция с CDN,
инкрементальные деплои, анализ зависимостей и последующая постобработка
сборки.
В отличие от более тяжеловесных сборщиков, esbuild не
предоставляет «классического» manifest-режима из коробки в виде
отдельного высокоуровневого API-флага, однако аналогичный результат
достигается через комбинацию API metafile, плагинов и
постобработки результата сборки.
Ключевой механизм, на котором строится генерация манифеста в
esbuild, — это опция metafile.
При включении:
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outdir: 'dist',
metafile: true
});
в результате возвращается объект metafile, содержащий
подробную информацию о графе модулей и выходных файлах.
Структура 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 в манифест файлов,
используется постобработка результата сборки.
Базовая идея заключается в преобразовании секции 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"
}
Плагины позволяют автоматически формировать манифест в процессе сборки, без внешнего скрипта.
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()]
});
Одной из типичных задач манифеста является отображение:
Для этого анализируется 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;
};
При использовании динамических импортов 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": []
}
}
Манифест часто используется для связывания локальных файлов с удалёнными 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;
};
В серверном рендеринге манифест применяется для:
Пример:
const manifest = JSON.parse(
fs.readFileSync('dist/manifest.json', 'utf-8')
);
function getBundle(entry) {
return manifest[entry];
}
При сборке стилей через 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"]
}
}
Манифест может включать:
Пример расширенного формирования:
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;
};
Несмотря на гибкость, подход имеет ограничения:
Поэтому манифест в esbuild всегда является производной
структурой, создаваемой поверх metafile, а не
самостоятельной функцией сборщика.