Механизм разрешения модулей в Webpack построен поверх библиотеки enhanced-resolve, которая реализует расширяемый пайплайн поиска и интерпретации импортов. Вся система резолва представляет собой последовательность хуков, управляемых через Tapable, где каждый этап может быть перехвачен, модифицирован или полностью заменён через плагины.
Кастомные резолверы используются для изменения поведения поиска модулей: переопределения путей, внедрения виртуальных модулей, трансформации импортов, реализации альтернативных схем резолва (монорепозитории, CDN-импорты, виртуальные FS, условные окружения).
Основная точка расширения — поле resolve.plugins в конфигурации сборщика. Каждый плагин получает доступ к ResolverFactory и цепочке хуков конкретного резолвера.
Структура резолвера и ключевые хуки
Резолвер в Webpack состоит из нескольких уровней:
Ключевые хуки:
Каждый хук позволяет вмешиваться в процесс и изменять как входные данные запроса, так и результат поиска.
Подключение кастомного резолвера через конфигурацию
Базовый способ добавления кастомной логики — регистрация плагина:
// webpack.config.js
module.exports = {
resolve: {
plugins: [
new CustomResolverPlugin()
]
}
};
Плагин должен реализовать метод apply, который получает доступ к resolverFactory:
class CustomResolverPlugin {
apply(resolverFactory) {
resolverFactory.hooks.resolver.for('normal').tap(
'CustomResolverPlugin',
(resolver) => {
resolver.hooks.resolve.tapAsync(
'CustomResolverPlugin',
(request, resolveContext, callback) => {
callback();
}
);
}
);
}
}
Модель работы enhanced-resolve
Система резолва работает по следующей цепочке:
Кастомные резолверы могут вмешиваться на любом этапе, но наиболее часто используются на стадиях:
Пример перехвата и подмены модулей
Один из частых сценариев — подмена импортируемого модуля:
class AliasByConditionPlugin {
apply(resolverFactory) {
resolverFactory.hooks.resolver.for('normal').tap(
'AliasByConditionPlugin',
(resolver) => {
resolver.hooks.file.tapAsync(
'AliasByConditionPlugin',
(request, callback) => {
if (request.request === 'legacy-lib') {
const obj = {
...request,
request: 'modern-lib'
};
return callback(null, obj);
}
callback();
}
);
}
);
}
}
Такой подход позволяет внедрять условные алиасы без использования resolve.alias.
Работа с виртуальными модулями
Кастомные резолверы часто используются для генерации модулей, которых физически не существует.
Пример — виртуальный модуль:
class VirtualModuleResolver {
apply(resolverFactory) {
resolverFactory.hooks.resolver.for('normal').tap(
'VirtualModuleResolver',
(resolver) => {
resolver.hooks.resolve.tapAsync(
'VirtualModuleResolver',
(request, context, callback) => {
if (request.request === 'virtual:config') {
const newRequest = {
...request,
path: '/__virtual__/',
request: './config.js',
resolved: true
};
return callback(null, newRequest);
}
callback();
}
);
}
);
}
}
В связке с loader’ами это позволяет реализовывать конфигурации, генерируемые на лету.
Интеграция с filesystem и memory-based storage
enhanced-resolve поддерживает замену файловой системы через inputFileSystem:
resolverFactory.hooks.resolver.for('normal').tap(
'MemoryFSResolver',
(resolver) => {
resolver.fileSystem = myMemoryFS;
}
);
Это используется для:
Кастомный резолвер может полностью заменить чтение файлов, включая package.json и symlink обработку.
Обработка package.json и mainFields
Один из самых сложных этапов резолва — выбор entry точки пакета.
Webpack анализирует:
Кастомный резолвер может изменить приоритет:
resolver.hooks.descriptionFile.tapAsync(
'CustomMainFieldsPlugin',
(content, path, callback) => {
if (content.browser) {
content.preferredEntry = content.browser;
}
callback(null, content);
}
);
Это позволяет реализовать:
Расширение логики extensions
Механизм extensions управляет поиском файлов без указания расширения:
resolve: {
extensions: ['.ts', '.js', '.json']
}
Кастомный резолвер может вмешаться в процесс:
resolver.hooks.file.tapAsync(
'ExtensionPriorityPlugin',
(request, callback) => {
if (request.request && !request.request.includes('.')) {
request.request = request.request + '.custom.js';
}
callback(null, request);
}
);
Это используется для:
Монорепозитории и кастомные директории модулей
В сложных проектах modules переопределяется:
resolve: {
modules: [
'src',
'shared',
'node_modules'
]
}
Кастомный резолвер может расширять логику поиска:
resolver.hooks.module.tapAsync(
'WorkspaceModulesPlugin',
(request, callback) => {
if (request.request.startsWith('@shared/')) {
request.request = request.request.replace('@shared/', '');
request.path = '/repo/shared/';
}
callback(null, request);
}
);
Это применяется в:
Контекстный резолв и условия окружения
Кастомные резолверы часто добавляют условность:
Пример:
resolver.hooks.resolve.tapAsync(
'EnvResolverPlugin',
(request, context, callback) => {
if (process.env.APP_ENV === 'production') {
if (request.request === 'debug-tools') {
return callback(null, false);
}
}
callback();
}
);
Такой механизм позволяет исключать модули из графа зависимостей на этапе резолва.
Работа с symlinks и linked packages
Webpack по умолчанию поддерживает symlinks, но поведение можно изменить:
resolve: {
symlinks: false
}
Кастомный резолвер может полностью контролировать этот процесс, изменяя realpath:
resolver.hooks.result.tapAsync(
'SymlinkResolverPlugin',
(request, callback) => {
if (request.path.includes('linked-package')) {
request.path = fs.realpathSync(request.path);
}
callback(null, request);
}
);
Это критично для:
Отладка и диагностика резолва
Резолвер предоставляет детализированную трассировку:
Кастомные плагины могут добавлять собственную телеметрию:
resolver.hooks.noResolve.tap('DebugResolver', (request) => {
console.log('Failed resolve:', request.request, request.path);
});
Также возможно расширение stats:
Композиция нескольких резолверов
Сложные проекты используют несколько резолверов одновременно:
Каждый имеет собственную цепочку хуков.
resolverFactory.hooks.resolver.for('context').tap(
'ContextResolverPlugin',
(resolver) => {
// отдельная логика для require.context
}
);
Это важно для:
Глубокая интеграция с системой плагинов Tapable
Вся система резолва построена на Tapable hooks:
Кастомные резолверы фактически представляют собой композицию этих хуков.
Это даёт возможность:
Паттерны проектирования кастомных резолверов
На практике используются несколько устойчивых подходов:
Alias-first resolution Перехват и замена путей до стандартного резолва
Middleware chaining Несколько плагинов, каждый модифицирует request
Virtual filesystem mapping Полная абстракция над файловой системой
Environment-based routing Разные графы зависимостей для разных сред
Namespace resolvers Специальные префиксы (например, “virtual:”, “@api:”, “~internal:”)
Каждый паттерн влияет на структуру сборки и граф зависимостей, формируя альтернативную модель модульности поверх стандартного Node.js resolution algorithm.