首页
/ shadcn/ui TanStack Start 单仓模板:用 pnpm Workspace + Turborepo 搭建多包 UI 工程

shadcn/ui TanStack Start 单仓模板:用 pnpm Workspace + Turborepo 搭建多包 UI 工程

2026-09-04 19:04:42作者:丁柯新Fawn

shadcn/ui 官方仓库在 templates/ 下提供了一批开箱即用的项目骨架,其中 start-monorepo 模板将 TanStack Start 应用与共享 UI 包组织成一个 pnpm + Turborepo 驱动的 monorepo:应用代码位于 apps/web,组件库代码位于 packages/ui,两者通过 workspace:* 协议与路径别名联动。读完本文,你将掌握该模板的完整目录结构、添加/引用 shadcn/ui 组件的标准流程,以及 components.jsonexports 映射、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.4engines.node >= 20:模板锁定 pnpm 10 且要求 Node 20 及以上;
  • 五个顶层脚本 build / dev / lint / format / typecheck 全部委托给 Turborepo 调度(turbo buildturbo dev …);
  • pnpm.onlyBuiltDependencies 中额外声明了 esbuildlightningcss,与工作区配置呼应。

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: truecache: 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.jsonexports 字段:

"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 包",无需版本号、无需发布。

组件的两条引用路径:包名与路径别名

模板中组件其实有两条可达路径,理解它们的差异有助于排查导入问题:

  1. 包名路径(运行时/打包层)@workspace/ui/components/button 经由 exports 映射解析到 packages/ui/src/components/button.tsx。README 给出的标准用法即为此路径:

    import { Button } from "@workspace/ui/components/button";
    
  2. 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 配置文件,分工明确:

应用侧 apps/web/components.json

{
  "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.cssaliases 各项统一指向 @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 基础层与所有组件样式都通过这一条链路进入浏览器。

日常开发流程小结

在模板中完成一次"加组件 → 用组件"的完整流程如下:

  1. 在仓库根目录执行 pnpm install 安装工作区依赖;
  2. 执行 pnpm dlx shadcn@latest add button -c apps/web,组件落盘至 packages/ui/src/components/
  3. 在任意路由或组件中 import { Button } from "@workspace/ui/components/button"
  4. 运行 pnpm dev(等价于 turbo dev,由 apps/web/package.json 映射为 vite dev --port 3000)启动应用,样式经根路由自动加载。

此外,根级还有 lint / format / typecheck 任务,Turborepo 会按依赖图在所有 apps/*packages/* 成员上分别执行各自的 eslintprettier --write "**/*.{ts,tsx}"tsc --noEmit 脚本,无需逐包手工触发。

小结

start-monorepo 模板的价值在于把三件事一次做对:用 pnpm workspace 声明包边界、用 Turborepo 编排构建与常驻进程、用 components.jsonaliases.uiexports 映射把 shadcn 组件固化为工作区共享资产。理解了 README 中那两行命令背后 @workspace/ui/* 别名、:?url 样式导入与 @source 跨包扫描的完整链路,你就能在自有项目中复用同一套单仓组织方式,并针对自己的框架(Vite、Astro、React Router 等)调整对应的接入点。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384