Turborepo Monorepo 最佳实践:从目录结构、包管理与依赖策略到缓存优化的完整指南

Turborepo Monorepo 最佳实践:从目录结构、包管理与依赖策略到缓存优化的完整指南 构建工具开发工具CLI【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址https://gitcode.com/gh_mirrors/tu/turbo点击查看免费下载导读本文是 TurborepoRust 编写的 JavaScript/TypeScript 构建系统monorepo 工程化的实践指南主体内容来源于仓库内 best-practices 参考文档 及其姊妹篇 structure.md、packages.md、dependencies.md。阅读完本文你将掌握如何组织apps/与packages/两类包的目录结构、如何为内部包选择合适的编译策略JIT 或预编译并最大化 Turborepo 缓存收益、如何通过exports字段与 workspace 协议正确管理依赖、以及如何避开常见反模式。仓库中的 examples/basic 是这些原则的完整落地范例可对照验证。仓库结构apps/与packages/的标准布局标准目录布局一个健康的 Turborepo monorepo 通常长这样my-monorepo/ ├── apps/ # Application packages可部署 │ ├── web/ │ ├── docs/ │ └── api/ ├── packages/ # Library packages共享代码 │ ├── ui/ │ ├── utils/ │ └── config-*/ # 共享配置eslint、typescript 等 ├── package.json # 根 package.json依赖极少 ├── turbo.json # Turborepo 配置 ├── pnpm-workspace.yaml # pnpm或在 package.json 中声明 workspaces └── pnpm-lock.yaml # Lockfile必需仓库内的 examples/basic 就是这个布局的官方示例根目录只有package.json、turbo.json、pnpm-workspace.yaml与pnpm-lock.yamlapps/下是两个可部署的 Next.js 应用docs与webpackages/下是ui、eslint-config、typescript-config三个库包。对照 pnpm-workspace.yaml 可以看到 workspace 声明packages: - apps/* - packages/*四条核心原则apps/只放可部署产物Next.js 站点、API、CLI——凡是会部署上线的东西。packages/只放库被应用或其他包消费的共享代码。每个包只做一件事单一职责避免万能包。禁止嵌套包不要把包放进包里面后面会详述嵌套 wildcard 的问题。从源码结构看turborepo-tests/integration 中大量集成测试同样围绕apps/packages/双目录约定展开这已成为生态中的事实标准。包的类型Application 与 LibraryApplication Packagesapps/可部署它们是包依赖图的端点。不被其他包安装应用不应成为其他包的依赖。不存放共享代码需要共享的代码必须抽到packages/。// apps/web/package.json { name: web, private: true, dependencies: { repo/ui: workspace:*, next: latest } }仓库中 apps/web/package.json 的实践完全一致web是private: true的应用包其dependencies只包含repo/uiworkspace 协议与next/react等运行时依赖而eslint、typescript等工具链放在devDependencies。Library Packagespackages/共享代码工具函数、组件、配置。命名空间化使用repo/或yourorg/前缀。明确导出定义包对外暴露的内容。// packages/ui/package.json { name: repo/ui, exports: { ./button: ./src/button.tsx, ./card: ./src/card.tsx } }仓库中的 packages/ui/package.json 更进一步用通配符导出全部组件{ name: repo/ui, version: 0.0.0, private: true, exports: { ./*: ./src/*.tsx }, scripts: { lint: eslint . --max-warnings 0, check-types: tsc --noEmit }, dependencies: { react: 19.2.8, react-dom: 19.2.8 } }注意其中private: true与version: 0.0.0——内部库包如果不发布可以保持版本为0.0.0并禁止发布。包编译策略JIT 与预编译的取舍Just-in-Time最简单直接把 TypeScript 源码导出由消费方应用的打包器去编译。{ name: repo/ui, exports: { ./button: ./src/button.tsx } }优点零构建配置、改动即时生效。缺点Turborepo 无法缓存该包的构建要求消费方打包器支持 TS 编译不能用 TypeScriptpaths需改用 Node.js 子路径导入见下文。适用场景消费方使用现代打包器Turbopack、webpack、Vite想最小化配置且构建耗时在可接受范围。Compiled库推荐包用tsc或打包器自行编译产物。{ name: repo/ui, exports: { ./button: { types: ./src/button.tsx, default: ./dist/button.js } }, scripts: { build: tsc } }优点产物可被 Turborepo 缓存、随处可用。缺点配置更多。记住要把dist/**加进turbo.json的outputs条件导出预编译场景{ exports: { ./button: { types: ./src/button.tsx, import: ./dist/button.mjs, require: ./dist/button.cjs, default: ./dist/button.js } } }从编译产物与测试目录看Turborepo 的缓存层正是以outputs中的产物路径为对象做哈希与回放见 turborepo-lib/src 下的运行缓存实现因此预编译包的outputs声明直接决定缓存命中率。依赖管理Install Where Used在使用的包里安装依赖# Good: 在需要的包里安装 pnpm add lodash --filterrepo/utils # Avoid: 全部装到根目录 pnpm add lodash -w # 仅限仓库级工具不同包管理器对应命令# npm npm install lodash --workspacerepo/utils # yarn yarn workspace repo/utils add lodash # bun cd packages/utils bun add lodash本地安装的四大收益清晰每个包的package.json精确列出自身所需依赖。灵活不同包可用不同版本如legacy-ui用 react 17ui用 react 18。更好的缓存把依赖装在根目录会改动 workspace 锁文件使所有缓存失效装在包内则只影响该包。支持裁剪turbo prune可为 Docker 镜像剔除未用依赖。根目录只该放什么只有仓库级工具{ devDependencies: { turbo: latest, husky: ^8.0.0, lint-staged: ^15.0.0 } }不应该放react、next、express、lodash、axios、zod 以及测试库除非全仓共用。仓库根 package.json 正是如此turbo在devDependencies脚本只做turbo run的委派。内部依赖使用 workspace 协议// pnpm/bun { repo/ui: workspace:* } // npm/yarn { repo/ui: * }注意 npm/yarn 使用workspace:*是错误的写法。Turborepo 正是依赖这些内部依赖关系来排序构建dependsOn: [^build]时先构建依赖包再构建消费方。多包安装# pnpm pnpm add jest --save-dev --filterweb --filterrepo/ui # npm npm install jest --save-dev --workspaceweb --workspacerepo/ui # yarn (v2) yarn workspaces foreach -R --from {web,repo/ui} add jest --dev版本同步策略Option 1工具化# syncpack npx syncpack list-mismatches npx syncpack fix-mismatches # manypkg npx manypkg/cli check npx manypkg/cli fix # sherifRust 实现非常快 npx sherifOption 2包管理器命令# pnpm - 全仓更新 pnpm up --recursive typescriptlatest # npm - 更新所有 workspace npm install typescriptlatest --workspacesOption 3pnpm Catalogspnpm 9.5# pnpm-workspace.yaml packages: - apps/* - packages/* catalog: react: ^18.2.0 typescript: ^5.3.0// 任意 package.json { dependencies: { react: catalog: // 使用 catalog 中的版本 } }Peer Dependencies库包期望由消费方提供依赖时// packages/ui/package.json { peerDependencies: { react: ^18.0.0, react-dom: ^18.0.0 }, devDependencies: { react: ^18.0.0, // 供开发/测试使用 react-dom: ^18.0.0 } }Exports 最佳实践用exports字段而非main{ exports: { .: ./src/index.ts, ./button: ./src/button.tsx, ./utils: ./src/utils.ts } }避免 Barrel 文件// BAD: packages/ui/src/index.ts export * from ./button; export * from ./card; export * from ./modal; // ... 即使只需要一个组件也会引入全部 // GOOD: 在 package.json 中直接导出 { exports: { ./button: ./src/button.tsx, ./card: ./src/card.tsx } }命名空间化包名// Good { name: repo/ui } { name: acme/utils } // Avoid与 npm registry 冲突 { name: ui } { name: utils }多入口与 JIT 包的子路径导入多入口{ exports: { .: ./src/index.ts, // repo/ui ./button: ./src/button.tsx, // repo/ui/button ./card: ./src/card.tsx, // repo/ui/card ./hooks: ./src/hooks/index.ts // repo/ui/hooks } }JIT 包内部互引时不要用 TypeScriptpathsJIT 场景会失效改用 Node.js 子路径导入TypeScript 5.4// JIT指向源码 { imports: { #*: ./src/* } }// packages/ui/button.tsx —— 注意用 .ts 扩展名 import { MY_STRING } from #utils.ts;// 预编译指向产物 { imports: { #*: ./dist/* } }// 预编译包中用 .js 扩展名 import { MY_STRING } from #utils.js;常见反模式跨包边界访问文件// BAD: 直接探进别的包 import { Button } from ../../packages/ui/src/button; // GOOD: 安装后按包名导入 import { Button } from repo/ui/button;共享代码放在 app 里// BAD apps/ web/ shared/ # 这应该是一个包 utils.ts // GOOD packages/ utils/ # 正确的共享包 src/utils.ts根依赖过多// BAD: 根目录堆应用依赖 { dependencies: { react: ^18, next: ^14, lodash: ^4 } } // GOOD: 根目录只放仓库工具 { devDependencies: { turbo: latest, husky: latest } }忘记在 turbo.json outputs 声明产物// 包构建输出到 dist/但 turbo.json 不知道 { tasks: { build: { outputs: [.next/**] // 少了 dist/** } } } // 正确 { tasks: { build: { outputs: [.next/**, dist/**] } } }根配置package.json 与 turbo.json 的职责根 package.json{ name: my-monorepo, private: true, packageManager: pnpm9.0.0, scripts: { build: turbo run build, dev: turbo run dev, lint: turbo run lint, test: turbo run test }, devDependencies: { turbo: latest } }关键点private: true—— 防止误发布。packageManager—— 强制统一包管理器版本。仓库根 package.json 即声明packageManager: pnpm11.25.0并配合engines.node约束。脚本只委派给turbo run—— 根脚本本身不含任何真实构建逻辑。devDependencies保持最小仅 turbo 与仓库工具。永远优先用 Package Tasks始终优先包级任务只有包级任务实在无法实现时才用 Root Tasks。// packages/web/package.json { scripts: { build: next build, lint: eslint ., test: vitest, typecheck: tsc --noEmit } }包级任务让 Turborepo 能够并行——web#lint与api#lint同时跑独立缓存—— 每个包的任务输出分别缓存精确过滤——turbo run test --filterweb只跑一个包。Root Tasks 只是兜底无法按包执行的场景且不可并行、不可过滤应尽量避免// 除非必要否则避免 —— 串行、不可并行、不可过滤 { scripts: { lint: eslint apps/web eslint apps/api eslint packages/ui } }根 turbo.json 任务配置{ $schema: https://v2-11-0.turborepo.dev/schema.json, tasks: { build: { dependsOn: [^build], outputs: [dist/**, .next/**, !.next/cache/**, !.next/dev/**] }, lint: {}, test: { dependsOn: [build] }, dev: { cache: false, persistent: true } } }配置要点解读dependsOn: [^build]依赖包先构建拓扑排序。outputs声明产物路径供缓存!前缀排除.next/cache、.next/dev等不应缓存的内容。dev任务cache: falsepersistent: true开发服务器不缓存、持续运行。仓库 examples/basic/turbo.json 的实践与之呼应并额外为build声明了inputs: [$TURBO_DEFAULT$, .env*]、为lint/check-types声明了dependsOn: [^lint]/dependsOn: [^check-types]。全局配置迁移futureFlags.globalConfiguration若启用futureFlags.globalConfiguration全局设置移到global键下{ $schema: https://v2-11-0.turborepo.dev/schema.json, futureFlags: { globalConfiguration: true }, global: { inputs: [tsconfig.json], env: [CI] }, tasks: { build: { dependsOn: [^build], outputs: [dist/**, .next/**, !.next/cache/**, !.next/dev/**] }, lint: {}, test: { dependsOn: [build] }, dev: { cache: false, persistent: true } } }Workspace 配置与目录组织pnpm推荐# pnpm-workspace.yaml packages: - apps/* - packages/*npm/yarn/bun/nub// package.json { workspaces: [apps/*, packages/*] }nub 会跟随仓库现有的锁文件使用 pnpm 格式锁文件pnpm-lock.yaml或 nub 的lock.yaml时若存在pnpm-workspace.yaml则读取它否则读取package.jsonworkspaces。aubeaube 使用aube-workspace.yaml与 pnpm 相同的packages:格式回退到pnpm-workspace.yaml或package.jsonworkspaces。Polyglot workspaces实验性futureFlags.experimentalCargoWorkspaces—— 将 Cargo workspace crates 视为 Turborepo 包。本仓库本身就是一个佐证根 Cargo.toml 管理着crates/下数十个 Rust crate。futureFlags.experimentalPythonWorkspaces—— 将 uv workspace 成员视为 Turborepo 包。分组组织包# pnpm-workspace.yaml packages: - apps/* - packages/* - packages/config/* # 配置类分组 - packages/features/* # 功能类分组packages/ ├── ui/ ├── utils/ ├── config/ │ ├── eslint/ │ ├── typescript/ │ └── tailwind/ └── features/ ├── auth/ └── payments/不要做的事# BAD: 嵌套通配符会导致行为歧义 packages: - packages/** # 不要这样做TypeScript 配置共享基础配置包packages/ └── typescript-config/ ├── package.json ├── base.json ├── nextjs.json └── library.json// packages/typescript-config/base.json { compilerOptions: { strict: true, esModuleInterop: true, skipLibCheck: true, moduleResolution: bundler, module: ESNext, target: ES2022 } }仓库 examples/basic/packages/typescript-config/base.json 是完整版还包含declaration、declarationMap、isolatedModules、noUncheckedIndexedAccess等严格选项。各包继承// packages/ui/tsconfig.json { extends: repo/typescript-config/library.json, compilerOptions: { outDir: dist, rootDir: src }, include: [src], exclude: [node_modules, dist] }不需要根 tsconfig.json工作区根目录通常不需要tsconfig.json每个包自己配置并继承共享配置包。根tsconfig.json一旦改动会导致所有任务缓存失效只在运行非包级脚本时才需要。内部包用tsc而非打包器内部包优先tsc。打包器可能先于应用打包器加工代码造成难以排查的问题。预编译包开启 declaration maps{ compilerOptions: { declaration: true, declarationMap: true } }生成.d.ts与.d.ts.map以支持 IDE 跳转到定义。仓库的 base.json 默认就开启了这两项。避免 TypeScript Project References项目引用会引入复杂性与额外缓存层Turborepo 已经能更好地处理依赖关系。ESLint 配置共享配置包packages/ └── eslint-config/ ├── package.json ├── base.js ├── next.js └── react-internal.js// packages/eslint-config/package.json { name: repo/eslint-config, type: module, exports: { ./base: ./base.js, ./next-js: ./next.js, ./react-internal: ./react-internal.js }, devDependencies: { eslint: ^9.39.1 } }仓库中的 packages/eslint-config 正是这种结构base.js、next.js、react-internal.js三个配置入口并被apps/web等通过workspace:*协议引用。在包中使用ESLint 9 flat config// apps/web/eslint.config.js import { nextJsConfig } from repo/eslint-config/next-js; export default nextJsConfig;Lockfile缓存正确性的基石必须有锁文件原因可复现的构建Turborepo 解析包依赖关系缓存正确性。没有锁文件会看到不可预期的行为。务必提交锁文件# 提交你的锁文件 git add pnpm-lock.yaml # 或 package-lock.json、yarn.lock从实现上看Turborepo 的缓存哈希基于任务的 inputs含锁文件锁文件变更会作为全局输入使相关缓存失效参见 examples/basic/turbo.json 中inputs对.env*的声明以及根 turbo.json 的全局inputs配置思路。常见问题排查Module not found确认依赖安装在正确的包里运行pnpm install/npm install更新锁文件检查包的exports是否已定义。版本冲突不同包使用不同版本是特性而非 bug。如需一致性使用工具syncpack、manypkg使用 pnpm catalogs创建 lint 规则。提升Hoisting问题部分工具期望依赖在特定位置用包管理器配置解决# .npmrc (pnpm) public-hoist-pattern[]*eslint* public-hoist-pattern[]*prettier*在真实仓库中验证这些原则本仓库自身就是一个大型实战考场可对照验证上述所有原则目录结构examples/basic 严格按照apps/web、docspackages/ui、eslint-config、typescript-config组织应用包apps/web/package.json 声明private: true以workspace:*引用repo/ui工具链全部在devDependencies库包导出packages/ui/package.json 用exports[./*]暴露组件源码JIT 策略并提供check-types、lint包级任务共享配置packages/typescript-config 提供base.json/nextjs.json/react-library.json各包通过extends继承根配置examples/basic/turbo.json 声明dependsOn、outputs、cache: false的dev任务根 package.json 只放 turbo/prettier/typescript 并声明packageManager。结合 turborepo-tests/integration 中的数百个集成测试可以看到这些结构约定在真实构建调度、缓存命中与依赖图解析场景中的行为是阅读本文后进一步深入 Turborepo 内部机制的入口。赞分享构建工具开发工具CLI【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址https://gitcode.com/gh_mirrors/tu/turbo点击查看免费下载相关推荐Langfuse 的 Turborepo Monorepo 最佳实践从目录结构、包策略到依赖管理Langfuse 的 Turborepo Monorepo 最佳实践从目录结构、包策略到依赖管理 导读 本指南以 Turborepo Monorepo 最佳实人工智能LLMOps可观测性AI 评测LLM 网关后端前端零基础上手Toto-2.0-22m5分钟完成安装与多变量时间序列预测实战零基础上手Toto 2.0 22m5分钟完成安装与多变量时间序列预测实战 Toto 2.0 22m是Datadog开发的多变量时间序列预测基础模型属于TotButtercup缓存策略构建结果与依赖缓存优化Buttercup缓存策略构建结果与依赖缓存优化 概述 Buttercup作为DARPA AIxCC挑战赛中的Cyber Reasoning System网创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考