modules: нестандартные директории для поиска

Механизм разрешения модулей в Webpack определяет, где искать импортируемые файлы и пакеты. По умолчанию Webpack использует стандартный каталог node_modules, а также относительные пути относительно текущего файла.

Параметр resolve.modules позволяет определить дополнительные директории, в которых Webpack будет искать модули при импорте без относительного пути.

Базовый пример:

const path = require('path');

module.exports = {
  resolve: {
    modules: [
      'node_modules',
      path.resolve(__dirname, 'src')
    ]
  }
};

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

import Button from 'components/Button';
import api from 'services/api';

Вместо:

import Button from '../. ./components/Button';
import api from '../. ./services/api';

Как работает поиск модулей

Когда Webpack встречает импорт:

import Header from 'components/Header';

он начинает последовательный поиск:

  1. Проверяет встроенные alias
  2. Проверяет resolve.modules
  3. Проверяет node_modules
  4. Проверяет расширения из resolve.extensions

Если указано:

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules'
  ]
}

Webpack попытается найти:

src/components/Header

затем:

node_modules/components/Header

Значение порядка директорий

Порядок элементов внутри modules критически важен.

Пример:

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules'
  ]
}

Webpack сначала ищет модуль в src, а уже потом в node_modules.

Если поменять порядок:

resolve: {
  modules: [
    'node_modules',
    path.resolve(__dirname, 'src')
  ]
}

приоритет получат npm-пакеты.


Использование абсолютных путей

Наиболее распространённая практика — использование абсолютных путей через path.resolve.

Правильный вариант:

const path = require('path');

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules'
  ]
}

Нежелательный вариант:

resolve: {
  modules: ['./src']
}

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


Упрощение структуры импортов

Без resolve.modules глубокая вложенность приводит к неудобным путям:

import Modal from '../. ./. ./. ./components/ui/Modal';

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

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules'
  ]
}

импорт становится значительно чище:

import Modal from 'components/ui/Modal';

Типичная структура проекта

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

project/
├── src/
│   ├── components/
│   ├── pages/
│   ├── hooks/
│   ├── services/
│   └── utils/
├── node_modules/
├── webpack.config.js
└── package.json

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

const path = require('path');

module.exports = {
  resolve: {
    modules: [
      path.resolve(__dirname, 'src'),
      'node_modules'
    ]
  }
};

Теперь возможны импорты:

import HomePage from 'pages/HomePage';
import useAuth from 'hooks/useAuth';
import formatDate from 'utils/formatDate';

Несколько пользовательских директорий

Webpack поддерживает любое количество директорий:

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    path.resolve(__dirname, 'shared'),
    path.resolve(__dirname, 'core'),
    'node_modules'
  ]
}

Это полезно в крупных монорепозиториях и многоуровневых архитектурах.


Использование в монорепозиториях

Структура:

monorepo/
├── packages/
│   ├── ui/
│   ├── core/
│   └── utils/
├── apps/
│   └── frontend/
│       └── webpack.config.js

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

const path = require('path');

module.exports = {
  resolve: {
    modules: [
      path.resolve(__dirname, '../. ./packages'),
      'node_modules'
    ]
  }
};

Импорт:

import Button from 'ui/Button';
import logger from 'core/logger';

Отличие modules от alias

resolve.modules задаёт каталоги поиска.

resolve.alias создаёт псевдонимы.

modules

resolve: {
  modules: [
    path.resolve(__dirname, 'src')
  ]
}

Импорт:

import Button from 'components/Button';

alias

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

Импорт:

import Button from '@components/Button';

Когда лучше использовать modules

modules подходит:

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

Когда лучше использовать alias

alias предпочтительнее:

  • при необходимости явного пространства имён;
  • для предотвращения конфликтов;
  • для переопределения библиотек;
  • для создания стабильных префиксов.

Потенциальные конфликты имён

Опасная конфигурация:

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules'
  ]
}

Если внутри src появится папка:

src/react/

импорт:

import React from 'react';

может начать ссылаться не на npm-пакет, а на локальную директорию.


Защита от конфликтов

Наиболее безопасный подход — использование namespace-директорий:

src/app/
src/shared/
src/core/

Импорты:

import Button from 'app/components/Button';

Либо использование alias:

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

Совместимость с TypeScript

Если используется TypeScript, настройки Webpack необходимо синхронизировать с tsconfig.json.

Webpack:

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules'
  ]
}

TypeScript:

{
  "compilerOptions": {
    "baseUrl": "src"
  }
}

