Turborepo 非 Monorepo 实战:用 turbo 任务编排与缓存管理单个 Next.js 应用
【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo
导读
Turborepo 通常与「monorepo(多包仓库)」绑定出现,但它的任务编排与增量缓存能力同样适用于单个项目的代码仓库。本文以官方仓库中由核心团队维护的 non-monorepo 示例 为骨架,完整讲解如何用turbo管理一个独立的 Next.js 应用:包括create-turbo的脚手架方式、turbo.json中四个核心任务的配置语义、以及$TURBO_DEFAULT$、persistent等关键机制在源码层面的实现原理。读完本文,你将掌握在非 monorepo 场景下落地 Turborepo 任务缓存与长期运行任务管理的完整方案。
什么是 Turborepo non-monorepo starter
这个示例(目录为 examples/non-monorepo)演示了一个核心事实:Turborepo 并不强制要求多包结构。它使用 Turborepo 管理一个单一、非 monorepo 的项目——在这里是一个单独的 Next.js 应用程序。
在该目录的 meta.json 中,官方将它的定位描述为:
{ "name": "Non-monorepo", "description": "A standalone application using Turborepo", "maintainedByCoreTeam": true }maintainedByCoreTeam: true表明这是由 Turborepo 核心团队直接维护的示例,可作为生产实践的权威参照。
快速开始
与仓库中其他示例(basic、with-nestjs、with-svelte等)一致,non-monorepo 同样通过create-turbo脚手架模板创建,命令为:
npx create-turbo@latest -e non-monorepo-e(即--example)参数指定从该模板生成项目。创建完成后,目录中只包含一个单包应用,没有任何packages/工作区目录——这与 basic 示例 的 monorepo 结构形成了鲜明对照。
项目结构剖析
整个模板的文件布局非常精简:
non-monorepo/ ├── app/ │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── public/ # Next.js 静态资源 ├── eslint.config.mjs ├── next.config.ts ├── package.json ├── package-lock.json ├── postcss.config.mjs ├── tsconfig.json └── turbo.json根目录的 package.json 定义了应用自身的脚本:
{ "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "eslint", "check-types": "next typegen && tsc --noEmit" } }这里有个值得注意的细节:check-types由next typegen与tsc --noEmit两步组合而成,而不是简单的tsc --noEmit。同时 next.config.ts 中配置了typescript.ignoreBuildErrors: true与experimental.useTypeScriptCli: false,其注释解释了原因:该项目并行运行 TypeScript 7(tsc)与 TypeScript 6 API(typescript),为了让 typescript-eslint 等工具正常工作,Next.js 需要加载 TypeScript 6 API 而非 TypeScript 7 CLI 来执行自身检查。这提醒我们:当 turbo 任务对接的底层工具链发生变化时,任务脚本的拆分与配置需要联动调整。
Turborepo 在这里扮演的角色,就是将这些分散的 npm scripts 统一为turbo build、turbo lint、turbo check-types、turbo dev四个可缓存的、可编排的任务。
turbo.json 任务配置详解
non-monorepo 示例的核心配置位于 turbo.json:
{ "$schema": "https://turborepo.dev/schema.json", "ui": "tui", "tasks": { "build": { "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": [".next/**", "!.next/cache/**", "!.next/dev/**"] }, "lint": {}, "check-types": {}, "dev": { "cache": false, "persistent": true } } }下面逐项拆解。
build:定义缓存输入与输出
build任务通过inputs与outputs两个字段声明了 Turborepo 增量缓存的核心边界:
inputs:声明参与任务哈希计算的输入文件集合。["$TURBO_DEFAULT$", ".env*"]的含义是——对包目录下的所有文件($TURBO_DEFAULT$的展开语义见下文源码解析)进行哈希,并额外把根目录的.env*环境文件纳入哈希。这意味着修改.env、.env.local等文件同样会使build任务哈希失效、触发重新构建。outputs:声明任务产物,用于命中缓存后的恢复。这里输出是.next/**,同时用!前缀排除.next/cache/**与.next/dev/**——这两类文件是 Next.js 自身的缓存与开发产物,不应被 Turbo 缓存或恢复。
由于单包项目不存在包间依赖,build任务没有声明dependsOn: ["^build"]。对比 basic 示例的 turbo.json(monorepo 版)中build、lint、check-types都带有dependsOn: ["^build"]等拓扑依赖声明,可以清晰看出:非 monorepo 场景下任务依赖被大幅简化,turbo 的任务图退化为单节点执行,这正是「单项目也能用 turbo」的关键原因。
lint 与 check-types:零配置即可获得缓存
lint与check-types两个任务都是空对象{}。这并非占位符,而是 Turborepo 的零配置缓存约定:未声明outputs时,Turborepo 默认将dist/**、build/**(以及其他按约定推导的常见产物目录)作为输出进行缓存。因此即便不写任何配置,turbo lint与turbo check-types也能获得「输入未变化则直接回放结果」的缓存能力,只是由于这两个任务通常不产生持久化产物,缓存的收益更多体现在「跳过重复执行」上。
dev:长期运行任务的正确姿势
dev任务的两个字段在非 monorepo 场景中最容易被忽视,却恰恰是最重要的:
cache: false:开发服务器是交互式、有副作用的进程,产物不可复用,因此明确关闭缓存。persistent: true:声明这是一个**长期运行(persistent)**的任务——它不会自行退出,而是持续监听文件变化提供热更新服务。这个标记会触发 Turborepo 引擎的特殊处理(详见下文源码解析)。
四个核心命令的实战操作
按照官方 README 的说明,模板已预置好四个可直接使用的任务。
构建应用
npx turbo build触发 Next.js 生产构建(等价于next build),产物写入.next/。首次运行会构建并写入缓存,之后只要inputs声明范围内的文件(含.env*)没有变化,再次执行将从缓存直接恢复结果。
Lint 源码
npx turbo lint执行 ESLint 检查(等价于eslint)。本模板的 eslint.config.mjs 采用了 flat config 写法,组合了eslint-config-next/core-web-vitals与eslint-config-next/typescript,并通过withoutReactPlugin适配 ESLint 10(eslint-plugin-react 尚不支持时剔除其插件与react/前缀规则),同时用globalIgnores覆盖.next/**、out/**、build/**、next-env.d.ts等默认忽略项。
类型检查
npx turbo check-types执行next typegen && tsc --noEmit两步类型检查。next typegen生成路由等类型信息,随后tsc --noEmit做纯类型校验。
启动开发服务器
npx turbo dev启动 Next.js 开发服务器(等价于next dev)。由于配置了cache: false与persistent: true,该任务不会被打断、不会被缓存,并且会得到 Turborepo 针对长期运行任务的专门调度处理。
源码级的机制解析:从配置到引擎
以上配置项并非黑盒魔法,其语义在 Rust 实现的 Turborepo 引擎中有明确对应。
$TURBO_DEFAULT$:包目录的哈希边界
在 crates/turborepo-lib/src/run/builder.rs 的untracked_scan_prefixes注释中,Turborepo 明确了哈希扫描的范围规则:
对于每个参与任务,其哈希基于包目录进行文件哈希;任务未声明
inputs或声明了$TURBO_DEFAULT$时,会对包目录下的所有内容进行哈希。
也就是说,inputs: ["$TURBO_DEFAULT$", ".env*"]实际上是在「默认全量哈希」的基础上叠加了.env*的额外覆盖。同一段注释还指出:当哈希输入可能越过包目录边界(如根任务相对于仓库根目录哈希、globalDependencies覆盖全仓库、或$TURBO_ROOT$/..形式的 glob)时,Turbo 会拒绝做「仅包内」的范围优化。这解释了为何 non-monorepo 示例中的任务全部保持「包内哈希」的简洁形态——单项目仓库中,包目录即仓库根目录,哈希边界天然完整。
persistent 任务的引擎约束
persistent: true并不是一句装饰性声明。在 crates/turborepo-lib/src/engine/mod.rs 中,引擎在构建任务图时会校验:
- persistent 任务不能作为其他任务的依赖(报错
"{persistent_task}" is a persistent task, "{dependant}" cannot depend on it); - 当存在 persistent 任务时,会对并发任务数量提出约束(
You have {persistent_count} persistent tasks but turbo is configured for concurrency ...)。
从工程语义上理解:dev服务器需要与build、lint等一次性任务共存于任务图中,但前者永不退出,后者必须等待其完成——两者天然冲突。Turbo 通过persistent标记将长期运行任务隔离调度,避免任务图死锁。在 cli/args.rs 中也有对应提示,将persistent与with参数(等待指定任务完成的运行模式)关联说明。这也是官方 README 中强调npx turbo dev可放心使用的原因。
与 monorepo 示例的差异速览
| 维度 | non-monorepo(本文) | basic(monorepo 示例) |
|---|---|---|
| 包结构 | 单个 Next.js 应用 | 多个 apps + packages 工作区 |
dependsOn: ["^build"] | 无(无包间依赖) | 有(需要拓扑排序) |
| 哈希范围 | 单包目录(即仓库根目录) | 各包目录 + 根依赖折叠 |
| 使用场景 | 单项目也想获得任务缓存与统一命令 | 多包依赖编排与增量构建 |
两者最大的共同点是:turbo的缓存与任务编排能力与仓库形态解耦。即便未来项目规模增长、需要拆分为 monorepo,现有turbo.json中的build/lint/check-types/dev任务定义也能平滑迁移。
小结
通过 examples/non-monorepo 这份官方示例可以看到:Turborepo 的价值不限于大型 monorepo,它同样为单项目仓库提供了「统一任务入口 + 增量缓存 + 长期任务管理」的轻量方案。实践要点可归纳为:
- 用
npx create-turbo@latest -e non-monorepo一键生成模板; - 为
build声明inputs与outputs,让缓存边界精确可复用; lint/check-types留空即可获得默认缓存行为;- 开发类任务务必声明
cache: false+persistent: true,以适配引擎对长期运行任务的特殊调度; - 理解
$TURBO_DEFAULT$与 persistent 校验(对应源码 builder.rs 与 engine/mod.rs),才能在配置变更时做出正确判断。
【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考