Опция nodePaths: дополнительные пути поиска

Опция nodePaths в esbuild расширяет стандартный алгоритм разрешения модулей, добавляя дополнительные директории, в которых выполняется поиск импортируемых пакетов и файлов. Она влияет исключительно на этап resolution и не изменяет процесс трансформации или бандлинга.

В стандартной схеме Node.js и esbuild поиск зависимостей выполняется последовательно:

  1. Проверка локальных путей относительно текущего файла
  2. Поиск в node_modules текущего каталога
  3. Подъём вверх по дереву директорий с повторением поиска node_modules

Опция nodePaths вмешивается в этот процесс, добавляя один или несколько дополнительных корневых каталогов, которые проверяются на наличие модулей до или параллельно с обходом стандартных node_modules.

Если указано несколько путей, каждый из них рассматривается как отдельный корень поиска пакетов.

Синтаксис и структура

Опция передаётся в конфигурации esbuild как массив строк:

import esbuild from "esbuild";

esbuild.build({
  entryPoints: ["src/index.js"],
  bundle: true,
  nodePaths: [
    "src/shared",
    "external_modules",
    "../common_libs"
  ]
});

Каждый путь интерпретируется как директория верхнего уровня, внутри которой esbuild пытается найти импортируемые зависимости так же, как в node_modules.

Алгоритм поиска с nodePaths

При разрешении импорта, например:

import utils from "utils";

esbuild выполняет последовательность:

  1. Проверка локального node_modules

  2. Проверка всех директорий из nodePaths:

    • src/shared/utils
    • external_modules/utils
    • ../common_libs/utils
  3. Подъём к родительским директориям и повтор стандартного поиска node_modules

Каждый путь из nodePaths рассматривается как потенциальный корень пакета, где utils может быть либо папкой, либо файлом (utils.js, utils/index.js и т.д.).

Отличие от NODE_PATH

Существует схожая концепция переменной окружения NODE_PATH, однако поведение nodePaths отличается:

  • NODE_PATH зависит от окружения процесса и влияет на Node.js runtime
  • nodePaths задаётся явно в конфигурации сборщика и влияет только на esbuild
  • nodePaths более предсказуем в контексте сборки, так как не зависит от внешнего окружения

Использование nodePaths делает сборку воспроизводимой, так как все пути зафиксированы в конфигурации проекта.

Применение в монорепозиториях

В монорепозиториях часто возникает ситуация, когда пакеты расположены вне стандартного node_modules. Например:

repo/
  packages/
    app/
    shared/
    ui/

При попытке импорта:

import Button from "ui/Button";

без дополнительной настройки esbuild не сможет разрешить ui, если он не установлен как зависимость в node_modules.

С nodePaths можно добавить:

nodePaths: ["packages"]

Теперь packages/ui и packages/shared становятся доступными как корневые модули, что позволяет использовать более короткие и стабильные импорты внутри монорепозитория.

Влияние на приоритет разрешения

Порядок директорий в nodePaths имеет значение. При наличии конфликтующих модулей:

nodePaths: ["a", "b"]

и наличия utils в обеих директориях, будет использован первый найденный вариант из a/utils.

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

Совместимость с расширениями файлов

esbuild применяет стандартные правила расширений при поиске модулей внутри nodePaths:

  • .js
  • .jsx
  • .ts
  • .tsx
  • .json
  • директории с index.*

Пример структуры:

shared/
  utils/
    index.js

Импорт:

import utils from "utils";

разрешается в shared/utils/index.js, если shared указан в nodePaths.

Взаимодействие с external и alias логикой

Хотя nodePaths влияет на resolution, он не заменяет:

  • external — исключение модулей из бандла
  • плагины esbuild для кастомного разрешения
  • ручные алиасы через plugin resolve

При наличии плагинов, изменяющих onResolve, их логика может полностью перекрыть поведение nodePaths. В этом случае nodePaths становится частью fallback-механизма.

Использование в связке с TypeScript

При использовании TypeScript nodePaths не заменяет baseUrl или paths из tsconfig.json. Однако он может использоваться для синхронизации структуры резолвинга между сборщиком и компилятором типов.

Типичная схема:

  • TypeScript: baseUrl: ./packages
  • esbuild: nodePaths: ["packages"]

Это снижает расхождения между IDE, type-checking и сборкой.

Поведение при отсутствующих директориях

Если путь из nodePaths не существует, esbuild игнорирует его без ошибки. Это важно для CI-сред, где часть директорий может отсутствовать.

Такое поведение делает конфигурацию устойчивой к частично собранным или условным структурам проекта.

Ограничения и особенности

  • nodePaths не создаёт виртуальных модулей
  • не влияет на CDN или URL-импорты
  • не изменяет поведение ESM vs CommonJS
  • не работает как полноценный alias system (нет шаблонов или регулярных выражений)

По сути это механизм расширения корневых каталогов поиска, а не система переписывания путей.

Производительность резолвинга

Добавление большого числа путей в nodePaths увеличивает количество проверок файловой системы. При глубокой структуре монорепозитория это может повлиять на время первой сборки.

Оптимизация заключается в:

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

Поведение с кешированием

esbuild активно кеширует результаты resolution. Поэтому повторные сборки с неизменной структурой nodePaths практически не увеличивают время выполнения, даже при большом числе импортов.

Кеш учитывает:

  • список nodePaths
  • относительные пути модулей
  • состояние файловой системы на момент первого резолва