shadcn/ui React Router Monorepo 模板:组件包共享与 pnpm workspace + Turbo 工程实践
本文以 shadcn/ui 仓库中的 React Router monorepo 模板(templates/react-router-monorepo)为主体,讲清这套模板的核心工作流:如何在 web 应用中通过 shadcn CLI 添加组件、组件被放置到 packages/ui 共享包中的哪个位置、以及如何从应用侧导入使用。读完后你能掌握 pnpm workspace + Turborepo + React Router 7 的三层组件共享机制,理解 components.json 别名映射如何决定组件的落盘路径与导入路径,并可据此在真实的 monorepo 中复用同一套 UI 组件体系。
模板定位:把 shadcn/ui 组件集中到共享包
该模板是一个基于 React Router 的 monorepo 结构,web 应用通过共享包 @workspace/ui 使用 shadcn/ui 组件。模板由两个工作区包构成:
apps/web:React Router 7 应用("react-router": "7.15.1"),负责路由与页面;packages/ui:命名@workspace/ui的私有 UI 包,shadcn/ui 组件、cn工具函数与全局样式都落在这里。
workspace 的包成员由 pnpm-workspace.yaml 声明:
packages:
- "apps/*"
- "packages/*"
allowBuilds:
esbuild: true
msw: false
根目录的 package.json 要求 node >= 20、pnpm@10.33.4,并通过 turbo 统一驱动 build / dev / format / typecheck 四个脚本。turbo.json 中 build 任务声明 dependsOn: ["^build"](先构建上游依赖包)、dev 任务为 persistent: true 且 cache: false,这是典型的"包依赖拓扑 + 长驻开发服务器"配置。
添加组件:shadcn CLI 的跨目录安装
模板 README 给出的核心操作是:在 web 应用根目录执行:
pnpm dlx shadcn@latest add button -c apps/web
-c apps/web 指定了 shadcn CLI 的工作目录。组件并不会写进 web 应用自身,而是被放置到 packages/ui/src/components 目录。这一行为由 apps/web 下的 components.json 驱动:
{
"style": "radix-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "../../packages/ui/src/styles/globals.css",
"baseColor": "neutral",
"cssVariables": true
},
"iconLibrary": "remixicon",
"aliases": {
"components": "@/components",
"hooks": "@/hooks",
"lib": "@/lib",
"utils": "@workspace/ui/lib/utils",
"ui": "@workspace/ui/components"
},
"rtl": false,
"menuColor": "inverted",
"menuAccent": "subtle"
}
关键字段逐项说明:
rsc: false:声明不使用 React Server Components,与 React Router 的框架无关的lib组件模式一致(React Router 默认以框架层提供 SSR,而组件本身按客户端组件方式编写);tailwind.css指向../../packages/ui/src/styles/globals.css:CLI 注册/修改 Tailwind 变量时直接改写ui包的全局样式文件,而不是web应用内的文件;aliases.ui = "@workspace/ui/components":CLI 生成/查找组件时的逻辑别名被解析为 workspace 包导出路径,这正是组件最终落盘到packages/ui/src/components的原因;aliases.utils = "@workspace/ui/lib/utils":组件内部的cn等工具统一引用共享包,避免每个应用各持一份。
packages/ui 自身还保留了一份 components.json,其别名直接写成包导出路径("components": "@workspace/ui/components"),保证在 ui 包内直接执行 CLI 命令时行为一致。
使用组件:通过包导出路径导入
组件落盘后,在应用中按 README 所示方式导入:
import { Button } from "@workspace/ui/components/button";
模板自带的首页 home.tsx 正是这一用法的示例——它已经预装了 button 组件并直接渲染:
import { Button } from "@workspace/ui/components/button"
export default function Home() {
return (
<div className="flex min-h-svh p-6">
<div className="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
<div>
<h1 className="font-medium">Project ready!</h1>
<p>You may now add components and start building.</p>
<p>We've already added the button component for you.</p>
<Button className="mt-2">Button</Button>
</div>
</div>
</div>
)
}
这个导入路径能够成立,依赖 ui 包 package.json 中的 exports 字段定义的子路径映射:
"exports": {
"./globals.css": "./src/styles/globals.css",
"./lib/*": "./src/lib/*.ts",
"./components/*": "./src/components/*.tsx",
"./hooks/*": "./src/hooks/*.ts"
}
即 @workspace/ui/components/button 解析到 packages/ui/src/components/button.tsx,@workspace/ui/lib/utils 解析到 packages/ui/src/lib/utils.ts。ui 包是一个不做构建的纯源码包(没有 build 脚本,仅 format 与 typecheck),由消费端(web 的 Vite)直接编译,这简化了开发体验,也意味着 web 与 ui 必须共享一致的 React 版本(两者均为 ^19.2.6)。
类型与路径解析的三重对齐
同一个 @workspace/ui/* 前缀在三处做了对齐配置,缺一即会报错:
- Vite 运行时解析:vite.config.ts 开启
resolve.tsconfigPaths: true,让 Vite 遵循 tsconfig 的paths映射,插件为tailwindcss() + reactRouter(); - TypeScript 编译期解析:apps/web/tsconfig.json 中声明
"@workspace/ui/*": ["../../packages/ui/src/*"](另有"@/*": ["./app/*"]),且typecheck脚本为react-router typegen && tsc,会先生成.react-router/types下的路由类型; - workspace 依赖声明:
web的package.json中@workspace/ui: "workspace:*"通过 pnpm 的 workspace 协议建立真实依赖关系。
ui 包提供的 cn 工具函数实现于 utils.ts,是标准的 clsx + tailwind-merge 组合:
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
shadcn/ui 组件内部的类名合并全部经由这个 cn,这也是 aliases.utils 必须指向共享包的原因。
样式系统:全局 CSS 与跨包内容扫描
组件的样式依赖 Tailwind CSS 4。ui 包的 globals.css 头部集中声明了模板的技术栈组合与关键工程细节:
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/outfit";
@custom-variant dark (&:is(.dark *));
@source "../../../apps/**/*.{ts,tsx}";
@source "../../../components/**/*.{ts,tsx}";
@source "../**/*.{ts,tsx}";
这里有两点对 monorepo 用户很关键:
@source显式把apps/**纳入 Tailwind 的内容扫描范围。由于样式文件在packages/ui/src/styles/下,默认扫描范围不会覆盖apps/web,若不声明@source,应用中使用的工具类将被 purge 掉——这是组件包与应用分离时最容易踩的坑;- 暗色模式采用
@custom-variant dark的类名方案(.dark作用域),主题变量全部以 oklch 色值写入:root/.dark两套 CSS 变量(如--primary: oklch(0.648 0.2 131.684)),与components.json中cssVariables: true、baseColor: "neutral"的配置对应。
ui 包的依赖列表(radix-ui、class-variance-authority、clsx、tailwind-merge、@remixicon/react、tw-animate-css、zod 等)也说明了组件的运行期支撑:Radix 原语 + CVA 变体 + Remixicon 图标库,与 components.json 的 iconLibrary: "remixicon" 一致。
SSR 与运行方式
React Router 侧的默认配置在 react-router.config.ts 中为 ssr: true(如需 SPA 模式可改为 false)。web 包的脚本提供了完整生命周期:
pnpm dev # turbo 驱动,React Router 开发服务器(persistent)
pnpm build # turbo 按 ^build 拓扑先构建 ui 包,再构建 web
pnpm typecheck
小结
这套模板的核心骨架可以归纳为三步:在 apps/web 下执行 pnpm dlx shadcn@latest add <component> -c apps/web → 组件经 components.json 别名映射落入 packages/ui/src/components → 应用通过 @workspace/ui/components/* 包导出导入使用。围绕这一骨架,exports 子路径映射、tsconfig paths、Vite tsconfigPaths 与 globals.css 的 @source 声明共同保证了"组件包与应用分离"下类型、运行与样式的端到端一致。模板同时提供了同系列的 Vite monorepo 版本(templates/vite-monorepo)等对照参考,若你需要框架无关的组件共享方案可进一步查看。
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 StartedRust0623
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