news 2026/9/20 5:10:51

Turborepo 内部包(Internal Packages)创建与组织完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Turborepo 内部包(Internal Packages)创建与组织完整指南

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 中创建一个内部包,遵循以下六步即可:

  1. packages/目录下创建包目录(如packages/ui/);
  2. 添加package.json,声明包名(name)与导出映射(exports);
  3. src/下添加源码;
  4. 若使用 TypeScript,添加tsconfig.json
  5. 在消费方(consuming package)的package.json中安装该包为依赖;
  6. 运行包管理器安装命令,更新 lockfile。

这个流程与仓库内 basic 示例工程 的实际结构完全一致:packages/ui/下包含package.jsontsconfig.jsonsrc/(内含button.tsxcard.tsxcode.tsx),以及 lint 用的eslint.config.mjs

注意:内部包必须被 workspace 识别。pnpm 通过根目录的 pnpm-workspace.yaml 声明packages/*;npm/yarn 则在根package.jsonworkspaces字段中声明。详见仓库结构最佳实践。

包的编译策略: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.jsonnextjs.jsonreact-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-turboeslint-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/**),同时把lintcheck-typesdevcache: falsepersistent: 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.jsonapps/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 的lintcheck-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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 5:10:21

智能体测试实战指南:应对不确定性,构建分层质量保障体系

1. 智能体测试与传统软件测试的根本差异1.1 需求从“明确函数”变成了一段“自由对话”我最早接触智能体测试时&#xff0c;第一反应是拿之前做接口测试的经验往上套&#xff1a;构造输入、校验输出、断言通过就完事。结果第一个用例就把我难住了——同一个问题&#xff0c;让智…

作者头像 李华
网站建设 2026/9/20 5:07:37

LinkSwift 网盘直链解析指南:9 大网盘文件 5 分钟拿到真实下载地址

LinkSwift 网盘直链解析指南&#xff1a;9 大网盘文件 5 分钟拿到真实下载地址 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 &#xff0c;支持 百度网盘 / 阿里云盘 / 中国移动…

作者头像 李华
网站建设 2026/9/20 5:07:23

OpenResearch 实践指南:构建透明可复现的研究工作流

不知道你有没有过这种感觉——花三个月做完一个研究课题&#xff0c;回头想分享成果时&#xff0c;却发现自己连中间删掉的关键分支、当时为什么选这个样本、跳过某个方法的原因全都想不起来了。我之前经常这样。明明过程里踩了无数坑&#xff0c;最后交出去的报告却很"光…

作者头像 李华
网站建设 2026/9/20 5:06:22

系统测试用例评审检查表:从经验判断到量化把关

简介&#xff1a;系统测试用例评审检查表是一份面向测试人员、测试经理及软件研发团队的实用工具模板&#xff0c;用于规范测试用例评审流程&#xff0c;确保用例质量并提升系统测试覆盖率与缺陷发现能力。资源共1个PDF文件&#xff0c;大小仅39KB&#xff0c;轻量便携&#xf…

作者头像 李华
网站建设 2026/9/20 5:03:58

基于Python和Vue的游戏创意工坊与推广平台全栈开发实战

做这个“Python基于Vue的游戏创意工坊与推广平台”项目&#xff0c;是我去年底接的一个比较典型的全栈开发需求。简单说&#xff0c;它就是一个面向游戏玩家和独立游戏作者的社区站点&#xff1a;作者可以在平台上发布创意原型、模组、关卡设计甚至独立游戏DEMO&#xff0c;玩家…

作者头像 李华
网站建设 2026/9/20 5:01:06

手写C++与C#日志函数:从printf到线程安全的完整实现

在我接触过的项目里&#xff0c;写“日志函数”的水平&#xff0c;能直接看出一个开发者对工程的认真程度。我接手过一个老服务&#xff0c;代码里到处都是裸的 printf&#xff0c;没有时间、没有级别、没有出处&#xff0c;某个凌晨线上数据出问题&#xff0c;我打开日志文件一…

作者头像 李华