Бандлинг серверного кода представляет собой процесс объединения множества файлов приложения и зависимостей в один или несколько готовых артефактов для выполнения в среде Node.js. В отличие от фронтенд-разработки, где бандлинг ориентирован на браузеры, серверная сборка учитывает особенности платформы Node.js, систему модулей, встроенные API и требования к деплою.
Esbuild предоставляет высокопроизводительный механизм сборки серверных приложений благодаря архитектуре, написанной на Go, и эффективному анализу графа зависимостей.
Основные задачи серверного бандлинга:
Исходная структура проекта:
src/
├── index.js
├── database.js
├── services/
│ └── userService.js
└── utils/
└── logger.js
Файл входа:
import { createUser } from "./services/userService.js";
createUser();
Команда сборки:
npx esbuild src/index.js \
--bundle \
--platform=node \
--outfile=dist/app.js
После выполнения команды создаётся файл:
dist/
└── app.js
Внутри него содержится весь код приложения и локальных зависимостей.
Для серверного кода практически всегда используется настройка:
platform: "node"
или
--platform=node
Пример конфигурации:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
outfile: "dist/app.js"
});
Этот режим сообщает Esbuild, что код будет запускаться в Node.js.
В результате:
Рассмотрим пример:
import fs from "fs";
import path from "path";
console.log(fs.existsSync(path.resolve("./")));
Конфигурация:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
outfile: "dist/app.js"
});
Модули:
fs
path
http
https
crypto
stream
events
не будут встроены в итоговый бандл.
В результирующем коде останутся обращения к системным API Node.js.
Это уменьшает размер сборки и сохраняет совместимость с платформой.
Esbuild умеет генерировать код под конкретную версию Node.
Например:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
target: "node20",
outfile: "dist/app.js"
});
Или:
npx esbuild src/index.js \
--bundle \
--platform=node \
--target=node20 \
--outfile=dist/app.js
Возможные значения:
node14
node16
node18
node20
node22
Чем современнее целевая версия, тем меньше преобразований требуется выполнять Esbuild.
Node.js поддерживает несколько систем модулей:
Esbuild способен генерировать оба варианта.
Конфигурация:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
format: "cjs",
outfile: "dist/app.js"
});
Результат:
const service = require("./service");
Такой формат используется в большинстве существующих серверных приложений.
Конфигурация:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
format: "esm",
outfile: "dist/app.js"
});
Результирующий код использует:
import ...
export ...
Для работы обычно требуется:
{
"type": "module"
}
в файле package.json.
Иногда нет необходимости включать пакеты из node_modules внутрь бандла.
Например:
import express from "express";
Можно оставить пакет внешним:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
external: ["express"],
outfile: "dist/app.js"
});
В результате:
require("express");
останется без изменений.
Во время запуска пакет будет загружен из node_modules.
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
external: [
"express",
"pg",
"mongoose",
"redis"
],
outfile: "dist/app.js"
});
Подход особенно полезен для:
Допускается применение шаблонов:
external: ["aws-sdk/*"]
или
external: ["@aws-sdk/*"]
Пример:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
external: ["@aws-sdk/*"],
outfile: "dist/app.js"
});
Все пакеты AWS SDK будут исключены из итоговой сборки.
В некоторых сценариях требуется один файл без node_modules.
Например:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
outfile: "dist/server.js"
});
Если не использовать external, Esbuild попытается
встроить максимально возможное количество зависимостей.
Структура деплоя:
dist/
└── server.js
Запуск:
node dist/server.js
Подобный подход удобен для:
Esbuild удаляет неиспользуемый код и для серверных приложений.
Исходный модуль:
export function used() {
console.log("used");
}
export function unused() {
console.log("unused");
}
Использование:
import { used } from "./utils.js";
used();
Во время сборки функция:
unused()
будет удалена.
Преимущества:
Для production-сборок часто применяется минификация.
Конфигурация:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
minify: true,
outfile: "dist/app.js"
});
CLI-вариант:
npx esbuild src/index.js \
--bundle \
--platform=node \
--minify \
--outfile=dist/app.js
Минификация выполняет:
Esbuild способен одновременно компилировать TypeScript и выполнять бандлинг.
Исходный файл:
interface User {
id: number;
name: string;
}
const user: User = {
id: 1,
name: "Alex"
};
console.log(user);
Конфигурация:
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
platform: "node",
outfile: "dist/app.js"
});
Отдельный запуск TypeScript Compiler для базовой транспиляции не требуется.
Source Maps позволяют отлаживать уже собранный код.
Настройка:
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
platform: "node",
sourcemap: true,
outfile: "dist/app.js"
});
После сборки появляются файлы:
dist/
├── app.js
└── app.js.map
Ошибки и стек вызовов можно сопоставлять с исходными файлами.
Для крупных серверных приложений доступно разделение бандла.
Конфигурация:
await esbuild.build({
entryPoints: [
"src/api.js",
"src/worker.js"
],
bundle: true,
splitting: true,
format: "esm",
platform: "node",
outdir: "dist"
});
Esbuild создаёт:
dist/
├── api.js
├── worker.js
└── chunk-XXXXX.js
Общие зависимости помещаются в отдельные чанки.
Часто серверное приложение состоит из нескольких процессов:
src/
├── api.js
├── worker.js
├── scheduler.js
└── migration.js
Конфигурация:
await esbuild.build({
entryPoints: [
"src/api.js",
"src/worker.js",
"src/scheduler.js",
"src/migration.js"
],
bundle: true,
platform: "node",
outdir: "dist"
});
Результат:
dist/
├── api.js
├── worker.js
├── scheduler.js
└── migration.js
Каждая точка входа получает собственную сборку.
Метафайл содержит подробную информацию о процессе сборки.
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
metafile: true,
outfile: "dist/app.js"
});
Получение данных:
const result = await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
metafile: true,
write: false
});
console.log(result.metafile);
Метафайл помогает анализировать:
Для разработки удобно автоматически пересобирать серверное приложение.
const context = await esbuild.context({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
outfile: "dist/app.js"
});
await context.watch();
После изменения файлов сборка запускается автоматически.
Обычно режим watch комбинируется с инструментами:
nodemon
node --watch
pm2
tsx
Исходный код:
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.send("Hello");
});
app.listen(3000);
Конфигурация:
await esbuild.build({
entryPoints: ["src/server.js"],
bundle: true,
platform: "node",
external: ["express"],
outfile: "dist/server.js"
});
Запуск:
node dist/server.js
Пакет Express будет использоваться из node_modules, а весь остальной код окажется внутри бандла.
Пример:
import pg from "pg";
const client = new pg.Client({
connectionString: process.env.DB_URL
});
await client.connect();
Конфигурация:
await esbuild.build({
entryPoints: ["src/index.js"],
bundle: true,
platform: "node",
external: ["pg"],
outfile: "dist/app.js"
});
Драйвер базы данных остаётся внешним модулем.
Подобная практика распространена для:
import * as esbuild from "esbuild";
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
platform: "node",
target: "node20",
format: "esm",
minify: true,
sourcemap: false,
treeShaking: true,
external: [
"pg",
"redis",
"express"
],
outfile: "dist/server.js"
});
Такая конфигурация обеспечивает: