Разрешение симлинков: symlinks

В файловых системах Unix-подобных ОС и Windows существуют символьные ссылки — symbolic links или symlinks. Симлинк представляет собой специальный объект файловой системы, который указывает на другой файл или каталог.

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

За это отвечает параметр:

resolve: {
    symlinks: true
}

По умолчанию значение равно true.


Как работают симлинки

Предположим, существует структура:

project/
├── node_modules/
│   └── shared-lib -> ../. ./shared-lib
├── src/
│   └── index.js

Каталог shared-lib подключён в node_modules не как обычная директория, а как символическая ссылка.

Такое часто происходит:

  • при использовании npm link
  • при разработке локальных библиотек
  • в monorepo
  • при использовании Yarn Workspaces
  • при использовании pnpm
  • внутри Docker-монтажей
  • при локальной разработке пакетов

Если параметр включён:

resolve: {
    symlinks: true
}

Webpack:

  1. Находит симлинк
  2. Определяет реальный путь
  3. Продолжает разрешение уже по физическому расположению

Например:

node_modules/shared-lib

может быть преобразован в:

/home/user/shared-lib

Что происходит внутри resolver

Когда импортируется модуль:

import lib from 'shared-lib';

Webpack выполняет:

  1. Поиск директории пакета
  2. Проверку наличия symlink
  3. Вызов fs.realpath
  4. Получение физического пути
  5. Кэширование результата
  6. Дальнейшее разрешение зависимостей

Это влияет на:

  • идентификацию модулей
  • deduplication
  • кеш
  • HMR
  • tree shaking
  • разделение чанков
  • загрузку loader-ов
  • правила include/exclude

Пример реального пути

Исходная структура:

project/
├── node_modules/
│   └── ui-kit -> ../. ./packages/ui-kit

Импорт:

import Button from 'ui-kit/Button';

При symlinks: true Webpack может считать модуль расположенным здесь:

../. ./packages/ui-kit/Button.js

а не внутри:

node_modules/ui-kit

Если параметр отключён:

resolve: {
    symlinks: false
}

Webpack перестаёт вычислять реальный путь.

Симлинк рассматривается как обычная директория.

То есть путь остаётся:

node_modules/shared-lib

даже если фактически пакет расположен в другом месте.


Основные различия

Поведение true false
Определение real path Да Нет
Использование fs.realpath Да Нет
Физический путь пакета Используется Игнорируется
Производительность Ниже Выше
Совместимость с monorepo Иногда проблемная Лучше
Работа include/exclude Может ломаться Более предсказуема

Влияние на include и exclude

Очень важная особенность связана с Babel Loader.

Конфигурация:

{
    test: /\.js$/,
    include: path.resolve(__dirname, 'src'),
    loader: 'babel-loader'
}

Если пакет подключён через symlink и symlinks: true, то физический путь может оказаться вне src.

Например:

/home/user/packages/shared

Тогда правило include перестанет работать.


Типичная проблема с monorepo

Структура:

root/
├── packages/
│   ├── app/
│   └── ui/

ui подключается в app через workspace-ссылку.

Webpack при symlinks: true может видеть:

/packages/ui

вместо:

/packages/app/node_modules/ui

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

  • Babel не транспилирует код
  • TypeScript loader пропускает файлы
  • CSS loader не применяет правила
  • asset-модули не обрабатываются

При отключении симлинков Webpack начинает считать workspace-пакеты обычными зависимостями из node_modules.

Это позволяет:

  • стабилизировать пути
  • избежать конфликтов loader-ов
  • упростить include/exclude
  • улучшить производительность
  • устранить дублирование модулей

Пример настройки для monorepo

module.exports = {
    resolve: {
        symlinks: false
    }
};

Очень распространённая практика для:

  • Lerna
  • Nx
  • Turborepo
  • Yarn Workspaces
  • pnpm workspace

Команда:

npm link

создаёт симлинк внутри node_modules.

Например:

node_modules/my-lib -> /Users/dev/my-lib

Webpack при symlinks: true начнёт использовать:

/Users/dev/my-lib

а не путь внутри node_modules.


Проблема дублирования React

