拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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 中创建一个内部包遵循以下六步即可在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-TimeJIT——直接导出 TypeScriptJIT 包直接导出 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 ExportsCompiled 包为不同消费环境提供不同产物类型、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. 运行安装更新 lockfilepnpm install # 更新 lockfile把新依赖写入锁定文件lockfile 对 Turborepo 至关重要它用于解析包依赖图、保证构建可复现与缓存正确性。缺少 lockfile 会导致缓存行为不可预测详见仓库结构最佳实践的 Lockfile 一节。3. 导入并使用// apps/web/src/page.tsx import { Button } from repo/ui/button; export default function Page() { return ButtonClick 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-configTS 配置、eslint-configESLint 配置各自独立成包。配置类包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/未列入 outputsTurborepo 不会缓存该产物缓存命中率会大幅下降。参考 basic 示例的 turbo.json它声明了.next/**并排除.next/cache/**、.next/dev/**同时把lint、check-types、devcache: false、persistent: true都纳入任务图。TypeScript 最佳实践用 Node.js Subpath Imports别用pathsTypeScript 的compilerOptions.paths在 JIT 包中会失效打包器无法跨包解析这些映射。改用 Node.js subpath importsTypeScript 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-DefinitionCompiled 包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 ReferencesProject 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 --filterrepo/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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门