При сборке современных веб-приложений одной из ключевых задач становится корректная связка статических HTML-файлов с результатами бандлинга JavaScript и CSS. Инструмент типа esbuild ориентирован на высокую скорость обработки модулей, но сам по себе не занимается полноценной HTML-оркестрацией. Поэтому типичный рабочий процесс включает дополнительный этап: генерацию HTML с подстановкой хешей файлов, обеспечивающих контроль версий и кэш-бастинг.
Хеши в именах файлов позволяют решать сразу несколько задач: предотвращение использования устаревших ресурсов браузером, упрощение инвалидирования кэша и повышение предсказуемости доставки ассетов в продакшене.
При классической схеме сборки результирующий файл может выглядеть как
app.js или bundle.css. Браузер агрессивно
кэширует такие ресурсы, и при обновлении приложения пользователь может
продолжать использовать старую версию скриптов.
Хеширование решает эту проблему путем включения в имя файла уникального идентификатора содержимого:
app.a1b2c3d4.jsstyles.91f8aa22.cssЛюбое изменение содержимого приводит к изменению хеша, а значит — к новому URL. Это гарантирует автоматическое обновление ресурсов без ручного управления кэшем.
esbuild поддерживает шаблоны именования выходных файлов через
параметр entryNames, chunkNames и
assetNames. Именно эти параметры используются для внедрения
хешей.
Пример конфигурации:
import * as esbuild from 'esbuild';
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outdir: 'dist',
entryNames: '[name].[hash]',
chunkNames: 'chunks/[name]-[hash]',
assetNames: 'assets/[name]-[hash]',
metafile: true
});
В результате сборка генерирует не только файлы с хешами, но и метаинформацию, необходимую для дальнейшей генерации HTML.
Опция metafile: true создаёт структурированный
JSON-объект, содержащий карту входных и выходных файлов. Этот файл
является ключевым источником данных для автоматической генерации
HTML.
Пример фрагмента metafile:
{
"outputs": {
"dist/index.8f3a1c.js": {
"entryPoint": "src/index.js",
"inputs": {
"src/index.js": { "bytesInOutput": 1200 }
}
},
"dist/style.77aa12.css": {}
}
}
На основе этой структуры можно определить фактические имена файлов, включая хеши, и автоматически вставить их в HTML-шаблон.
Генерация HTML сводится к замене статических ссылок на вычисленные пути из metafile. Основная задача — извлечь entry point и сопоставить его с итоговым output-файлом.
Алгоритм:
metafile: truesrc/index.js)import fs from 'fs';
function generateHtmlFromMetafile(metafile) {
const outputs = metafile.outputs;
let jsFile = '';
let cssFiles = [];
for (const out in outputs) {
if (out.endsWith('.js') && outputs[out].entryPoint) {
jsFile = out;
}
if (out.endsWith('.css')) {
cssFiles.push(out);
}
}
const cssLinks = cssFiles
.map(file => `<link rel="stylesheet" href="${file}">`)
.join('\n');
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
${cssLinks}
</head>
<body>
<div id="app"></div>
<script type="module" src="${jsFile}"></script>
</body>
</html>
`;
}
const metafile = JSON.parse(fs.readFileSync('dist/meta.json', 'utf-8'));
const html = generateHtmlFromMetafile(metafile);
fs.writeFileSync('dist/index.html', html);
При использовании code splitting появляется несколько JavaScript-файлов. В этом случае важно не просто найти entry point, а корректно обработать зависимости.
Metafile содержит информацию о связях:
Это позволяет построить граф зависимостей и упорядочить подключение файлов.
Пример логики:
function collectJsOutputs(outputs) {
return Object.keys(outputs).filter(file =>
file.endsWith('.js')
);
}
Однако в реальных сценариях требуется учитывать порядок загрузки и динамические импорты.
Генерация HTML редко реализуется вручную. Чаще применяется интеграция с шаблонизаторами:
В таких системах HTML становится шаблоном, а данные из metafile передаются как контекст.
Пример с EJS:
import ejs from 'ejs';
import fs from 'fs';
const template = fs.readFileSync('template.ejs', 'utf-8');
const html = ejs.render(template, {
js: jsFile,
css: cssFiles
});
Шаблон:
<!DOCTYPE html>
<html>
<head>
<% css.forEach(file => { %>
<link rel="stylesheet" href="<%= file %>">
<% }) %>
</head>
<body>
<div id="app"></div>
<script type="module" src="<%= js %>"></script>
</body>
</html>
esbuild поддерживает систему плагинов, через которую можно встроить генерацию HTML прямо в процесс сборки.
Плагин может выполнять действия после завершения build:
let htmlPlugin = {
name: 'html-generator',
setup(build) {
build.onEnd(result => {
const metafile = result.metafile;
const html = generateHtmlFromMetafile(metafile);
fs.writeFileSync('dist/index.html', html);
});
}
};
Подключение:
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outdir: 'dist',
metafile: true,
plugins: [htmlPlugin]
});
В многопейджевых приложениях существует несколько entry points:
home.jsdashboard.jsprofile.jsКаждый из них требует собственного HTML-файла. В этом случае генератор HTML становится фабрикой страниц.
Логика:
Пример:
function generateMultiPage(metafile) {
const pages = {};
for (const out in metafile.outputs) {
const info = metafile.outputs[out];
if (info.entryPoint) {
const name = info.entryPoint.split('/').pop().replace('.js', '');
pages[name] = out;
}
}
return pages;
}
Хеши обеспечивают уникальность, но требуют стабильного связывания между сборками. Важно, чтобы HTML всегда соответствовал актуальному metafile.
Проблемы, которые возникают:
Решения:
Иногда metafile преобразуется в более простой формат:
{
"main": {
"js": "index.8f3a1c.js",
"css": ["style.77aa12.css"]
}
}
Такой manifest облегчает работу HTML-генератора и отделяет сложную структуру esbuild от шаблонизации.
Генерация HTML с хешами часто дополняется оптимизацией загрузки:
Пример:
<link rel="preload" href="index.8f3a1c.js" as="script">
<link rel="stylesheet" href="style.77aa12.css">
<script type="module" src="index.8f3a1c.js" defer></script>
Хешированные файлы особенно эффективны при использовании CDN. Поскольку URL зависит от содержимого, можно включить агрессивное кэширование на уровне CDN без риска устаревших данных.
HTML в этом случае либо:
При включении ассетов в сборку esbuild также может переименовывать их с хешами. HTML-генератор обязан учитывать:
Metafile содержит их как assetFiles, что позволяет
автоматически строить корректные ссылки.
При увеличении размера проекта генерация HTML становится частью build pipeline:
В CI/CD пайплайнах этот процесс выполняется детерминированно, чтобы исключить расхождения между окружениями.
Связка выглядит как поток данных:
Такой подход обеспечивает строгую согласованность между кодом, именами файлов и HTML-документом без ручного вмешательства в ссылки на ассеты.