Паттерны в external: node:*, @scope/*

external в esbuild управляет тем, какие зависимости должны быть исключены из бандла и оставлены для разрешения в рантайме. Этот механизм становится критически важным при сборке библиотек, серверного кода и модульных систем, где часть импортов должна оставаться внешней относительно результирующего бандла.

В основе конфигурации лежит массив или функция, где задаются паттерны модулей. Эти паттерны могут быть как точными именами пакетов, так и группами через специальные шаблоны. Среди наиболее значимых конструкций выделяются node:* и @scope/*, поскольку они отражают два разных уровня абстракции: встроенные возможности Node.js и пространства имён пакетов в npm-экосистеме.


При сборке esbuild по умолчанию стремится включить все импортируемые модули в итоговый бандл. Это поведение оптимально для фронтенда, но становится ограничением для библиотек и серверных приложений.

Параметр external позволяет:

  • исключать модули из бандла
  • сохранять require/import в исходном виде
  • делегировать разрешение зависимостей окружению выполнения

Пример базовой конфигурации:

import esbuild from 'esbuild';

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  platform: 'node',
  external: ['express', 'lodash']
});

В этом случае express и lodash не попадут в итоговый бандл, а останутся внешними зависимостями.


Паттерн node:* как отражение встроенных модулей Node.js

Современный Node.js поддерживает пространственное именование встроенных модулей через префикс node:. Примеры:

  • node:fs
  • node:path
  • node:stream
  • node:crypto

Использование node: устраняет неоднозначность между встроенными модулями и пользовательскими пакетами.

В esbuild данный паттерн применяется для контроля поведения встроенных зависимостей в зависимости от цели сборки.

Базовое использование node:*

esbuild.build({
  entryPoints: ['src/server.js'],
  bundle: true,
  platform: 'node',
  external: ['node:*']
});

Такой подход означает, что любые импорты встроенных модулей Node.js останутся внешними:

import fs from 'node:fs';
import path from 'node:path';

Оба импорта не будут встроены в бандл.


Когда применяется external: [’node:*’]

Использование данного паттерна оправдано в нескольких сценариях:

1. Библиотечная сборка под Node.js

При разработке библиотеки встроенные модули Node.js не должны инлайниться, так как:

  • они уже присутствуют в окружении выполнения
  • их поведение зависит от версии Node.js
  • их бандлинг увеличивает размер без пользы

2. Публикация пакетов в npm

При подготовке пакета для распространения важно сохранить оригинальные импорты:

export function readConfig() {
  return import('node:fs');
}

3. Изоляция окружения

В случаях, когда сборка выполняется для нескольких рантаймов, исключение node:* позволяет избежать случайной подмены встроенных API полифилами.


Комбинирование node:* с другими external-паттернами

Часто node:* используется совместно с классическими внешними зависимостями:

external: [
  'node:*',
  'express',
  'pg',
  '@nestjs/*'
]

Такой подход формирует три уровня исключений:

  • встроенные модули Node.js
  • конкретные зависимости
  • группы пакетов по scope

Паттерн @scope/* и работа с пространствами имён npm

Экосистема npm активно использует scoped-пакеты вида:

  • @nestjs/core
  • @types/node
  • @babel/core
  • @company/utils

Символ @scope/* в external задаёт групповой паттерн, покрывающий все пакеты внутри указанного пространства имён.


Базовое поведение @scope/*

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  external: ['@nestjs/*']
});

В этом случае:

import { Module } from '@nestjs/common';
import { Inject } from '@nestjs/core';

Оба импорта остаются внешними.


Причины использования @scope/*

1. Архитектурная изоляция фреймворков

Фреймворки часто состоят из множества пакетов в одном scope. Например, @nestjs/* или @angular/*. Их бандлинг:

  • приводит к дублированию кода
  • ломает peer dependency модель
  • увеличивает размер артефакта

2. Поддержка плагинной архитектуры

Системы плагинов часто загружают scoped-модули динамически:

const plugin = await import(`@company/plugin-${name}`);

Бандлинг таких зависимостей приводит к потере динамичности.

3. Сохранение semver-логики

Scoped-пакеты часто обновляются независимо друг от друга. Включение их в бандл нарушает независимость версий.


Различие между @scope/* и точечным external

Точечное перечисление:

external: ['@nestjs/core', '@nestjs/common']

Scoped-паттерн:

external: ['@nestjs/*']

Разница заключается в масштабируемости:

  • точечный список требует постоянного обновления
  • wildcard автоматически покрывает новые пакеты в scope

Совместное использование node:* и @scope/*

Комбинация этих паттернов формирует типовую конфигурацию для серверных библиотек:

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  platform: 'node',
  external: [
    'node:*',
    '@nestjs/*',
    '@prisma/*',
    'express',
    'fastify'
  ]
});

Такой набор задаёт три категории внешних зависимостей:

  1. Встроенные API Node.js
  2. Фреймворковые scoped-пакеты
  3. Конкретные инфраструктурные библиотеки

Поведение wildcard-паттернов в external

esbuild поддерживает простое сопоставление строк без полноценного glob-движка. Это означает:

  • node:* — совпадение по префиксу node:
  • @scope/* — совпадение по началу строки до первого сегмента после /

Пример логики:

Импорт external: [’node:*’] external: [’@scope/*’]
node:fs исключается нет эффекта
node:crypto исключается нет эффекта
@scope/a нет эффекта исключается
@scope/utils нет эффекта исключается

Практика: функции для динамического формирования external

В сложных сборках external часто формируется программно:

const external = [
  'node:*',
  ...Object.keys(pkg.dependencies || {}),
  ...Object.keys(pkg.peerDependencies || {}).map(dep => `${dep}/*`)
];

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  external
});

Такая стратегия позволяет:

  • автоматически исключать зависимости из package.json
  • учитывать scoped-пакеты через wildcard
  • минимизировать ручное сопровождение конфигурации

Типовые ошибки при работе с node:* и @scope/*

1. Избыточное исключение внутренних модулей

external: ['node:*', '*']

Подобная конфигурация фактически отключает бандлинг зависимостей, что приводит к невозможности создания самодостаточного артефакта.


2. Неверное ожидание глубокого glob

external: ['@scope/**']

Такая запись не поддерживает рекурсивные шаблоны. Используется только @scope/*.


3. Конфликт с ESM-резолвингом

При использовании node:-префикса в ESM важно учитывать, что:

  • некоторые старые версии Node.js не поддерживают этот формат
  • внешние сборки могут ожидать bare imports без префикса

Поведение external в связке с platform: node

При указании:

platform: 'node'

esbuild уже предполагает наличие встроенных модулей. Однако:

  • node:* даёт явную семантику исключения
  • улучшает читаемость конфигурации
  • предотвращает случайный бандлинг встроенных API

Архитектурные стратегии применения

Библиотеки общего назначения

external: ['node:*', '@company/*']

Фокус на сохранении окружения выполнения и корпоративных пакетов.


Backend-приложения

external: ['node:*', 'express', 'fastify']

Сохраняется инфраструктурная гибкость и совместимость с Node.js runtime.


Микрофронтенды с Node SSR

external: ['node:*', '@scope/*']

Позволяет сохранять динамическую загрузку серверных модулей и независимость частей системы.


Поведение при tree-shaking и external

Важно различать:

  • tree-shaking работает внутри бандла
  • external полностью исключает модуль из анализа

Это означает:

  • код из external не оптимизируется
  • зависимости остаются нетронутыми
  • ответственность за их загрузку переносится на runtime

Влияние на размер и производительность

Использование node:* и @scope/* напрямую влияет на:

  • размер итогового бандла (уменьшение)
  • скорость сборки (ускорение)
  • время загрузки (зависит от runtime)
  • стабильность версий зависимостей

В типичных серверных сборках основное снижение размера достигается именно через external, а не через minify или tree-shaking.