工程化专题|踩坑实录 | 明明是同一套 tsconfig,同事能跑你不行?Monorepo 里配置 paths 别名,有一百种方式翻车。
问题场景
一个标准的 Monorepo 项目(pnpm workspace + Turborepo),结构如下:
csharp
packages/
shared/ ← 共享工具库
src/
utils/
format.ts
index.ts ← export { formatDate } from './utils/format'
tsconfig.json
web/ ← 前端应用
src/
pages/
Dashboard.tsx
tsconfig.json
tsconfig.base.json ← 根路径配置
你在 web/tsconfig.json 里配了 paths 别名:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
然后在 Dashboard.tsx 里写:
ts
import { formatDate } from '@/utils/format' // ❌ 编辑器报红
// ✅ 但 tsc 编译通过了
// ❌ 但 Vite 构建报错:Module not found
三种工具,三种结果。 排查了一下午才发现------path 别名在 Monorepo 里根本不是「配了就行」这么简单。
原因分析
1. TypeScript 只看 tsconfig,不管「运行时」
paths 是 TypeScript 编译时概念 ------它告诉 TS 编译器「遇到 @/ 就到 src/ 去找」。但 打包工具(Vite/Webpack)不认 TS 的 paths。
tsc编译:✅ 通过(TS 自己认识 paths)- Vite 构建:❌ 报错(Vite 不知道 @/ 是什么)
- VS Code 检查:❌ 红标(取决于哪个 tsconfig 生效)
💥 根源:TypeScript paths 只是「类型层面的重映射」,不是「模块解析的物理重写」。
2. Monorepo 里的「幽灵 tsconfig」
Monorepo 下每层 package 都有自己的 tsconfig,还可能继承根 tsconfig.base.json。如果:
web/tsconfig.json配了 paths- VSCode 打开的却是根目录的
tsconfig.json(没有 paths 配置)
→ 编辑器里的路径别名全部红标。TypeScript Server 用的是哪个 tsconfig?看状态栏右下角。
3. pnpm 的「幽灵依赖隔离」+ alias ≠ alias
pnpm 用 symlink 严格隔离依赖。当你在 web 中配置 @/ 指向 src/,如果 @/ 实际解析到的是 pnpm 的虚拟 store 路径而非你的代码文件,alias 就会「打偏」。尤其在引用跨包资源时尤为明显。
解决方案
方案一:Vite + vite-tsconfig-paths(推荐)
Vite 项目最省心:
bash
pnpm add -D vite-tsconfig-paths
ts
// vite.config.ts
import tsconfigPaths from 'vite-tsconfig-paths'
export default defineConfig({
plugins: [tsconfigPaths()], // 自动读取 tsconfig paths
})
效果 :Vite build / dev 全部识别 @/ → src/,无需复制 paths。
方案二:双配置 + 显式 reference(Monorepo 生产方案)
对于多 package 共享 paths 的场景,推荐「参考路径 + 显式继承」:
json
// tsconfig.base.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@shared/*": ["packages/shared/src/*"],
"@web/*": ["packages/web/src/*"]
}
}
}
每个子 package 显式引用:
json
// packages/web/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["../../packages/shared/src/*"]
}
},
"references": [
{ "path": "../shared" }
]
}
在 VSCode 中指定项目 TS 版本使用哪个 tsconfig:
json
// .vscode/settings.json
{
"typescript.preferences.importModuleSpecifier": "relative",
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.tsserver.experimental.enableProjectDiagnostics": true
}
确保状态栏右下角显示的 TS 项目正确。
方案三:彻底放弃 paths,用 package name 别名(Monorepo 终极方案)
Monorepo 最不容易出问题的不是 paths,而是 package name。
json
// packages/shared/package.json
{
"name": "@myapp/shared",
"exports": {
".": "./src/index.ts",
"./*": "./src/*.ts"
}
}
json
// packages/web/package.json
{
"dependencies": {
"@myapp/shared": "workspace:*"
}
}
ts
// packages/web/src/Dashboard.tsx
import { formatDate } from '@myapp/shared/utils/format'
// ✅ 一切正常,无 paths 依赖,无工具绑定
优点: 完全不依赖 tsconfig paths,Vite/pnpm/Turborepo 原生支持,无「三端歧义」。
方案四:Webpack 需要 resolve.alias
如果你是老项目 Webpack:
ts
// webpack.config.js
const path = require('path')
module.exports = {
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
}
注意:sync 两份 paths(tsconfig + webpack)很容易不一致,建议用脚本生成:
ts
// scripts/tsconfig-to-webpack-alias.js
const tsconfig = require('../tsconfig.json')
const { paths, baseUrl } = tsconfig.compilerOptions
const alias = {}
for (const [key, [value]] of Object.entries(paths)) {
const aliasKey = key.replace('/*', '')
const aliasValue = path.resolve(__dirname, '..', baseUrl, value.replace('/*', ''))
alias[aliasKey] = aliasValue
}
// → 用 Webpack resolve.alias = alias
诊断清单:paths 别名不工作先查这 6 点
| # | 检查项 | 排查方式 |
|---|---|---|
| 1 | VSCode 右下角 TS 项目 | 确认使用的是 Web 包还是根 tsconfig |
| 2 | 打包工具是否读了 paths | Vite 需插件、Webpack 需 alias、ESBuild 需配置 |
| 3 | paths 格式 | "@/*" 必须对应 ["src/*"](不是 "src/") |
| 4 | Monorepo 引用层级 | paths 中的相对路径相对于 baseUrl |
| 5 | pnpm 的 symlink | 在 node_modules 里看你的包是否真实指向代码 |
| 6 | 同事能跑你不能跑 | 大概率 TS Server 版本不同或 tsconfig 缓存 |
要点总结
- TypeScript paths 只是「类型提示」,运行时/构建工具不认,需要额外桥接
- Vite 最佳实践 :
vite-tsconfig-paths插件一键搞定 - Monorepo 终极方案 :用 package name (
@scope/pkg) 替代路径别名,摆脱 paths 依赖 - Webpack 旧项目 :维护
resolve.alias与tsconfig.paths双向同步,建议生成脚本 - 排查路径崩了:先看 VSCode TS 项目、再看构建工具配没配、最后看 pnpm 链接是不是断了