tsconfig.json в разрешении модулейВ экосистеме TypeScript файл tsconfig.json определяет не
только параметры компиляции, но и правила резолва модулей. Среди
наиболее значимых опций, влияющих на систему импорта:
baseUrl — базовый путь для относительного разрешения
не-относительных импортовpaths — псевдонимы модулей (alias mapping)Эти механизмы активно используются в крупных проектах для упрощения структуры импортов и сокращения длинных относительных путей вида:
import { Button } from '../. ./. ./. ./components/ui/button'
до более декларативного:
import { Button } from '@ui/button'
или:
import { Button } from 'components/ui/button'
baseUrlОпция baseUrl задаёт корневую директорию, относительно
которой TypeScript интерпретирует неотносительные импорты.
Пример:
{
"compilerOptions": {
"baseUrl": "src"
}
}
При такой конфигурации импорт:
import { api } from "services/api"
будет разрешён как:
src/services/api
node_modulespathspathsОпция paths расширяет систему резолва, позволяя задавать
алиасы модулей через шаблоны.
Пример конфигурации:
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@ui/*": ["components/ui/*"],
"@lib/*": ["lib/*"],
"@features/*": ["features/*"]
}
}
}
При импорте:
import { Modal } from "@ui/modal"
TypeScript преобразует путь в:
src/components/ui/modal
Символ * работает как wildcard:
| Шаблон | Соответствие |
|---|---|
@ui/* |
@ui/button → components/ui/button |
@lib/* |
@lib/http/client → lib/http/client |
Esbuild ориентирован на скорость и минимализм. В отличие от TypeScript Compiler API или Webpack, он:
tsconfig.json для резолва путей
автоматическиbaseUrl и paths без
дополнительной настройкиПри стандартной сборке:
esbuild src/index.ts --bundle --outdir=dist
импорт:
import { Button } from "@ui/button"
приведёт к ошибке:
Could not resolve "@ui/button"
Для корректной работы алиасов требуется использование плагина, который реализует трансформацию путей на этапе резолва.
На практике применяется подход:
tsconfig.jsonbaseUrl и pathsonResolvepaths через плагинБазовая структура плагина выглядит следующим образом:
import fs from "fs"
import path from "path"
export const tsconfigPathsPlugin = (tsconfigPath = "./tsconfig.json") => {
const config = JSON.parse(fs.readFileSync(tsconfigPath, "utf8"))
const baseUrl = config.compilerOptions?.baseUrl || "."
const paths = config.compilerOptions?.paths || {}
const aliases = []
for (const [key, values] of Object.entries(paths)) {
const pattern = key.replace("/*", "")
const targets = values.map(v => v.replace("/*", ""))
aliases.push({
pattern,
targets
})
}
return {
name: "tsconfig-paths",
setup(build) {
for (const alias of aliases) {
build.onResolve({ filter: new RegExp(`^${alias.pattern}`) }, args => {
const matchedPath = args.path.replace(alias.pattern, "")
for (const target of alias.targets) {
const resolved = path.join(baseUrl, target, matchedPath)
return {
path: path.resolve(resolved)
}
}
})
}
}
}
}
const config = JSON.parse(fs.readFileSync(tsconfigPath, "utf8"))
Извлекаются:
compilerOptions.baseUrlcompilerOptions.pathsEsbuild требует явной логики сопоставления, поэтому:
@ui/* → @ui["components/ui/*"] → components/uiWildcard удаляется, чтобы получить базовый префикс.
onResolveEsbuild использует систему хуков:
build.onResolve({ filter: /^@ui/ }, ...)
Этот хук перехватывает каждый импорт, начинающийся с
@ui.
Ключевая логика:
const resolved = path.join(baseUrl, target, matchedPath)
Пример:
@ui/buttonsrccomponents/uisrc/components/ui/buttonTypeScript позволяет задавать массив значений:
"paths": {
"@lib/*": [
"lib/*",
"shared/lib/*"
]
}
В этом случае резолвер должен:
Расширенная версия:
for (const target of alias.targets) {
const resolved = path.resolve(baseUrl, target, matchedPath)
if (fs.existsSync(resolved + ".ts") || fs.existsSync(resolved + ".js")) {
return { path: resolved }
}
}
baseUrl
и относительных путейВажно учитывать, что baseUrl влияет только на
non-relative imports.
Поведение:
| Импорт | Поведение |
|---|---|
./utils |
обычный relative resolve |
../utils |
обычный relative resolve |
utils |
учитывает baseUrl |
@alias/x |
обрабатывается через paths |
Esbuild не применяет baseUrl напрямую, поэтому плагин
обязан учитывать его вручную.
Конфигурация:
"baseUrl": "src",
"paths": {
"@app/*": ["app/*"]
}
Но фактическая структура:
src/application/
Результат — резолвер указывает на несуществующие пути.
В монорепозиториях часто встречаются:
tsconfig.jsonbaseUrlpathsEsbuild плагин должен учитывать контекст пакета, иначе резолв будет некорректным.
Базовый плагин может возвращать путь без проверки, что приводит к ошибкам позже в сборке. Более надёжный подход:
.ts, .tsx, .js,
.jsxindex файловTypeScript автоматически поддерживает:
import { x } from "@lib"
→
@lib/index.ts
В Esbuild это нужно реализовать явно:
const candidates = [
resolved,
path.join(resolved, "index.ts"),
path.join(resolved, "index.js")
]
При --bundle Esbuild:
onResolve для каждого узлаЭто означает, что:
paths вызывается только при первом
обращенииОдно из преимуществ Esbuild — высокая скорость резолва. Однако плагины могут влиять на производительность:
fs.existsSync внутри onResolvepathsПеред запуском Esbuild можно преобразовать
tsconfig paths в:
Распространённые решения:
esbuild-plugin-tsconfig-paths@esbuild-plugins/tsconfig-pathsОни уже реализуют:
Типичный конфиг Esbuild с поддержкой paths:
import { build } from "esbuild"
import { tsconfigPathsPlugin } from "./plugins/tsconfig-paths.js"
build({
entryPoints: ["src/index.ts"],
bundle: true,
outdir: "dist",
platform: "node",
plugins: [
tsconfigPathsPlugin("./tsconfig.json")
]
})
Важно понимать различие:
Поэтому возможна ситуация:
Причина почти всегда в отсутствии синхронизации
paths.
При использовании разных модульных систем:
module: commonjsmodule: esnextпуть резолва остаётся одинаковым, но:
tsconfig pathsbaseUrl всегда применяется как корневая точкаpaths требуют явного маппинга через
onResolve* должен преобразовываться в сегменты
пути