В системе плагинов esbuild этап разрешения импортируемых путей
отделён от этапа загрузки содержимого модулей. За этот этап отвечает хук
onResolve, который перехватывает каждый импорт,
require, динамический import() и позволяет
изменить то, как путь будет интерпретирован сборщиком.
Основная роль onResolve — преобразование входного пути
модуля в формально разрешённый путь с указанием namespace, который далее
будет использован в onLoad. Этот хук фактически управляет
графом модулей.
Плагин в esbuild описывается через функцию setup, внутри
которой регистрируются обработчики:
const plugin = {
name: 'example-plugin',
setup(build) {
build.onResolve({ filter: /.*/ }, (args) => {
return {
path: args.path,
};
});
}
};
Ключевые элементы:
filter — регулярное выражение, определяющее, какие
импорты перехватываются(args) => result — логика резолваargs:
контекст разрешенияКаждый вызов onResolve получает контекст, описывающий
конкретный импорт:
argspath — исходный импортируемый путь
("./utils", "react", "fs" и
т.д.)
importer — модуль, из которого выполняется
импорт
namespace — namespace импортера (по умолчанию
file)
resolveDir — директория, относительно которой
выполняется разрешение
kind — тип импорта:
import-statementrequire-calldynamic-importentry-pointpluginData — данные, переданные предыдущими
плагинами
build.onResolve({ filter: /\.css$/ }, (args) => {
console.log(args.importer);
console.log(args.resolveDir);
return null;
});
onResolveВозвращаемый объект управляет дальнейшей обработкой модуля.
pathФинальный путь или идентификатор ресурса:
return {
path: '/absolute/path/to/file.css'
};
namespaceПозволяет перенаправить модуль в другой обработчик
onLoad:
return {
path: args.path,
namespace: 'custom'
};
Namespace используется как логический канал маршрутизации модулей.
externalПоле, исключающее модуль из бандла:
return {
path: args.path,
external: true
};
Используется для:
fs, path)sideEffectsПозволяет управлять tree-shaking на уровне конкретного импорта:
return {
path: args.path,
sideEffects: false
};
pluginDataПередача данных следующему этапу (onLoad):
return {
path: args.path,
pluginData: {
transformed: true
}
};
Каждый импорт проходит через последовательность:
onResolve (фильтрация и преобразование пути)onLoad (загрузка содержимого)Если несколько плагинов перехватывают один и тот же путь, порядок
определяется регистрацией: первый вернувший результат «побеждает», если
не используется явное продолжение через
onResolveResult.
filterРегулярное выражение определяет область применения:
build.onResolve({ filter: /^https?:\/\// }, (args) => {
return {
path: args.path,
namespace: 'http'
};
});
Приоритет задаётся порядком регистрации:
Типичный кейс — подмена импортов:
build.onResolve({ filter: /^lodash$/ }, () => {
return {
path: 'lodash-es'
};
});
Это позволяет:
build.onResolve({ filter: /^@utils\// }, (args) => {
return {
path: args.path.replace('@utils/', '/src/utils/')
};
});
Можно учитывать контекст:
build.onResolve({ filter: /.*/ }, (args) => {
if (args.importer.includes('node_modules')) {
return {
path: args.path,
external: true
};
}
});
Namespace играет ключевую роль в архитектуре плагинов.
file — обычные файлыexternal — исключённые зависимостиbuild.onResolve({ filter: /\.md$/ }, (args) => {
return {
path: args.path,
namespace: 'markdown'
};
});
Далее:
build.onLoad({ filter: /.*/, namespace: 'markdown' }, () => {
return {
contents: 'export default "parsed markdown";',
loader: 'js'
};
});
pluginData используется как механизм межэтапной
коммуникации:
build.onResolve({ filter: /\.svg$/ }, (args) => {
return {
path: args.path,
pluginData: {
optimize: true
}
};
});
build.onLoad({ filter: /.*/, namespace: 'file' }, (args) => {
if (args.pluginData?.optimize) {
// оптимизация SVG
}
return {
contents: '...',
loader: 'text'
};
});
onResolve может сигнализировать об ошибке:
build.onResolve({ filter: /\.secret$/ }, (args) => {
return {
errors: [{
text: 'Доступ к секретным файлам запрещён',
location: null
}]
};
});
Также возможно возвращать warnings.
Возврат null означает:
build.onResolve({ filter: /.*/ }, () => {
return null;
});
build.onResolve({ filter: /^https:\/\// }, (args) => {
return {
path: args.path,
namespace: 'http'
};
});
build.onResolve({ filter: /^@components\// }, (args) => {
return {
path: args.path.replace('@components/', './src/components/')
};
});
build.onResolve({ filter: /^[a-z].*/ }, (args) => {
if (args.path.startsWith('node:')) {
return { path: args.path, external: true };
}
});
onLoad для каждого типаonResolve — нормализацияЕсли несколько onResolve подходят под один импорт:
Это делает порядок регистрации критическим фактором архитектуры.
onLoadonResolve определяет:
onLoad затем использует эти данные для:
Связка этих двух хуков формирует полный цикл обработки модуля в esbuild.