TypeScript和Turborepo构建Monorepo实战
一个仓库。多个应用。零混乱。这就是高级工程师真正使用的架构。
你有一个 Next.js 应用、一个通用组件库、一个 REST api,以及一个 design token 包——全部分别位于不同的仓库中。因此,每次对共享按钮组件的修改都意味着 4 个 PR、3 条崩溃的 CI 流水线,以及一个非常愤怒的团队。其实有更好的方式。
Monorepo 并不新鲜。十多年来,Google 和 Meta 都在单一仓库中运行其整个产品代码库。对我们其他人来说,这似乎是新的——其中大多数技术已经存在一段时间,但工具链终于跟上了。结合 Turborepo 中的 TypeScript project references 和 pnpm workspaces,你可以获得真正可用于生产级别的 monorepo:快速构建、共享包、增量缓存,以及不会让你想去种草的 DX。
这是我第一次搭建时希望拥有的一份简短指南。让我们从零开始构建。
| 指标 | 效果 |
|---|---|
| CI 时间下降 | 85%(使用 Turborepo 远程缓存后) |
| 依赖管理 | 单一版本,多应用共享 |
| 包管理 | pnpm 原生 monorepo 支持——workspace 协议 |
为什么选择 Turborepo 而不是 Nx 或 Lerna?
答案实际上取决于团队规模。
Nx 非常强大,但带有大量主观约束——它是一个完整的框架,而不仅仅是构建编排工具。Lerna 主要是一个包发布工具,是仓库中各个包之间的协调者。
Turborepo 处于中间位置:一个专注于任务运行和缓存的构建系统,同时在其他方面不过多干涉。
本地 + 远程缓存与增量构建。Turborepo 通过对所有输入(源文件、环境变量、依赖等)进行哈希计算来判断是否需要重建,只有当某个包使用的输入发生修改时才会重新构建。在已预热的 CI 缓存中,这非常高效。
基于依赖的任务编排。你声明哪个任务依赖哪个。turbo build 会在构建你的 Next.js 应用之前先构建 @repo/ui 包——在可能的情况下并行执行,自动完成,顺序正确。
零锁定。Turborepo 只是构建在包管理器 workspace 之上的一层薄封装。移除它,一切仍然可以正常运行——你的 package.json scripts 仍会执行,只是没有缓存。这是一个良好的约束。
起作用的文件夹结构
在编写任何配置之前,先确定结构。社区形成的一种约定——也是 Turborepo 官方 starter 所采用的方式——是将 apps(可部署的内容)与 packages(内部库)分离:
my-monorepo/
├── apps/
│ ├── web/ ← Next.js 15 前端
│ ├── docs/ ← Next.js 文档站点
│ └── api/ ← Node/Express 或 tRPC API
├── packages/
│ ├── ui/ ← 共享 react 组件
│ ├── tsconfig/ ← 基础 TypeScript 配置
│ ├── eslint-config/← 共享 ESLint 规则
│ └── utils/ ← 共享工具函数、校验器
├── turbo.json
├── pnpm-workspace.yaml
└── package.json重要规则:apps/ 中的内容不得被另一个 app 引用。如果两个应用都需要某段代码,那么它应该放在 packages/ 中。
连接配置:turbo.json + TypeScript References
下面是一个真实的 turbo.json,展示了具有正确依赖顺序的标准流水线——build、lint、test 和 type-check:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"lint": {},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}需要注意的关键是 ^build 语法:插入符号表示“首先在该任务所依赖的所有包中运行 build”。因此,当你在 Next.js 中运行 turbo build 时,由于我们使用的是 monorepo,当命令在 js 应用的根目录执行时,它会自动先构建 @repo/ui 和 @repo/utils。无需手动排序,也不会陷入循环依赖地狱。
现在,将其与可复用包中的 TypeScript project references 结合:
{
"extends": "@repo/tsconfig/react-library.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"declaration": true,
"declarationMap": true
},
"include": ["./src"],
"exclude": ["node_modules", "dist"]
}💡 Pro Tip
为每个公共包启用 declaration 和 declarationMap。这可以为消费应用提供准确的类型信息,并在无需运行完整构建的情况下,实现 VS Code 跨包跳转到定义。
正确共享组件库的方式
在 monorepo 中常见的错误是在第一天就过度设计共享 UI 包。先发布一个非常简单的起点,然后逐步演进。下面是一个带有实际 TypeScript 导出的共享组件:
import type { ButtonhtmlAttributes, ReactNode } from 'react';
type Variant = 'primary' | 'ghost' | 'destructive';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
isLoading?: boolean;
children: ReactNode;
}
export function Button({
variant = 'primary',
isLoading = false,
children,
className,
...rest
}: ButtonProps) {
return (
<button
>={variant}
disabled={isLoading || rest.disabled}
className={['btn', className].filter(Boolean).join(' ')}
{...rest}
>
{isLoading ? 'Loading…' : children}
</button>
);
}在你的 Next.js 应用中,它像任何 npm 包一样被消费——从 TypeScript 的角度来看,它就是一个包:
import { Button } from '@repo/ui';
export default function HomePage() {
return (
<main>
<Button variant="primary" onClick={() => console.log('clicked')}>
Get Started
</Button>
</main>
);
}Turborepo 支持渐进式采用。这意味着你不必重构整个仓库才能获得缓存收益——只需在一个流水线任务上启用,然后逐步扩展。
远程缓存:改变游戏规则(CI 流水线亦然)
最后一个组成部分——也是让团队真正关心的部分——是 CI 上的远程缓存。没有它,每次流水线运行都会从零开始重新编译所有内容。Turbo 的远程缓存(Vercel 或自托管)允许 CI 跳过自上次执行以来输入未发生变化的任务。
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build, Lint, Test (with remote cache)
run: pnpm turbo build lint test typecheck
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}两个环境变量。这就是远程缓存设置的全部内容。第一次运行会完整构建。之后,如果某个包未发生变化,则为缓存命中——不到一秒。
停止交付复杂性,开始交付功能
Monorepo 中最难的部分并不是工具本身——而是让团队信任它。当开发者第一次运行 pnpm turbo build,看到它因为没有变化而跳过 11 个包,看到整个过程在 4 秒而不是 4 分钟内完成——那一刻,他们就不会再问“我们为什么要这样做”。
从小开始。共享一个 tsconfig 包,一个 UI 组件包,一个 Turborepo 流水线。根据需要添加 packages。架构会随着你演进,而你的 git log 终于会成为一个由真正协作构建产品的人所书写的连贯故事。
原文地址:https://medium.com/@mernstackdevbykevin/monorepos-with-typescript-turborepo-setup-best-practices-aa94296a8dc3
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!