🚀 “踩坑日记”:shadcn + Vite 在 Monorepo 中配置报错

问题介绍

ui.shadcn.com/docs/instal...

按照这个官方文档配置 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(含 referencestsconfig.app.json / tsconfig.node.json)、eslint.config.jspackage.json
  • ESLint 在启用类型感知规则(或需要类型信息的配置)时,会通过 @typescript-eslint/parser 加载 TypeScript Program,这需要明确告诉它:以哪个目录为根去解析 projecttsconfig*.json)。

错误现象

  • 在 Monorepo 根或任一包里运行 eslint,报错显示发现多个候选 TSConfigRootDir
  • 这是因为解析器试图自动探测 tsconfig 根目录,但同时看到了多个包的 tsconfig,于是拒绝继续。

原因分析

  • TypeScript-ESLint 的解析器需要一个"根目录"(tsconfigRootDir)来解释你提供的 parserOptions.project(即哪些 tsconfig*.json 参与构建类型信息)。
  • 在 Monorepo 中,如果没有明确为每个包设定独立的 tsconfigRootDir 与对应的 project,解析器会在工作区内"看见"多个包的 tsconfig,从而无法确定到底应该用哪个根,最终报错。

快速修复(针对单个包)

packages/SearchChatUI 为例,给它的 eslint.config.js 增加明确的 parserOptions.tsconfigRootDirparserOptions.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" 的方式,让每个包都明确自己的 tsconfigRootDirproject

示例(伪代码,按需调整包路径):

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 或启用了需要类型信息的规则),一定要提供 tsconfigRootDirproject

  • 如果你不需要类型感知规则(为了更快的性能),可以只用非 type-checked 的推荐集,省略 project(但要权衡规则能力):

    js 复制代码
    extends: [tseslint.configs.recommended] // 非类型感知
    // 不设置 parserOptions.project
  • 在包内运行 npm run linteslint .)比在根随意运行更可控。

  • ESM 环境下,要用 fileURLToPath(import.meta.url) 获取当前文件路径来计算 __dirname


验证步骤

  1. 在目标包目录执行:
    • npm run lint
  2. 确认不再出现 "No tsconfigRootDir was set ... multiple candidate TSConfigRootDirs ..." 的错误。
  3. 如果还有包报同样错误,逐个为它们的配置添加 tsconfigRootDirproject

性能与类型感知

  • 类型感知规则需要构建 TypeScript Program,解析器会加载并分析 project 指定的 tsconfig;在大 Monorepo 中这可能较慢。
  • 推荐做法:
    • 只有在确实需要类型规则的包上开启 project
    • 使用按包 override 控制范围。
    • 结合 CI 分层执行(先非类型感知快速检查,再在关键包跑类型感知规则)。

小结

这个报错本质是 Monorepo 环境下 "类型规则需要明确上下文" 的提醒。只要为每个包设定清晰的 tsconfigRootDirproject,ESLint 就能准确地获取类型信息并稳定工作。按包划分 override 是根级统一配置的好方式;而在包内独立配置则更为直觉。


参考链接

相关推荐
冬男zdn3 小时前
优雅处理数组的几个实用方法
前端·javascript
Kaze_story4 小时前
Vue第四节:组件化、组件生命周期
前端·javascript·vue.js
yuzhiboyouye4 小时前
web前端开发自测清单
前端
妮妮分享4 小时前
H5获取定位的方式是什么?
java·前端·javascript
一只爱吃糖的小羊4 小时前
🕳️ React 避坑指南:"闭包陷阱"
react.js
weixin_439930644 小时前
前端js日期计算跨月导致的错误
开发语言·前端·javascript
零一科技4 小时前
瑞吉外卖项目,前端源码(用户端)解析
前端
用户93051065822244 小时前
module federation,monorepo分不清楚?
前端·架构
柳安4 小时前
手写new操作符执行过程
前端·javascript