В Esbuild система плагинов построена вокруг перехвата этапов резолва
и загрузки модулей. Это даёт возможность изменять поведение сборщика без
модификации исходного кода проекта. Основной инструмент для работы с
импортами — хук onResolve, который управляет тем, как
интерпретируются пути модулей, и onLoad, который определяет
содержимое загружаемых модулей.
Плагин, предназначенный для замены импортов во время сборки, обычно решает задачи:
Ключевая особенность подхода Esbuild — разделение этапов:
Именно комбинация этих двух механизмов используется для полноценной подмены импортов.
Плагин Esbuild представляет собой объект с методом
setup, который получает API сборщика.
const replaceImportsPlugin = (options = {}) => ({
name: 'replace-imports',
setup(build) {
// логика плагина
}
});
Внутри setup регистрируются обработчики:
build.onResolvebuild.onLoadonResolve позволяет изменить путь модуля до того, как
Esbuild начнёт его загружать.
Задача: заменить импорт libA на libB.
const replaceImportsPlugin = () => ({
name: 'replace-imports',
setup(build) {
build.onResolve({ filter: /^libA$/ }, (args) => {
return {
path: 'libB'
};
});
}
});
В этом случае любой импорт:
import x from 'libA';
будет перенаправлен в:
import x from 'libB';
Esbuild позволяет задавать виртуальные пространства имён через
namespace, что особенно полезно при сложных подменах.
build.onResolve({ filter: /^virtual:/ }, (args) => {
return {
path: args.path,
namespace: 'virtual-modules'
};
});
Далее onLoad обрабатывает этот namespace:
build.onLoad({ filter: /.*/, namespace: 'virtual-modules' }, () => {
return {
contents: `export const value = 42;`,
loader: 'js'
};
});
Такой подход позволяет полностью заменить модуль без файловой системы.
Иногда недостаточно изменить путь — требуется полностью подменить содержимое модуля.
build.onResolve({ filter: /^env-config$/ }, () => {
return { path: 'env-config', namespace: 'env' };
});
build.onLoad({ filter: /.*/, namespace: 'env' }, () => {
return {
contents: `
export const API_URL = "https://prod.api.com";
`,
loader: 'js'
};
});
Здесь импорт остаётся прежним, но содержимое подменяется на этапе загрузки.
Один из наиболее распространённых сценариев — замена модулей в зависимости от режима сборки.
const env = process.env.NODE_ENV;
const replaceImportsPlugin = () => ({
name: 'replace-imports',
setup(build) {
build.onResolve({ filter: /^logger$/ }, () => {
return {
path: env === 'production' ? 'logger.prod' : 'logger.dev'
};
});
}
});
В production подключается лёгкая версия логгера, в development — расширенная.
Esbuild не имеет встроенного alias как Webpack, но его легко
реализовать через onResolve.
const aliasMap = {
'@': './src',
'@utils': './src/utils',
'@api': './src/api'
};
build.onResolve({ filter: /^@/ }, (args) => {
for (const alias in aliasMap) {
if (args.path.startsWith(alias)) {
return {
path: args.path.replace(alias, aliasMap[alias])
};
}
}
});
Такой механизм позволяет полностью эмулировать поведение alias-систем.
Иногда требуется заменить стороннюю библиотеку собственной реализацией.
build.onResolve({ filter: /^lodash$/ }, () => {
return {
path: 'my-lodash-shim',
external: false
};
});
Или наоборот — исключить модуль из бандла:
build.onResolve({ filter: /^fs$/ }, () => {
return {
path: 'fs',
external: true
};
});
Esbuild позволяет гибко перехватывать группы импортов.
build.onResolve({ filter: /^@components\/.*$/ }, (args) => {
return {
path: args.path.replace('@components', './src/components')
};
});
Это позволяет централизованно управлять структурой проекта без изменения исходных импортов.
Виртуальные модули часто используются для:
build.onResolve({ filter: /^virtual:config$/ }, () => {
return {
path: 'virtual:config',
namespace: 'virtual'
};
});
build.onLoad({ filter: /.*/, namespace: 'virtual' }, () => {
const config = {
mode: process.env.NODE_ENV,
version: '1.0.0'
};
return {
contents: `export default ${JSON.stringify(config)}`,
loader: 'json'
};
});
Аргумент args в onResolve содержит
контекст:
importer — файл, из которого идёт импортpath — импортируемый модульЭто позволяет делать контекстные замены:
build.onResolve({ filter: /^service$/ }, (args) => {
if (args.importer.includes('/admin/')) {
return { path: 'admin-service' };
}
return { path: 'public-service' };
});
Так реализуются разные реализации одного модуля в зависимости от слоя приложения.
Плагины могут работать последовательно, создавая цепочки преобразований.
build.onResolve({ filter: /^api$/ }, () => {
return { path: 'api/v2' };
});
build.onResolve({ filter: /^api\/v2$/ }, () => {
return { path: 'api/v2/index' };
});
Такая схема полезна при постепенной миграции архитектуры.
При замене импортов важно учитывать расширения файлов:
build.onResolve({ filter: /\.ts$/ }, (args) => {
return {
path: args.path.replace('.ts', '.js')
};
});
Или более аккуратно:
build.onResolve({ filter: /\.[jt]s$/ }, (args) => {
return {
path: args.path.replace(/\.[jt]s$/, '.js')
};
});
При работе с заменой импортов важно учитывать особенности:
onResolve не изменяет содержимое файла, только
путьonLoad всегда привязан к результату резолваexternal: true полностью исключает модуль из графа
сборкиОсобенно критично учитывать порядок регистрации обработчиков: Esbuild применяет их в порядке добавления.
В реальных проектах замена импортов часто комбинируется:
const plugin = () => ({
name: 'complex-replace',
setup(build) {
// алиасы
build.onResolve({ filter: /^@utils/ }, (args) => {
return { path: args.path.replace('@utils', './src/utils') };
});
// окружение
build.onResolve({ filter: /^config$/ }, () => {
return {
path: process.env.NODE_ENV === 'prod'
? 'config.prod'
: 'config.dev'
};
});
// мокирование
build.onResolve({ filter: /^http-client$/ }, () => {
return { path: 'http-client-mock' };
});
}
});