shadcn/ui TanStack Start 单仓模板:用 pnpm Workspace + Turborepo 搭建多包 UI 工程
shadcn/ui 官方仓库在 templates/ 下提供了一批开箱即用的项目骨架,其中 start-monorepo 模板将 TanStack Start 应用与共享 UI 包组织成一个 pnpm + Turborepo 驱动的 monorepo:应用代码位于 apps/web,组件库代码位于 packages/ui,两者通过 workspace:* 协议与路径别名联动。读完本文,你将掌握该模板的完整目录结构、添加/引用 shadcn/ui 组件的标准流程,以及 components.json、exports 映射、Tailwind @source 等关键配置在单仓中如何协同工作。
模板定位与目录结构
start-monorepo 是一个面向 TanStack Start(Vite 生态的 React 全栈框架)的单仓模板,其核心设计目标只有一个:让 shadcn/ui 组件不再散落在应用包里,而是集中托管在独立的 packages/ui 包中,供工作区内多个应用复用。
从仓库实际文件看,模板采用经典的两级结构:
templates/start-monorepo/
├── apps/
│ └── web/ # TanStack Start 应用(Vite + React 19)
│ ├── src/routes/ # 路由文件(__root.tsx、index.tsx)
│ ├── components.json # 应用侧 shadcn 配置
│ ├── tsconfig.json # 定义 @workspace/ui/* 路径映射
│ └── vite.config.ts
├── packages/
│ └── ui/ # @workspace/ui 共享包
│ ├── src/
│ │ ├── components/ # shadcn 组件落盘目录
│ │ ├── hooks/
│ │ ├── lib/
│ │ └── styles/globals.css
│ ├── components.json # 包侧 shadcn 配置
│ └── package.json # 定义 exports 导出映射
├── package.json # 根脚本(turbo build/dev/lint/...)
├── pnpm-workspace.yaml
└── turbo.json
工作区声明与运行环境约束
工作区成员由 pnpm-workspace.yaml 声明:
packages:
- "apps/*"
- "packages/*"
allowBuilds:
esbuild: true
lightningcss: true
unrs-resolver: true
msw: false
apps/* 与 packages/* 两个 glob 决定了哪些目录被视为独立包。allowBuilds 则显式列出了允许执行安装后构建脚本的依赖(esbuild、lightningcss 等),这是较新 pnpm 版本收紧原生构建依赖时的白名单机制。
根 package.json 定义了运行前提与顶层命令:
packageManager: pnpm@10.33.4、engines.node >= 20:模板锁定 pnpm 10 且要求 Node 20 及以上;- 五个顶层脚本
build/dev/lint/format/typecheck全部委托给 Turborepo 调度(turbo build、turbo dev…); pnpm.onlyBuiltDependencies中额外声明了esbuild与lightningcss,与工作区配置呼应。
Turborepo 任务图
turbo.json 定义了跨包的任务编排规则,其中有几处值得注意:
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".output/**"]
},
"dev": {
"cache": false,
"persistent": true
}
build任务的dependsOn: ["^build"]表示构建web之前会先构建其上游依赖包(@workspace/ui),^是 Turborepo 的"依赖包优先"语法;dev任务标记为persistent: true且cache: false,因为 Vite dev server 是常驻进程,不适合缓存;outputs: [".output/**"]对应 TanStack Start 产物目录,命中缓存条件时可跳过重复构建。
添加组件:在应用目录下执行 shadcn CLI
模板 README 给出的核心操作是:在 web 应用根目录执行 shadcn CLI 的 add 命令,并用 -c 参数指定目标包:
pnpm dlx shadcn@latest add button -c apps/web
这条命令的实际效果是:CLI 读取 apps/web/components.json 中的配置(尤其是 aliases.ui 指向 @workspace/ui/components),将生成的组件文件落盘到 packages/ui/src/components 目录,而不是应用自己的 src 目录。这正是该模板"组件集中托管"设计的关键点——-c apps/web 指定的是执行上下文所在的应用包,而组件物理位置由别名配置重定向到共享 UI 包。
UI 包的导出契约:exports 映射
组件为什么能通过 @workspace/ui/components/button 这样的子路径被 import?答案在 packages/ui/package.json 的 exports 字段:
"exports": {
"./globals.css": "./src/styles/globals.css",
"./lib/*": "./src/lib/*.ts",
"./components/*": "./src/components/*.tsx",
"./hooks/*": "./src/hooks/*.ts"
}
四个子路径把 src 下的每类资产暴露为包级入口:CSS、工具函数、组件与 hooks 各自成体系。同时 web 应用通过依赖声明建立工作区关联:
"@workspace/ui": "workspace:*"
workspace:* 是 pnpm 的本地包引用协议,表示"始终解析到工作区内同名的 @workspace/ui 包",无需版本号、无需发布。
组件的两条引用路径:包名与路径别名
模板中组件其实有两条可达路径,理解它们的差异有助于排查导入问题:
-
包名路径(运行时/打包层):
@workspace/ui/components/button经由exports映射解析到packages/ui/src/components/button.tsx。README 给出的标准用法即为此路径:import { Button } from "@workspace/ui/components/button"; -
TS 路径别名(类型检查层):apps/web/tsconfig.json 中额外声明了:
"paths": { "@/*": ["./src/*"], "@workspace/ui/*": ["../../packages/ui/src/*"] }这让 TypeScript 直接把
@workspace/ui/*映射到src源码文件,配合 vite.config.ts 中的resolve: { tsconfigPaths: true },Vite 在开发期也能按同一套别名解析模块。两条路径最终指向同一份源码,保证类型提示与运行时行为一致。
两份 components.json 的职责分工
模板存在两份 shadcn 配置文件,分工明确:
{
"style": "radix-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"hooks": "@/hooks",
"lib": "@/lib",
"utils": "@workspace/ui/lib/utils",
"ui": "@workspace/ui/components"
}
}
包侧 packages/ui/components.json 结构相同,但 tailwind.css 指向包内相对路径 src/styles/globals.css,aliases 各项统一指向 @workspace/ui/*。
关键配置项解读:
| 配置项 | 取值 | 作用 |
|---|---|---|
style |
radix-nova |
指定组件的视觉/交互风格基线 |
rsc |
false |
明确这是非 RSC 环境(TanStack Start 客户端路由应用),CLI 不会生成 React Server Component 相关代码 |
tailwind.css |
包内 globals.css |
告诉 CLI 主题 CSS 变量的写入位置——注意应用侧写的是跨包相对路径,确保新组件的样式变量统一汇入 UI 包 |
tailwind.baseColor |
neutral |
生成 CSS 变量时采用的基础色板 |
iconLibrary |
lucide |
组件内图标统一使用 lucide 图标库 |
aliases.ui |
@workspace/ui/components |
决定 shadcn add 的落盘目标,这是整个模板组件集中托管的枢纽 |
Tailwind 跨包扫描:@source 指令
组件集中在 packages/ui、页面代码在 apps/web,Tailwind 如何同时识别两侧的工具类使用?答案在 packages/ui/src/styles/globals.css:
@import "tailwindcss";
@source "../../../apps/**/*.{ts,tsx}";
@source "../../../components/**/*.{ts,tsx}";
@source "../**/*.{ts,tsx}";
Tailwind CSS v4 默认只扫描样式文件所在包内的源码;这里通过三条 @source 指令显式把扫描范围扩展到工作区应用目录(apps/**)与 UI 包自身(../**),保证无论从哪个包写的 className 都能被编译进最终的样式产物。
样式如何进入应用:globals.css?url 引入
最后闭环的一环是 CSS 的分发。TanStack Start 的根路由 apps/web/src/routes/__root.tsx 做了这样的处理:
import appCss from "@workspace/ui/globals.css?url"
export const Route = createRootRoute({
head: () => ({
links: [
{
rel: "stylesheet",
href: appCss,
},
],
// ...
}),
})
:?url 后缀是 Vite 的资源导入约定:不把 CSS 内容内联进 JS,而是让 Vite 处理该样式文件并输出一张独立的样式资源 URL,再以 <link rel="stylesheet"> 的形式注入根文档的 <head>。这意味着:UI 包的单一 globals.css 就是全站唯一的样式入口,shadcn CLI 写入的主题变量、Tailwind 基础层与所有组件样式都通过这一条链路进入浏览器。
日常开发流程小结
在模板中完成一次"加组件 → 用组件"的完整流程如下:
- 在仓库根目录执行
pnpm install安装工作区依赖; - 执行
pnpm dlx shadcn@latest add button -c apps/web,组件落盘至packages/ui/src/components/; - 在任意路由或组件中
import { Button } from "@workspace/ui/components/button"; - 运行
pnpm dev(等价于turbo dev,由 apps/web/package.json 映射为vite dev --port 3000)启动应用,样式经根路由自动加载。
此外,根级还有 lint / format / typecheck 任务,Turborepo 会按依赖图在所有 apps/* 与 packages/* 成员上分别执行各自的 eslint、prettier --write "**/*.{ts,tsx}" 与 tsc --noEmit 脚本,无需逐包手工触发。
小结
start-monorepo 模板的价值在于把三件事一次做对:用 pnpm workspace 声明包边界、用 Turborepo 编排构建与常驻进程、用 components.json 的 aliases.ui 加 exports 映射把 shadcn 组件固化为工作区共享资产。理解了 README 中那两行命令背后 @workspace/ui/* 别名、:?url 样式导入与 @source 跨包扫描的完整链路,你就能在自有项目中复用同一套单仓组织方式,并针对自己的框架(Vite、Astro、React Router 等)调整对应的接入点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00