Поддержка exports в package.json

Поле exports в package.json определяет публичный API пакета и управляет тем, какие модули могут быть импортированы извне. В контексте сборщика Esbuild это становится ключевым механизмом контроля разрешения модулей, особенно в современных библиотеках, ориентированных на ESM и гибридные окружения.

Основная идея exports заключается в том, чтобы заменить неявный доступ к внутренней структуре пакета на строго описанный интерфейс. Вместо прямых импортов вида my-lib/dist/internal/file.js используется декларативное сопоставление экспортируемых точек входа.


Базовая структура exports

Поле exports может задавать единственную точку входа или набор субпутей:

{
  "name": "my-lib",
  "version": "1.0.0",
  "exports": "./dist/index.js"
}

В этом случае пакет разрешается только через основной импорт:

import lib from "my-lib";

Попытка обратиться к внутренним файлам:

import x from "my-lib/dist/internal.js";

становится невозможной, если сборщик и рантайм соблюдают правила exports.


Объектная форма экспорта

Более гибкий вариант — объектная форма:

{
  "exports": {
    ".": "./dist/index.js",
    "./feature": "./dist/feature.js"
  }
}

Здесь:

  • "." — основной модуль пакета
  • "./feature" — публичный субпуть

Использование:

import lib from "my-lib";
import feature from "my-lib/feature";

Esbuild при такой конфигурации строго следует указанной карте и не позволяет обходить её через файловые пути.


Поддержка условных экспортов

exports поддерживает условия, позволяющие адаптировать модуль под разные окружения:

{
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.js"
    }
  }
}

Условия:

  • import — используется для ESM
  • require — для CommonJS
  • default — fallback

Esbuild учитывает эти условия при сборке в зависимости от параметров format и platform.


Влияние Esbuild на разрешение exports

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

  • поле exports в package.json
  • поле main (как fallback)
  • поле module (в некоторых сценариях)
  • настройки platform: node | browser | neutral
  • формат сборки (esm или cjs)

Пример конфигурации:

import esbuild from "esbuild";

esbuild.build({
  entryPoints: ["src/index.js"],
  bundle: true,
  platform: "node",
  format: "esm",
  outdir: "dist"
});

В этом режиме Esbuild выбирает экспорт, соответствующий import-ветке условий.


Субпуть-экспорты и контроль структуры пакета

Субпуть-экспорты позволяют явно ограничить доступ к внутренним частям пакета:

{
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils/index.js",
    "./hooks/*": "./dist/hooks/*.js"
  }
}

Поддержка шаблонов (*) позволяет массово маппировать структуры каталогов.

Использование:

import { helper } from "my-lib/utils";
import hook from "my-lib/hooks/use-hook";

Esbuild разрешает такие пути на этапе сборки, заменяя их на конкретные файлы.


Ограничения глубоких импортов

До появления exports было распространено использование глубоких импортов:

import x from "my-lib/dist/internal/x.js";

При включённом exports такие обращения блокируются. Esbuild ведёт себя аналогично Node.js: если путь не описан в exports, он считается недоступным.

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


Совместимость с CommonJS и ESM

Esbuild учитывает различия модульных систем при выборе экспорта:

  • для format: esm приоритет у import
  • для format: cjs приоритет у require

Пример:

{
  "exports": {
    ".": {
      "import": "./esm/index.js",
      "require": "./cjs/index.js"
    }
  }
}

Такой подход позволяет одной библиотеке поддерживать оба мира без дублирования логики импорта на стороне пользователя.


Влияние поля browser и других fallback-механизмов

Хотя exports имеет приоритет, Esbuild может учитывать дополнительные поля:

  • browser — замена модулей для браузерной сборки
  • main — устаревшая точка входа
  • module — ESM-вариант (в legacy-пакетах)

Однако при наличии exports большинство fallback-механизмов игнорируется, поскольку exports считается источником истины.


Поведение при отсутствии совпадений

Если импорт не соответствует ни одному ключу exports, Esbuild завершает резолв с ошибкой:

Could not resolve "my-lib/unknown" (exports field does not define this subpath)

Это поведение совпадает с Node.js и позволяет выявлять ошибки на этапе сборки, а не в рантайме.


Условия окружения и пользовательские conditions

Esbuild поддерживает передачу пользовательских условий через conditions:

esbuild.build({
  entryPoints: ["src/index.js"],
  bundle: true,
  conditions: ["custom", "browser"]
});

И в package.json:

{
  "exports": {
    ".": {
      "browser": "./dist/browser.js",
      "custom": "./dist/custom.js",
      "default": "./dist/index.js"
    }
  }
}

Условия проверяются в порядке приоритета, что позволяет строить сложные схемы дистрибуции.


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

В монорепозиториях exports часто используется для строгого разделения пакетов. Esbuild корректно разрешает такие структуры при условии правильного указания путей:

packages/
  core/
    package.json
    src/
  ui/
    package.json
    src/

Каждый пакет описывает собственный exports, предотвращая перекрёстные внутренние импорты.


Типичные ошибки при работе с exports в Esbuild

  • Отсутствие "." в exports, что блокирует основной импорт пакета
  • Несоответствие путей сборки и указанных экспортов
  • Попытка импортировать неописанные субпуты
  • Конфликт между main, module и exports при миграции старых пакетов
  • Использование шаблонов * без соответствующей структуры файлов после сборки

Поведение при сборке и бандлинге

При bundle: true Esbuild стремится полностью резолвить зависимости на этапе сборки. Это означает, что exports применяется не только к внешним импортам, но и к внутреннему графу зависимостей.

В результате:

  • внутренняя структура пакетов становится прозрачной для сборщика
  • доступ ограничивается строго описанными точками входа
  • возможна более агрессивная оптимизация дерева зависимостей

Роль exports в современной экосистеме сборки

Использование exports становится стандартом для библиотек, ориентированных на:

  • ESM-first разработку
  • поддержку Node.js и браузера одновременно
  • контроль публичного API
  • предсказуемую сборку в инструментах вроде Esbuild

Esbuild, благодаря быстрому резолвингу и строгому соблюдению схемы exports, делает поведение сборки более детерминированным и близким к реальному Node.js окружению, минимизируя расхождения между разработкой и продакшеном.