Символические ссылки и опция preserveSymlinks

esbuild — высокопроизводительный бандлер, ориентированный на скорость сборки за счёт реализации на Go и агрессивной оптимизации графа модулей. В экосистеме Node.js и фронтенд-инструментов он часто используется как замена более медленным JavaScript-бандлерам в задачах разработки и продакшена.

Символические ссылки (symlinks) представляют собой специальные файлы, которые указывают на другой путь в файловой системе. В контексте JavaScript-проектов они часто встречаются в следующих сценариях:

  • монорепозитории (pnpm, Yarn workspaces, Lerna)
  • локальная разработка пакетов через npm link
  • разделение общих модулей между несколькими проектами
  • сложные схемы деплоя, где зависимости физически находятся вне node_modules

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

  • разрешение импортов (import)
  • идентификацию модулей как «одинаковых» или «разных»
  • кэширование зависимостей
  • корректность tree-shaking и дедупликации

Важнейшая проблема: один и тот же модуль может быть доступен по разным путям, если проходит через symlink-цепочку. Это ломает предположения бандлера о уникальности модулей.

Модель резолвинга модулей в esbuild

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

При стандартной настройке esbuild:

  • символические ссылки разыменовываются (realpath)
  • итоговый путь нормализуется до физического расположения файла
  • модуль считается уникальным по реальному пути, а не по пути импорта

Это поведение важно для:

  • корректного объединения зависимостей
  • предотвращения дублирования пакетов
  • стабильного tree-shaking

Однако оно может приводить к расхождениям с Node.js в специфических случаях.

Проблема двойных экземпляров модулей

Типичная проблема при активных symlinks:

  • пакет A зависит от react
  • пакет B (в монорепозитории) также зависит от react
  • оба пакета подключены через symlink в один проект

Без корректной настройки может возникнуть ситуация:

  • два разных пути к одному и тому же пакету
  • два экземпляра React в рантайме
  • нарушение правил хуков React
  • некорректное состояние контекста

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

Параметр preserveSymlinks изменяет стратегию разрешения модулей и определяет, как esbuild обрабатывает символические ссылки при сборке.

Основное поведение

При включении:

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

При выключении (по умолчанию):

  • выполняется realpath-разрешение
  • symlink заменяется на физический путь
  • возможна дедупликация зависимостей

Параметр исторически связан с поведением Node.js (--preserve-symlinks):

  • влияет на кеш модулей
  • изменяет идентичность require()
  • сохраняет различие между ссылкой и реальным файлом

esbuild реализует аналогичную модель на этапе сборки, а не выполнения, что делает последствия более «статическими».

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

При preserveSymlinks: true:

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

При preserveSymlinks: false:

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

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

Структура:

repo/
  packages/
    ui/
      index.js
    app/
      index.js

app использует ui через workspace symlink.

  • ui резолвится в физический путь packages/ui
  • esbuild объединяет зависимости
  • общий граф модулей становится единым
  • ui остаётся виртуально связанным через symlink
  • путь интерпретируется как отдельная сущность
  • возможны различия в идентификации модулей внутри графа

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

esbuild активно использует кеширование результатов резолвинга.

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

Совместимость с инструментами экосистемы

pnpm

pnpm активно использует symlink-структуры:

  • node_modules строится через виртуальные ссылки
  • зависимости строго изолированы

При работе с esbuild:

  • preserveSymlinks: false чаще даёт более «схлопнутый» граф
  • true может быть ближе к реальной runtime-структуре Node

Yarn workspaces

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

При локальной разработке пакетов:

  • preserveSymlinks: true может быть полезен для симуляции поведения linked-пакета
  • false приводит к «слиянию» с реальной копией пакета

Влияние на ESM и CommonJS

ESM

  • идентичность модуля зависит от URL/пути
  • symlink влияет на уникальность импорта
  • возможны разные экземпляры одного модуля

CommonJS

  • зависит от require-кеша
  • Node может различать или объединять модули в зависимости от режима запуска
  • esbuild лишь формирует результат, но не управляет runtime-кешем

Дублирование состояния

Особенно критично для:

  • React
  • Vue (реактивность)
  • state management библиотеки

Причина:

  • один пакет загружен дважды из-за разных путей

Несовпадение окружений

Сборка и runtime ведут себя по-разному:

  • esbuild с preserveSymlinks
  • Node без preserveSymlinks

Это приводит к:

  • различию в поведении импортов
  • трудноуловимым багам

Ломание tree-shaking

При различной идентичности модулей:

  • оптимизатор не может объединить экспорт
  • увеличивается размер бандла

Рекомендованные сценарии применения

  • стандартные SPA-проекты
  • приложения без сложных symlink-структур
  • максимальная оптимизация бандла
  • монорепозитории с единым hoisting-резолвингом
  • разработка через npm link
  • плагины и расширяемые архитектуры
  • сценарии, где важно сохранить «виртуальные границы» пакетов
  • диагностика проблем с резолвингом модулей

Внутренняя логика резолвинга

В упрощённом виде поведение esbuild можно описать так:

  • если preserveSymlinks = false:

    • вычислить realpath(importPath)
    • использовать физический путь как ключ модуля
  • если preserveSymlinks = true:

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

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

Взаимодействие с другими опциями esbuild

bundle

При включённом bundling:

  • влияние symlinks усиливается
  • граф модулей строится глобально

splitting

При code splitting:

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

platform: node

При Node-ориентированной сборке:

  • preserveSymlinks становится ближе к runtime-поведению
  • но всё ещё не полностью идентично Node.js loader’у

Итоговая модель поведения

Поведение символических ссылок в esbuild определяется балансом между:

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

Опция preserveSymlinks переключает интерпретацию между двумя моделями:

  • физическая модель (realpath) — оптимизация и дедупликация
  • виртуальная модель (symlink-aware) — сохранение структуры проектов