Turborepo 内部包(Internal Packages)创建与组织完整指南
【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo
导读
在 Turborepo monorepo 中,内部包(Internal Packages)是共享代码的基石:UI 组件、工具函数、类型定义、ESLint 与 TypeScript 配置都可以以"包"的形式被多个应用复用。本指南基于 Turborepo 官方最佳实践文档,结合仓库内的 basic 示例工程 与 结构规范文档,系统讲解内部包的创建流程、编译策略、exports 定义、安装使用方式、目录组织原则与常见坑点,读完即可在真实 monorepo 中落地一套规范、可缓存、易维护的内部包体系。
内部包创建清单(Package Creation Checklist)
在 Turborepo 中创建一个内部包,遵循以下六步即可:
- 在
packages/目录下创建包目录(如packages/ui/); - 添加
package.json,声明包名(name)与导出映射(exports); - 在
src/下添加源码; - 若使用 TypeScript,添加
tsconfig.json; - 在消费方(consuming package)的
package.json中安装该包为依赖; - 运行包管理器安装命令,更新 lockfile。
这个流程与仓库内 basic 示例工程 的实际结构完全一致:packages/ui/下包含package.json、tsconfig.json、src/(内含button.tsx、card.tsx、code.tsx),以及 lint 用的eslint.config.mjs。
注意:内部包必须被 workspace 识别。pnpm 通过根目录的 pnpm-workspace.yaml 声明
packages/*;npm/yarn 则在根package.json的workspaces字段中声明。详见仓库结构最佳实践。
包的编译策略:JIT 与 Compiled 二选一
内部包有两种主流编译策略,选择决定了包是否需要自建构建产物、是否能被 Turborepo 缓存。
策略一:Just-in-Time(JIT)——直接导出 TypeScript
JIT 包直接导出 TypeScript 源码,由消费方应用的打包器(bundler)完成编译。
// packages/ui/package.json { "name": "@repo/ui", "exports": { "./button": "./src/button.tsx", "./card": "./src/card.tsx" }, "scripts": { "lint": "eslint .", "check-types": "tsc --noEmit" } }适用场景:
- 消费方应用使用现代打包器(Turbopack、webpack、Vite);
- 希望配置最简、零构建步骤;
- 对缓存依赖不强,构建时长可接受。
局限性:
- 包本身没有构建产物,Turborepo 无法为该包建立构建缓存;
- 消费方必须支持 TypeScript 编译;
- 不能依赖 TypeScript 的
paths配置(应改用 Node.js subpath imports,详见下文)。
策略二:Compiled——包自行编译
Compiled 包自己负责编译,导出构建产物(通常输出到dist/)。
// packages/ui/package.json { "name": "@repo/ui", "exports": { "./button": { "types": "./src/button.tsx", "default": "./dist/button.js" } }, "scripts": { "build": "tsc", "dev": "tsc --watch" } }配套的tsconfig.json:
// packages/ui/tsconfig.json { "extends": "@repo/typescript-config/library.json", "compilerOptions": { "outDir": "dist", "rootDir": "src" }, "include": ["src"], "exclude": ["node_modules", "dist"] }适用场景:
- 希望 Turborepo 缓存构建任务(这是使用 Compiled 策略的最大动机);
- 包会被非打包器工具(如 Node.js 服务、测试框架)直接消费;
- 需要最大化兼容性。
重要提醒:记得在 turbo.json 的 outputs 中加上dist/**,否则构建产物不会被缓存!
定义 exports:入口点设计
exports字段决定了外部如何引入包的内容,是内部包 API 设计的核心。
多入口点(Multiple Entrypoints)
通过子路径(subpath)暴露多个模块:
{ "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 } }仓库中的实际示例采用了通配符写法:examples/basic/packages/ui/package.json 使用"./*": "./src/*.tsx",一行即可把所有src/*.tsx都暴露为@repo/ui/<name>子路径。注意通配符写法要求每个子路径都映射到.tsx文件,适合纯组件包;若包含多种文件类型或需要精确控制 API 面,逐条列出更稳妥。
条件导出(Conditional Exports,Compiled 包)
为不同消费环境提供不同产物(类型、ESM、CJS、兜底):
{ "exports": { "./button": { "types": "./src/button.tsx", "import": "./dist/button.mjs", "require": "./dist/button.cjs", "default": "./dist/button.js" } } }安装并使用内部包
1. 添加到消费方依赖
以apps/web消费@repo/ui为例:
// apps/web/package.json { "dependencies": { "@repo/ui": "workspace:*" // pnpm/bun 的写法 // "@repo/ui": "*" // npm/yarn 的写法 } }仓库 examples/basic/apps/web/package.json 即采用"@repo/ui": "workspace:*",同时将@repo/eslint-config、@repo/typescript-config作为 devDependencies 引入。
2. 运行安装更新 lockfile
pnpm install # 更新 lockfile,把新依赖写入锁定文件lockfile 对 Turborepo 至关重要:它用于解析包依赖图、保证构建可复现与缓存正确性。缺少 lockfile 会导致缓存行为不可预测(详见仓库结构最佳实践的 Lockfile 一节)。
3. 导入并使用
// apps/web/src/page.tsx import { Button } from '@repo/ui/button'; export default function Page() { return <Button>Click me</Button>; }一包一职责:目录组织原则
好的划分示例
packages/ ├── ui/ # 共享 UI 组件 ├── utils/ # 通用工具函数 ├── auth/ # 认证逻辑 ├── database/ # 数据库客户端/模型 ├── eslint-config/ # ESLint 配置 ├── typescript-config/ # TypeScript 配置 └── api-client/ # 生成的 API 客户端避免"巨无霸包"
// BAD: 一个包装下所有东西 packages/ └── shared/ ├── components/ ├── utils/ ├── hooks/ ├── types/ └── api/ // GOOD: 按职责拆分 packages/ ├── ui/ # 组件 ├── utils/ # 工具函数 ├── hooks/ # React hooks ├── types/ # 共享 TypeScript 类型 └── api-client/ # API 工具一包一职责的优势在 Turborepo 的任务编排中会被放大:每个包独立的 lint/check-types/build 任务可以并行执行、独立缓存、按--filter精确触发,而巨无霸包会让这些能力全部失效。仓库的 basic 示例 正是按此原则划分:ui(组件)、typescript-config(TS 配置)、eslint-config(ESLint 配置)各自独立成包。
配置类包(Config Packages)
配置类内部包是 monorepo 中复用构建与代码规范配置的标准做法。
TypeScript 配置包
// packages/typescript-config/package.json { "name": "@repo/typescript-config", "exports": { "./base.json": "./base.json", "./nextjs.json": "./nextjs.json", "./library.json": "./library.json" } }仓库 examples/basic/packages/typescript-config/ 提供了base.json、nextjs.json、react-library.json三个配置;其中react-library.json继承base.json并开启"jsx": "react-jsx"(见 react-library.json)。内部包的tsconfig.json通过extends复用:
// packages/ui/tsconfig.json { "extends": "@repo/typescript-config/react-library.json", "compilerOptions": { "outDir": "dist", "strictNullChecks": true }, "include": ["src"], "exclude": ["node_modules", "dist"] }(即 examples/basic/packages/ui/tsconfig.json 的实际内容。)
ESLint 配置包
// packages/eslint-config/package.json { "name": "@repo/eslint-config", "exports": { "./base": "./base.js", "./next": "./next.js" }, "dependencies": { "eslint": "^8.0.0", "eslint-config-next": "latest" } }仓库 examples/basic/packages/eslint-config/ 的版本更完整:"type": "module",导出./base、./next-js、./react-internal三个入口,并将eslint-plugin-turbo、eslint-config-prettier、@next/eslint-plugin-next等作为 devDependencies 固化在配置包内(见 package.json)。消费方(如apps/web)则通过 eslint.config.mjs 以 ESLint 9 flat config 的方式导入:
// apps/web/eslint.config.mjs import { nextJsConfig } from "@repo/eslint-config/next-js"; export default nextJsConfig;常见错误与规避
错误一:忘记定义 exports
// BAD: 没有 exports { "name": "@repo/ui" } // GOOD: 明确 exports { "name": "@repo/ui", "exports": { "./button": "./src/button.tsx" } }没有exports的内部包无法被稳定、精确地引入,也无法限制包的公共 API 面。
错误二:workspace 协议写错
// pnpm/bun { "@repo/ui": "workspace:*" } // 正确 // npm/yarn { "@repo/ui": "*" } // 正确 { "@repo/ui": "workspace:*" } // 在 npm/yarn 中是错的!workspace:*是 pnpm/bun 的协议,npm/yarn 应使用*(会通过 workspace 自动解析为本地包)。仓库中 pnpm 生态的示例全部使用workspace:*,见 apps/web/package.json 与 packages/ui/package.json。
错误三:turbo.json 的 outputs 遗漏 dist
// BAD: 包构建产物在 dist/,但 turbo.json 不知道 { "tasks": { "build": { "outputs": [".next/**"] // 缺少 dist/**! } } } // GOOD { "tasks": { "build": { "outputs": [".next/**", "dist/**"] } } }outputs 声明的是任务的可缓存产物。若 Compiled 包的dist/未列入 outputs,Turborepo 不会缓存该产物,缓存命中率会大幅下降。参考 basic 示例的 turbo.json:它声明了".next/**"(并排除.next/cache/**、.next/dev/**),同时把lint、check-types、dev(cache: false、persistent: true)都纳入任务图。
TypeScript 最佳实践
用 Node.js Subpath Imports,别用paths
TypeScript 的compilerOptions.paths在 JIT 包中会失效(打包器无法跨包解析这些映射)。改用 Node.js subpath imports(TypeScript 5.4+ 支持),在包内使用#前缀的别名。
JIT 包(指向源码,保留.ts扩展名):
// packages/ui/package.json { "imports": { "#*": "./src/*" } }// packages/ui/button.tsx import { MY_STRING } from "#utils.ts"; // 使用 .ts 扩展名Compiled 包(指向产物,使用.js扩展名):
// packages/ui/package.json { "imports": { "#*": "./dist/*" } }// packages/ui/button.tsx import { MY_STRING } from "#utils.js"; // 使用 .js 扩展名内部包用tsc,别用打包器
内部包优先用tsc而非打包器(如 esbuild/rollup)。打包器可能在产物到达应用打包器前就进行了代码转换(mangle),造成难以排查的问题;tsc只做类型擦除与转译,行为更可预期。
启用 Go-to-Definition(Compiled 包)
Compiled 包要支持 IDE 跳转定义,需开启声明映射:
// tsconfig.json { "compilerOptions": { "declaration": true, "declarationMap": true } }这会生成.d.ts与.d.ts.map文件,让编辑器能从dist/*.d.ts反向定位到src/*.tsx源码。
不需要根级 tsconfig.json
每个包应拥有自己的tsconfig.json。根级 tsconfig 一旦变更,会导致所有任务缓存失效(它是全局输入之一)。根 tsconfig 仅留给那些不属于任何包、需要在根目录运行的脚本使用。仓库内 basic 示例也遵循该原则:packages/ui/tsconfig.json、apps/web/tsconfig.json各自独立。
避免 TypeScript Project References
Project References 会引入额外的复杂度与另一层缓存机制。Turborepo 本身就负责管理包间依赖与构建顺序(通过dependsOn: ["^build"]),无需再用 Project References 重复编排。
与 Turborepo 任务缓存的联动
最后把视角拉回整个 monorepo 的运行机制:内部包的编译策略直接决定任务图(task graph)的形态。
- JIT 包没有 build 任务,其源码会被应用的打包器消费,因此它不产生独立缓存,但应用构建时对它的任何改动都会纳入应用任务的 inputs,从而正确失效应用缓存;
- Compiled 包有 build 任务,在 turbo.json 中以
"build": { "dependsOn": ["^build"] }声明依赖关系后,Turborepo 会先构建依赖包、缓存dist/产物,并在上游包未变化时直接命中缓存,跳过重建; - 配合
turbo run test --filter=@repo/ui之类的过滤命令,可以对单个内部包独立执行 lint、类型检查与测试(各包脚本见 packages/ui/package.json 的lint、check-types等 scripts)。
遵循本指南的包结构、exports 规范与编译策略,配合正确的 turbo.json outputs 配置,内部包即可成为 monorepo 中既解耦、又可缓存、可独立验证的高质量共享单元。
【免费下载链接】turboBuild system optimized for JavaScript and TypeScript, written in Rust项目地址: https://gitcode.com/gh_mirrors/tu/turbo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考