При сборке приложений с помощью Esbuild нередко возникает необходимость исключить встроенные модули Node.js из итогового бандла. Такая практика особенно важна при разработке библиотек, серверных приложений, инструментов командной строки и универсального JavaScript-кода, который может выполняться в различных средах.
К встроенным модулям Node.js относятся:
fspathoscryptohttphttpsstreameventsurlutilzlibnettlschild_processworker_threadsНапример:
import fs from 'fs';
import path from 'path';
const file = fs.readFileSync(
path.join(process.cwd(), 'config.json'),
'utf8'
);
По умолчанию Esbuild анализирует импортируемые зависимости и пытается включить их в результирующую сборку. Для встроенных модулей Node.js это поведение зависит от целевой платформы и конфигурации сборщика.
Существует несколько распространённых причин.
Модули вроде fs или path уже присутствуют в
среде выполнения Node.js.
Нет смысла включать их реализацию в итоговый пакет:
import fs from 'fs';
После сборки импорт может остаться неизменным:
const fs = require("fs");
При запуске Node.js самостоятельно загрузит необходимый встроенный модуль.
Некоторые сторонние зависимости используют встроенные модули Node.js. Если пытаться эмулировать их для браузера через полифиллы, размер бандла может значительно увеличиться.
Например:
import crypto from 'crypto';
Браузерный полифилл криптографии способен добавить десятки или даже сотни килобайт к итоговому файлу.
При разработке npm-пакетов часто предполагается, что конечная среда выполнения уже содержит Node.js.
Пример:
import fs from 'fs';
export function loadConfig(file) {
return fs.readFileSync(file, 'utf8');
}
Если такая библиотека будет использоваться в Node.js, нет
необходимости встраивать модуль fs внутрь пакета.
Серверный код часто зависит от системных возможностей:
import os from 'os';
import childProcess from 'child_process';
Подобные модули невозможно полноценно реализовать в браузере.
Исключение их из бандла позволяет избежать ошибок и сохранить ожидаемое поведение приложения.
Основной способ исключения модулей в Esbuild — использование
параметра external.
Пример:
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/app.js',
external: ['fs']
});
После сборки Esbuild не будет пытаться разрешать содержимое модуля
fs.
Импорт сохранится:
require("fs");
или
import fs from "fs";
в зависимости от формата сборки.
На практике обычно исключают сразу группу модулей.
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/app.js',
external: [
'fs',
'path',
'os',
'crypto'
]
});
Теперь все указанные зависимости будут считаться внешними.
Тот же результат можно получить через CLI.
esbuild src/index.js \
--bundle \
--outfile=dist/app.js \
--external:fs \
--external:path \
--external:os
Каждый внешний модуль задаётся отдельным параметром.
Для крупных проектов перечисление каждого встроенного модуля вручную неудобно.
Node.js предоставляет список встроенных модулей через пакет
module.
Пример автоматического формирования списка:
import { builtinModules } from 'module';
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/app.js',
external: builtinModules
});
Содержимое builtinModules может выглядеть следующим
образом:
[
'fs',
'path',
'os',
'http',
'https',
'stream',
'crypto'
]
Количество модулей зависит от версии Node.js.
Современный Node.js поддерживает специальный префикс:
import fs from 'node:fs';
import path from 'node:path';
Такая запись явно указывает, что используется встроенный модуль.
При работе с Esbuild необходимо учитывать оба варианта импорта.
Например:
external: [
'fs',
'node:fs',
'path',
'node:path'
]
Иначе часть импортов может остаться необработанной.
Практический пример:
import { builtinModules } from 'module';
const externals = [
...builtinModules,
...builtinModules.map(
module => `node:${module}`
)
];
Далее список используется в конфигурации:
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/app.js',
external: externals
});
Такое решение подходит для большинства серверных проектов.
При сборке под Node.js рекомендуется явно указывать платформу.
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
platform: 'node',
outfile: 'dist/app.js'
});
Параметр:
platform: 'node'
сообщает Esbuild, что код предназначен для среды Node.js.
Это влияет на:
Важно понимать различие между этими параметрами.
Определяет среду выполнения:
platform: 'node'
Определяет зависимости, которые не должны попадать в бандл:
external: ['fs']
Даже если сборка предназначена для Node.js, внешние зависимости могут потребовать явного исключения.
Пример:
await esbuild.build({
bundle: true,
platform: 'node',
external: ['fs']
});
Предположим, требуется получить ES-модуль.
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
format: 'esm',
platform: 'node',
external: ['fs'],
outfile: 'dist/index.mjs'
});
Исходный код:
import fs from 'fs';
console.log(fs.existsSync('file.txt'));
После сборки импорт сохранится:
import fs from "fs";
console.log(fs.existsSync("file.txt"));
Esbuild не будет пытаться встроить содержимое модуля.
Конфигурация:
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
format: 'cjs',
platform: 'node',
external: ['fs'],
outfile: 'dist/index.js'
});
Результат:
var fs = require("fs");
Загрузка будет выполнена непосредственно Node.js во время запуска программы.
Esbuild поддерживает шаблонные значения.
Пример:
external: ['node:*']
Теперь все импорты вида:
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
будут автоматически считаться внешними.
Это особенно полезно в современных проектах, активно использующих
пространство имён node:.
import * as esbuild from 'esbuild';
import { builtinModules } from 'module';
const externals = [
...builtinModules,
...builtinModules.map(
module => `node:${module}`
)
];
await esbuild.build({
entryPoints: ['src/server.js'],
bundle: true,
platform: 'node',
format: 'esm',
target: 'node20',
outfile: 'dist/server.js',
external: externals
});
Особенности данной конфигурации:
node:*;Неправильный вариант:
external: ['fs']
Код:
import fs from 'node:fs';
Может потребовать отдельной обработки.
Более надёжный вариант:
external: [
'fs',
'node:fs'
]
или:
external: ['node:*']
Пример:
import fs from 'fs';
Конфигурация:
platform: 'browser'
Модуль fs не существует в браузере.
Даже если он исключён через external, итоговый код всё
равно не сможет корректно работать в браузерной среде.
import { exec } from 'child_process';
Модуль:
child_process
зависит от возможностей операционной системы и недоступен в браузере.
Его исключение из бандла не делает код переносимым между платформами.
Пример структуры:
src/
├─ server/
│ └─ app.js
└─ client/
└─ app.js
Если клиентский модуль импортирует серверный:
import '../server/app.js';
то в дерево зависимостей попадут импорты вроде:
import fs from 'fs';
Это часто приводит к ошибкам сборки или выполнения.
После завершения сборки полезно убедиться, что модуль действительно остался внешним.
Исходный код:
import fs from 'fs';
Результат:
import fs from "fs";
или:
const fs = require("fs");
Если внутри итогового файла отсутствует встроенная реализация и сохранён исходный импорт, модуль успешно исключён из бандла.
import fs from 'fs';
import path from 'path';
Такие приложения почти всегда запускаются в Node.js и используют внешние встроенные модули.
import fs from 'fs';
import http from 'http';
Включать встроенные модули в бандл не требуется.
import os from 'os';
import child_process from 'child_process';
Наиболее распространённая практика — помечать их как внешние зависимости.
import path from 'path';
export function normalize(value) {
return path.normalize(value);
}
Сохранение зависимости от встроенного модуля позволяет уменьшить размер пакета и избежать дублирования функциональности Node.js.