external в esbuild управляет тем, какие зависимости должны быть исключены из бандла и оставлены для разрешения в рантайме. Этот механизм становится критически важным при сборке библиотек, серверного кода и модульных систем, где часть импортов должна оставаться внешней относительно результирующего бандла.
В основе конфигурации лежит массив или функция, где задаются паттерны
модулей. Эти паттерны могут быть как точными именами пакетов, так и
группами через специальные шаблоны. Среди наиболее значимых конструкций
выделяются node:* и @scope/*, поскольку они
отражают два разных уровня абстракции: встроенные возможности Node.js и
пространства имён пакетов в npm-экосистеме.
При сборке esbuild по умолчанию стремится включить все импортируемые модули в итоговый бандл. Это поведение оптимально для фронтенда, но становится ограничением для библиотек и серверных приложений.
Параметр external позволяет:
Пример базовой конфигурации:
import esbuild from 'esbuild';
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
platform: 'node',
external: ['express', 'lodash']
});
В этом случае express и lodash не попадут в
итоговый бандл, а останутся внешними зависимостями.
Современный Node.js поддерживает пространственное именование
встроенных модулей через префикс node:. Примеры:
node:fsnode:pathnode:streamnode:cryptoИспользование node: устраняет неоднозначность между
встроенными модулями и пользовательскими пакетами.
В esbuild данный паттерн применяется для контроля поведения встроенных зависимостей в зависимости от цели сборки.
esbuild.build({
entryPoints: ['src/server.js'],
bundle: true,
platform: 'node',
external: ['node:*']
});
Такой подход означает, что любые импорты встроенных модулей Node.js останутся внешними:
import fs from 'node:fs';
import path from 'node:path';
Оба импорта не будут встроены в бандл.
Использование данного паттерна оправдано в нескольких сценариях:
При разработке библиотеки встроенные модули Node.js не должны инлайниться, так как:
При подготовке пакета для распространения важно сохранить оригинальные импорты:
export function readConfig() {
return import('node:fs');
}
В случаях, когда сборка выполняется для нескольких рантаймов,
исключение node:* позволяет избежать случайной подмены
встроенных API полифилами.
Часто node:* используется совместно с классическими
внешними зависимостями:
external: [
'node:*',
'express',
'pg',
'@nestjs/*'
]
Такой подход формирует три уровня исключений:
Экосистема npm активно использует scoped-пакеты вида:
@nestjs/core@types/node@babel/core@company/utilsСимвол @scope/* в external задаёт групповой паттерн,
покрывающий все пакеты внутри указанного пространства имён.
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
external: ['@nestjs/*']
});
В этом случае:
import { Module } from '@nestjs/common';
import { Inject } from '@nestjs/core';
Оба импорта остаются внешними.
Фреймворки часто состоят из множества пакетов в одном scope.
Например, @nestjs/* или @angular/*. Их
бандлинг:
Системы плагинов часто загружают scoped-модули динамически:
const plugin = await import(`@company/plugin-${name}`);
Бандлинг таких зависимостей приводит к потере динамичности.
Scoped-пакеты часто обновляются независимо друг от друга. Включение их в бандл нарушает независимость версий.
Точечное перечисление:
external: ['@nestjs/core', '@nestjs/common']
Scoped-паттерн:
external: ['@nestjs/*']
Разница заключается в масштабируемости:
Комбинация этих паттернов формирует типовую конфигурацию для серверных библиотек:
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
platform: 'node',
external: [
'node:*',
'@nestjs/*',
'@prisma/*',
'express',
'fastify'
]
});
Такой набор задаёт три категории внешних зависимостей:
esbuild поддерживает простое сопоставление строк без полноценного glob-движка. Это означает:
node:* — совпадение по префиксу node:@scope/* — совпадение по началу строки до первого
сегмента после /Пример логики:
| Импорт | external: [’node:*’] | external: [’@scope/*’] |
|---|---|---|
| node:fs | исключается | нет эффекта |
| node:crypto | исключается | нет эффекта |
| @scope/a | нет эффекта | исключается |
| @scope/utils | нет эффекта | исключается |
В сложных сборках external часто формируется программно:
const external = [
'node:*',
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {}).map(dep => `${dep}/*`)
];
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
external
});
Такая стратегия позволяет:
external: ['node:*', '*']
Подобная конфигурация фактически отключает бандлинг зависимостей, что приводит к невозможности создания самодостаточного артефакта.
external: ['@scope/**']
Такая запись не поддерживает рекурсивные шаблоны. Используется только
@scope/*.
При использовании node:-префикса в ESM важно учитывать,
что:
При указании:
platform: 'node'
esbuild уже предполагает наличие встроенных модулей. Однако:
node:* даёт явную семантику исключенияexternal: ['node:*', '@company/*']
Фокус на сохранении окружения выполнения и корпоративных пакетов.
external: ['node:*', 'express', 'fastify']
Сохраняется инфраструктурная гибкость и совместимость с Node.js runtime.
external: ['node:*', '@scope/*']
Позволяет сохранять динамическую загрузку серверных модулей и независимость частей системы.
Важно различать:
Это означает:
Использование node:* и @scope/* напрямую
влияет на:
В типичных серверных сборках основное снижение размера достигается именно через external, а не через minify или tree-shaking.