问题介绍
按照这个官方文档配置 shadcn + vite 项目后,遇到个错误:

按照官方文档配置,理应是没有错误的,但是我的项目特殊点就在于是一个 Monorepo 项目。
所以,当你在一个 Monorepo 里使用 TypeScript + ESLint(Flat Config,eslint.config.js)时,常会遇到下面这个解析错误:
sh
Parsing error: No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present:
- /Users/.../packages/SearchChat
- /Users/.../packages/SearchChatUI
You'll need to explicitly set tsconfigRootDir in your parser options.
See: https://typescript-eslint.io/packages/parser/#tsconfigrootdireslint
项目背景
- Monorepo 项目结构示例:
sh
/Users/.../ui-common
├── apps/
│ └── react-jsx/
├── packages/
│ ├── ChatMessage/
│ ├── CustomIcons/
│ ├── DatePicker/
│ ├── DropdownList/
│ ├── EntityUI/
│ └── SearchChatUI/
└── pnpm-workspace.yaml
- 每个包(例如
packages/SearchChatUI)通常都有自己的tsconfig.json(含references到tsconfig.app.json/tsconfig.node.json)、eslint.config.js、package.json。 - ESLint 在启用类型感知规则(或需要类型信息的配置)时,会通过
@typescript-eslint/parser加载 TypeScript Program,这需要明确告诉它:以哪个目录为根去解析project(tsconfig*.json)。
错误现象
- 在 Monorepo 根或任一包里运行
eslint,报错显示发现多个候选TSConfigRootDir。 - 这是因为解析器试图自动探测 tsconfig 根目录,但同时看到了多个包的 tsconfig,于是拒绝继续。
原因分析
- TypeScript-ESLint 的解析器需要一个"根目录"(
tsconfigRootDir)来解释你提供的parserOptions.project(即哪些tsconfig*.json参与构建类型信息)。 - 在 Monorepo 中,如果没有明确为每个包设定独立的
tsconfigRootDir与对应的project,解析器会在工作区内"看见"多个包的 tsconfig,从而无法确定到底应该用哪个根,最终报错。
快速修复(针对单个包)
以 packages/SearchChatUI 为例,给它的 eslint.config.js 增加明确的 parserOptions.tsconfigRootDir 和 parserOptions.project 即可。
js
// packages/SearchChatUI/eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
// 关键设置:指向当前包目录,避免 Monorepo 下的多 tsconfig 混淆
tsconfigRootDir: __dirname,
// 指定当前包使用的 tsconfig 列表(路径相对于 tsconfigRootDir)
project: [
'./tsconfig.json',
'./tsconfig.app.json',
'./tsconfig.node.json',
],
sourceType: 'module',
},
},
},
])
验证:
- 进入包目录运行
npm run lint(保证命令在包内执行) - 预期不再出现 Parsing error
在 Monorepo 根统一配置的做法(推荐)
如果你倾向于在根目录放一个统一的 eslint.config.js,可以使用 "按包 override" 的方式,让每个包都明确自己的 tsconfigRootDir 和 project。
示例(伪代码,按需调整包路径):
js
// eslint.config.js at workspace root
import { defineConfig } from 'eslint/config'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import js from '@eslint/js'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const rootDir = path.dirname(fileURLToPath(import.meta.url))
const pkg = (dir) => path.join(rootDir, 'packages', dir)
export default defineConfig([
{
files: ['packages/SearchChatUI/**/*.{ts,tsx}'],
extends: [js.configs.recommended, tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
tsconfigRootDir: pkg('SearchChatUI'),
project: [
path.join(pkg('SearchChatUI'), 'tsconfig.json'),
path.join(pkg('SearchChatUI'), 'tsconfig.app.json'),
path.join(pkg('SearchChatUI'), 'tsconfig.node.json'),
],
sourceType: 'module',
},
},
},
{
files: ['packages/SearchChat/**/*.{ts,tsx}'],
extends: [js.configs.recommended, tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
tsconfigRootDir: pkg('SearchChat'),
project: [
path.join(pkg('SearchChat'), 'tsconfig.json'),
path.join(pkg('SearchChat'), 'tsconfig.app.json'),
path.join(pkg('SearchChat'), 'tsconfig.node.json'),
],
sourceType: 'module',
},
},
},
// 为其他包继续添加 overrides...
])
要点:
- 每个包的 override 都拥有自己的
tsconfigRootDir。 project数组中的路径要基于该包目录。- 保持按包运行
eslint .(或通过 workspace 脚本定位到包)能减少路径解析混乱。
常见坑位与提示
-
project路径必须相对于tsconfigRootDir,不要写相对于工作区根的路径。 -
若你使用的是 TypeScript-ESLint 的"类型感知"配置(例如
tseslint.configs.recommendedTypeChecked或启用了需要类型信息的规则),一定要提供tsconfigRootDir与project。 -
如果你不需要类型感知规则(为了更快的性能),可以只用非 type-checked 的推荐集,省略
project(但要权衡规则能力):jsextends: [tseslint.configs.recommended] // 非类型感知 // 不设置 parserOptions.project -
在包内运行
npm run lint(eslint .)比在根随意运行更可控。 -
ESM 环境下,要用
fileURLToPath(import.meta.url)获取当前文件路径来计算__dirname。
验证步骤
- 在目标包目录执行:
npm run lint
- 确认不再出现 "No tsconfigRootDir was set ... multiple candidate TSConfigRootDirs ..." 的错误。
- 如果还有包报同样错误,逐个为它们的配置添加
tsconfigRootDir和project。
性能与类型感知
- 类型感知规则需要构建 TypeScript Program,解析器会加载并分析
project指定的 tsconfig;在大 Monorepo 中这可能较慢。 - 推荐做法:
- 只有在确实需要类型规则的包上开启
project。 - 使用按包 override 控制范围。
- 结合 CI 分层执行(先非类型感知快速检查,再在关键包跑类型感知规则)。
- 只有在确实需要类型规则的包上开启
小结
这个报错本质是 Monorepo 环境下 "类型规则需要明确上下文" 的提醒。只要为每个包设定清晰的 tsconfigRootDir 与 project,ESLint 就能准确地获取类型信息并稳定工作。按包划分 override 是根级统一配置的好方式;而在包内独立配置则更为直觉。
参考链接
- TypeScript-ESLint Parser 文档(
tsconfigRootDir):
typescript-eslint.io/packages/pa... - TypeScript-ESLint 配置指南(Flat Config)
typescript-eslint.io/getting-sta... - ESLint Flat Config 官方文档
eslint.org/docs/latest...