Многоэтапная сборка в контексте Esbuild представляет собой организацию процесса, при котором проект проходит через несколько последовательных стадий трансформации. Каждая стадия решает отдельную задачу: компиляцию TypeScript, транспиляцию современного JavaScript, объединение модулей, разделение кода, оптимизацию и финальную минификацию.
Ключевая особенность подхода заключается в том, что Esbuild не
навязывает фиксированный пайплайн. Вместо этого разработчик сам
выстраивает цепочку сборок, комбинируя отдельные вызовы
build() или transform().
Основные причины применения многоэтапной сборки:
Esbuild предоставляет два ключевых API:
build() — полная сборка проекта;transform() — преобразование отдельного файла или
фрагмента кода.Многоэтапность достигается за счёт последовательного выполнения
нескольких build() с разными параметрами:
import * as esbuild from 'esbuild';
// Этап 1: сборка исходного TypeScript в промежуточный JS
await esbuild.build({
entryPoints: ['src/index.ts'],
outdir: 'dist/intermediate',
bundle: false,
platform: 'node',
format: 'esm'
});
// Этап 2: финальная сборка и бандлинг
await esbuild.build({
entryPoints: ['dist/intermediate/index.js'],
outdir: 'dist',
bundle: true,
minify: true,
splitting: true,
format: 'esm'
});
Такой подход позволяет разделять ответственность между стадиями и упрощать контроль над результатом.
Типичная многоэтапная сборка включает несколько логических уровней.
На этом этапе выполняется:
Основная цель — получить чистый JS без объединения модулей.
await esbuild.build({
entryPoints: ['src/app.tsx'],
outdir: 'dist/step1',
bundle: false,
loader: {
'.tsx': 'tsx',
'.ts': 'ts'
}
});
На этом этапе включается bundle: true, и Esbuild
начинает строить граф зависимостей.
await esbuild.build({
entryPoints: ['dist/step1/app.js'],
outdir: 'dist/step2',
bundle: true,
format: 'esm',
splitting: true
});
Особенности:
splitting.Здесь происходит:
await esbuild.build({
entryPoints: ['dist/step2/app.js'],
outdir: 'dist/final',
bundle: true,
minify: true,
sourcemap: true
});
Одним из наиболее распространённых сценариев многоэтапной сборки является разделение target-окружений.
await esbuild.build({
entryPoints: ['src/server/index.ts'],
platform: 'node',
bundle: true,
outdir: 'dist/server',
external: ['express', 'fs']
});
await esbuild.build({
entryPoints: ['src/client/index.tsx'],
platform: 'browser',
bundle: true,
splitting: true,
format: 'esm',
outdir: 'dist/client'
});
Такое разделение позволяет:
Иногда многоэтапность реализуется не через build(), а
через цепочку transform().
import { transform } from 'esbuild';
let code = `
const x: number = 10;
console.log(x);
`;
const step1 = await transform(code, {
loader: 'ts'
});
const step2 = await transform(step1.code, {
minify: true
});
Такой подход используется:
Esbuild Context API позволяет ускорить многоэтапные сборки за счёт переиспользования состояния.
const ctx = await esbuild.context({
entryPoints: ['src/index.ts'],
bundle: true,
outdir: 'dist',
incremental: true
});
await ctx.rebuild();
await ctx.rebuild();
Инкрементальная модель особенно полезна при:
Esbuild позволяет извлекать метаинформацию через
metafile.
await esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
metafile: true,
outfile: 'dist/app.js'
});
Метаданные используются для следующего этапа:
Пример цепочки:
metafile: true;splitting
или external.Многоэтапный процесс часто оформляется как последовательность конфигураций:
const shared = {
logLevel: 'info',
sourcemap: true
};
// этап 1
await esbuild.build({
...shared,
entryPoints: ['src/index.ts'],
bundle: false,
outdir: 'dist/step1'
});
// этап 2
await esbuild.build({
...shared,
entryPoints: ['dist/step1/index.js'],
bundle: true,
outdir: 'dist/step2'
});
// этап 3
await esbuild.build({
...shared,
entryPoints: ['dist/step2/index.js'],
bundle: true,
minify: true,
outdir: 'dist/final'
});
Такой стиль упрощает поддержку и снижает дублирование конфигурации.
В монорепозиториях многоэтапная сборка применяется для:
Пример:
packages/shared;packages/ui, зависящего от shared;await esbuild.build({
entryPoints: ['packages/shared/src/index.ts'],
bundle: true,
outdir: 'dist/shared'
});
await esbuild.build({
entryPoints: ['packages/app/src/index.ts'],
bundle: true,
external: ['shared']
});
Многоэтапность не всегда означает последовательность. Часто этапы выполняются параллельно:
await Promise.all([
esbuild.build({
entryPoints: ['src/client.ts'],
outdir: 'dist/client',
bundle: true
}),
esbuild.build({
entryPoints: ['src/server.ts'],
outdir: 'dist/server',
bundle: true
})
]);
Многоэтапная архитектура может как ускорять, так и замедлять процесс.
Ускорение достигается за счёт:
Замедление возникает из-за:
Поэтому оптимальная стратегия требует баланса между количеством этапов и их стоимостью.
В production-сборках часто выделяются фиксированные этапы:
Каждый этап может быть реализован отдельным вызовом Esbuild, что упрощает отладку и контроль качества артефактов.