В современных JavaScript-проектах часто требуется поддерживать сразу два формата модулей: ESM (ECMAScript Modules) и CJS (CommonJS). Это связано с тем, что экосистема Node.js и фронтенд-инструменты находятся в переходном состоянии: часть библиотек и окружений работает только с CommonJS, а современный стек активно переходит на ESM.
Esbuild предоставляет возможность эффективно решать эту задачу за счёт быстрого бандлинга и встроенной системы генерации различных форматов выходного кода.
ESM (ECMAScript Modules) — стандарт модулей
JavaScript, использующий import и export.
// ESM
export function sum(a, b) {
return a + b;
}
import { sum } from './math.js';
Ключевые особенности:
CommonJS (CJS) — традиционная система модулей Node.js.
// CJS
function sum(a, b) {
return a + b;
}
module.exports = { sum };
const { sum } = require('./math');
Ключевые особенности:
require;Esbuild не создаёт два формата «из одного запуска автоматически», но предоставляет несколько стратегий, позволяющих получить ESM и CJS параллельно или последовательно.
Основные подходы:
format;ESM-выход формируется через параметр:
import * as esbuild from 'esbuild';
esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/index.esm.js',
bundle: true,
format: 'esm',
platform: 'node',
});
Особенности:
format: 'esm' включает генерацию
import/export;.esm.js или
.mjs;import * as esbuild from 'esbuild';
esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/index.cjs.js',
bundle: true,
format: 'cjs',
platform: 'node',
});
Особенности:
require/module.exports;.cjs.js или
.cjs.На практике используется запуск двух сборок в одном процессе.
import * as esbuild from 'esbuild';
async function buildAll() {
await Promise.all([
esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/index.esm.js',
bundle: true,
format: 'esm',
platform: 'node',
}),
esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/index.cjs.js',
bundle: true,
format: 'cjs',
platform: 'node',
}),
]);
}
buildAll();
Ключевая идея:
Для масштабируемых проектов удобно разделять конфигурации.
// build.mjs
import * as esbuild from 'esbuild';
const shared = {
entryPoints: ['src/index.js'],
bundle: true,
platform: 'node',
};
await esbuild.build({
...shared,
outfile: 'dist/index.esm.js',
format: 'esm',
});
await esbuild.build({
...shared,
outfile: 'dist/index.cjs.js',
format: 'cjs',
});
Преимущество подхода:
При генерации двух форматов важно учитывать структуру экспортов.
export function sum(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
Esbuild автоматически преобразует:
exportmodule.exportsTree-shaking работает только в ESM-сборке.
export function used() {}
export function unused() {}
В ESM-выходе:
unused может быть удалён;В CJS-выходе:
Следствие:
При генерации двух форматов важно контролировать
external.
external: ['react', 'lodash']
Различия:
import react from 'react';const react = require('react');Esbuild корректно адаптирует синтаксис автоматически.
Иногда требуется вручную управлять экспортами для обоих форматов.
Пример:
function sum(a, b) {
return a + b;
}
export { sum };
if (typeof module !== 'undefined') {
module.exports = { sum };
}
Однако такой подход обычно избыточен, так как esbuild сам выполняет трансформацию.
package.json для dual-packageПри публикации библиотеки часто применяется схема dual-package.
{
"name": "my-lib",
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js",
"exports": {
"import": "./dist/index.esm.js",
"require": "./dist/index.cjs.js"
}
}
Значение:
require() получает CJS;import получает ESM;При генерации двух форматов одновременно важно учитывать производительность.
metafile: true для анализа;Esbuild позволяет анализировать результат:
const result = await esbuild.build({
entryPoints: ['src/index.js'],
outfile: 'dist/index.esm.js',
bundle: true,
format: 'esm',
metafile: true,
});
console.log(result.metafile);
Это помогает:
Типовые случаи:
Node.js по-разному обрабатывает форматы:
.mjs всегда ESM;.cjs всегда CommonJS;.js зависит от package.json.Esbuild позволяет стандартизировать выходные файлы, избегая неоднозначностей.
При одновременной генерации ESM и CJS следует учитывать:
require() могут вести себя иначе;__dirname без эмуляции;Типичная структура:
src/
index.js
dist/
index.esm.js
index.cjs.js
build.js
Скрипт сборки:
{
"scripts": {
"build": "node build.js"
}
}
Esbuild выполняет:
import/export;При этом логика исходного кода остаётся неизменной — меняется только способ упаковки.
Для корректной генерации двух форматов важно:
default и
module.exports;Пример корректного API:
export function a() {}
export function b() {}
В сложных системах сборка часто разделяется:
Esbuild отвечает только за JS-часть, но легко интегрируется в пайплайн.
Логика dual-build сводится к простому принципу:
Esbuild делает этот процесс быстрым и предсказуемым за счёт минимальной конфигурации и высокой скорости обработки кода.