Иначе TypeScript не сможет корректно разрешать импорты.


Совместимость с Babel

При использовании Babel часто подключают:

babel-plugin-module-resolver

Пример:

plugins: [
  ['module-resolver', {
    root: ['./src']
  }]
]

Без синхронизации конфигураций возможны ситуации, когда:

  • Babel собирает проект успешно;
  • Webpack не находит модуль;
  • IDE показывает ошибку;
  • Jest не понимает пути.

Влияние на IDE и автодополнение

Некоторые IDE не учитывают resolve.modules автоматически.

Для корректной работы автодополнения требуется дополнительная настройка:

  • TypeScript baseUrl
  • jsconfig.json
  • конфигурация ESLint
  • настройка Jest
  • настройки IDE

Настройка ESLint

Для корректного анализа импортов:

npm install eslint-import-resolver-webpack --save-dev

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

module.exports = {
  settings: {
    'import/resolver': {
      webpack: {
        config: 'webpack.config.js'
      }
    }
  }
};

Настройка Jest

Jest не использует Webpack-конфигурацию автоматически.

Требуется указать:

module.exports = {
  moduleDirectories: [
    'node_modules',
    'src'
  ]
};

Производительность поиска модулей

Каждая дополнительная директория увеличивает количество проверок файловой системы.

Плохой пример:

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    path.resolve(__dirname, 'shared'),
    path.resolve(__dirname, 'core'),
    path.resolve(__dirname, 'legacy'),
    path.resolve(__dirname, 'old'),
    path.resolve(__dirname, 'experimental'),
    'node_modules'
  ]
}

Чем больше директорий, тем медленнее разрешение модулей.

Особенно заметно:

  • в больших проектах;
  • при HMR;
  • при watch-режиме;
  • на HDD;
  • в Docker-контейнерах;
  • в CI-средах.

Использование вместе с resolve.extensions

Webpack комбинирует modules и extensions.

Пример:

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules'
  ],

  extensions: ['.js', '.jsx', '.ts', '.tsx']
}

Импорт:

import App from 'pages/App';

Webpack проверит:

src/pages/App.js
src/pages/App.jsx
src/pages/App.ts
src/pages/App.tsx

Использование с динамическими импортами

Настройка действует и для import():

const module = await import('services/api');

Webpack будет использовать те же правила поиска.


Внутренние механизмы Enhanced Resolve

Webpack использует библиотеку:

enhanced-resolve

Именно она реализует:

  • поиск файлов;
  • проверку расширений;
  • работу alias;
  • поддержку symlink;
  • кэширование путей;
  • modules;
  • mainFields;
  • exports;
  • imports.

Разрешение директорий

При импорте:

import Layout from 'layouts/Main';

Webpack пытается найти:

layouts/Main.js
layouts/Main.jsx
layouts/Main.ts
layouts/Main/index.js

Поведение зависит от:

resolve.mainFiles

По умолчанию:

['index']

Пример полной конфигурации

const path = require('path');

module.exports = {
  resolve: {
    modules: [
      path.resolve(__dirname, 'src'),
      path.resolve(__dirname, 'shared'),
      'node_modules'
    ],

    extensions: [
      '.js',
      '.jsx',
      '.ts',
      '.tsx'
    ],

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

Практические рекомендации

Для небольших проектов

Достаточно:

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules'
  ]
}

Для крупных приложений

Лучше использовать:

  • alias
  • архитектурные namespace
  • ограниченное количество директорий
  • единый root-каталог

Для монорепозиториев

Желательно:

  • избегать глобального поиска по всему репозиторию;
  • использовать workspace-структуры;
  • минимизировать неоднозначность импортов;
  • изолировать пакеты.

Частые ошибки

Отсутствие node_modules

Ошибка:

resolve: {
  modules: [
    path.resolve(__dirname, 'src')
  ]
}

В этом случае Webpack может перестать находить npm-пакеты.

Правильно:

resolve: {
  modules: [
    path.resolve(__dirname, 'src'),
    'node_modules'
  ]
}

Использование относительных путей

Плохой вариант:

modules: ['./src']

Правильный вариант:

modules: [path.resolve(__dirname, 'src')]

Конфликты с npm-пакетами

Опасная структура:

src/lodash/
src/react/
src/vue/

Такие названия способны перекрыть реальные библиотеки.


Несинхронизированные конфигурации

Если Webpack настроен, а TypeScript или Jest — нет, появляются ошибки:

Module not found
Cannot resolve module
Unable to locate module

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