Одна из самых известных проблем связана с React.

Структура:

app/
node_modules/react

и:

my-lib/
node_modules/react

Если библиотека подключена через symlink, Webpack может увидеть две разные копии React.

Это приводит к ошибкам:

Invalid hook call

или:

Hooks can only be called inside...

Почему возникает duplicate dependency

Webpack определяет модуль по абсолютному пути.

Если пути разные:

/project/node_modules/react

и:

/Users/dev/my-lib/node_modules/react

Webpack считает их разными модулями.


При отключении realpath:

resolve: {
    symlinks: false
}

оба пути могут интерпретироваться как:

node_modules/react

Это уменьшает вероятность дублирования зависимостей.


Влияние на HMR

Hot Module Replacement сильно зависит от идентичности модулей.

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

  • полные перезагрузки страницы
  • потеря состояния
  • бесконечные обновления
  • повторное создание dependency graph

Отключение symlink-resolution нередко делает HMR стабильнее.


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

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

  • дополнительных вызовов файловой системы
  • fs.realpath
  • проверок inode
  • кэширования physical path

На больших monorepo это может становиться заметной нагрузкой.


Включённый режим полезен, когда необходимо:

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

Наиболее распространённые случаи:

Monorepo

resolve: {
    symlinks: false
}

Yarn Workspaces

pnpm

Локальная разработка библиотек

Docker volumes

Сложные include/exclude

Проблемы с duplicate React


Влияние на кеширование

Webpack кеширует результаты module resolution.

При использовании real path:

/home/user/lib

и symlink path:

/project/node_modules/lib

кеш может содержать разные записи для одного модуля.

Это увеличивает:

  • объём кеша
  • количество пересборок
  • вероятность cache miss

Взаимодействие с watch

Watch mode отслеживает изменения файлов.

При симлинках возникают сложности:

  • изменение real path
  • изменение link path
  • неоднозначность наблюдения
  • двойные события FS

Иногда Webpack начинает:

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

Особенности pnpm

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

Физически зависимости могут находиться:

.pnpm/

а в node_modules создаются ссылки.

Webpack без правильной настройки иногда:

  • неправильно определяет пути
  • дублирует пакеты
  • ломает HMR
  • создаёт конфликты loader-ов

Поэтому symlinks: false особенно распространён при pnpm.


Важно понимать различие.

resolve.alias

Подмена путей внутри resolver:

alias: {
    '@': path.resolve(__dirname, 'src')
}

Управление обработкой символических ссылок файловой системы.

Это совершенно разные механизмы.


Связь с Node.js

Node.js тоже умеет разрешать symlink.

Webpack частично повторяет логику Node resolver, но имеет собственную систему кеширования и dependency graph.

Параметр resolve.symlinks влияет именно на внутренний resolver Webpack.


Часто помогает вывод:

console.log(__filename);
console.log(process.cwd());

а также анализ:

npm ls

или:

pnpm why react

Проверка real path

В Node.js:

const fs = require('fs');

console.log(
    fs.realpathSync('./node_modules/shared-lib')
);

Можно увидеть физический путь, который Webpack будет использовать при symlinks: true.


Типичный конфиг для workspace-проектов

const path = require('path');

module.exports = {
    resolve: {
        symlinks: false
    },

    module: {
        rules: [
            {
                test: /\.js$/,
                include: [
                    path.resolve(__dirname, 'src')
                ],
                loader: 'babel-loader'
            }
        ]
    }
};

Проблемы при миграции

После переключения режима могут измениться:

  • module ids
  • chunk ids
  • cache keys
  • результаты tree shaking
  • vendor splitting
  • поведение HMR

Иногда требуется полная очистка:

rm -rf node_modules/.cache

Практика крупных проектов

Во многих больших проектах:

  • symlinks: false используется по умолчанию
  • monorepo оптимизируется под workspace-пакеты
  • loader-правила строятся вокруг virtual path
  • realpath-resolution отключается ради стабильности

Особенно это характерно для:

  • React-monorepo
  • design-system платформ
  • internal package ecosystem
  • микрофронтендов
  • больших CI/CD систем