行业资讯
pnpm 负责依赖,Turbo 负责任务:一次 Monorepo 治理的边界取舍
本文为作者原创首发于掘金现同步发布到 CSDN。内容整理自AI Mind项目的真实开发过程。GitHubhttps://github.com/HWYD/ai-mind对应代码版本v0.4.8-v0.4.9线上体验https://ai.hwyblog.cloud/instant-mindAI Mind 是一个持续迭代中的 Next.js AI Chat 项目。从最基础的本地聊天开始逐步加入流式协议、工具调用、MCP、Skill 和 Agent 能力。如果你对这个项目感兴趣或者这篇文章对你有一点帮助也欢迎顺手到 GitHub 帮 AI Mind 点个 Star⭐这会是对我继续更新很大的鼓励。设计 Monorepo 时最先要想清楚的往往不是“用哪个工具”而是“哪些东西应该放在一起、边界怎么划、验证入口由谁负责”。但在 AI Mind v0.4.8 这一版里我真正想解决的不是“接入 pnpm 和 Turborepo”而是一个更基础的问题当项目已经有多个 app 和 package 时依赖应该由谁负责任务应该由谁负责哪些步骤绝对不能被普通 cache 伪装成成功。AI Mind 当前不是一个大型组织级 Monorepo。它仍然是一个单产品项目只是已经长出了 Webapp主聊天应用、Project Assistant Service项目助手服务、database package数据库访问与 Prisma 基础设施和 stream-core package流式协议核心库。这个规模有点尴尬继续手写根脚本会越来越乱直接引入大型治理体系又明显过重。所以这篇文章不会讲“如何从零配置 pnpm Turbo”。我更想复盘的是在一个还不算大的真实项目里怎样把 dependency、task、cache 和数据库副作用分到正确的位置。这一版的核心判断是pnpm 负责 dependency依赖事实Turbo 负责 task execution任务执行副作用步骤保持显式不塞进普通 task graph 的 cache。Monorepo 的问题不是命令不够统一在 v0.4.8 之前AI Mind 已经不是单目录项目了。这里的 Monorepo 可以先简单理解为一个仓库里放多个相互协作的 workspace。workspace 就是仓库里的一个独立工作单元可以是 app也可以是内部 package。它不一定要独立发布也可能只是为了让职责边界更清楚、验证更稳定。AI Mind 这一阶段主要有四个 workspaceWebapp 负责主要聊天体验和 AI RuntimeProject Assistant Service 负责独立的辅助服务packages/database数据库 package承载 Prisma schema、Prisma Client 生成和数据库相关命令负责数据层基础设施packages/stream-core流式协议 package承载结构化 stream protocol 和前后端共享的流式数据类型负责跨端流式协议。这些模块放在一个仓库里是合理的因为它们围绕同一个产品演进也需要共享协议、类型和数据库契约。但只要项目开始变成多个 workspace新的问题就会出现根目录命令和 package-level commands包级命令容易分裂。CI、Docker、本地开发可能维护三套执行顺序。共享 package 改动后依赖它的 app 是否一定重新验证不应该靠人记。Prisma generation、migration、checkpoint setup 这类 side-effectful 步骤不能被普通 build cache 吞掉。内部依赖如果写错不能静默回退到 registry 或变成隐形耦合。所以这一版不是为了把命令写得更“高级”而是为了建立一个能解释、能验证、能继续演进的工程基线每个 workspace 依赖谁、先验证谁、哪些结果可以进入 cache、哪些状态必须重新准备都要有明确答案。pnpm 和 Turbo 不解决同一个问题这次治理里最重要的取舍是不把 pnpm 和 Turbo 混成一句“Monorepo 工具”。如果只用一句话区分pnpm 管 package / dependencyTurbo 管 command / task order。pnpm 负责的是依赖层事实哪些目录是 workspace内部依赖是否通过workspace:解析依赖版本是否来自统一 Catalogdependency build scripts依赖安装脚本是否经过显式允许lockfile 是否可复现。Turbo 负责的是任务层事实哪个 package 的 build 应该先跑哪些任务可以并行哪些任务会产出可复用的 outputs哪些任务可以 cache哪些任务是 long-running 或 side-effectful不能 cache。这两个问题如果混在一起后面会很难讲清楚。比如ai-mind/stream-core被 Webapp 依赖这是 pnpm 要维护的依赖事实当 stream-core 改了Webapp 的 typecheck / build 应该在它之后执行这是 Turbo 要维护的任务事实。这也是为什么我没有用一堆根目录脚本去手写顺序。脚本能跑但脚本本身不会告诉我们“这个顺序为什么正确”。task graph 的价值是让顺序来自依赖关系而不是来自某个人刚好记得要先跑哪个命令。先把安装变成可复现的事实v0.4.8 第一层治理是 pnpm 基线。原因很直接如果安装结果本身不稳定后面的 lint、test、build 再漂亮也没有意义。根目录、CI 和 Docker 都统一到 Node.js 22 和pnpm10.34.0并继续使用同一份pnpm-lock.yaml做 frozen install冻结安装。冻结安装的意思是安装时只接受 lockfile 里已经记录好的 dependency resolution不允许顺手重新计算一份新依赖。pnpm-workspace.yamlworkspace 发现、Catalog 和安装策略配置里保留了明确的 workspace 范围packages:-packages/*-apps/*这段配置看起来很普通但它解决的是“哪些目录属于这个 Monorepo”的事实来源问题。后续所有 workspace package 都只能在这个范围内被发现和治理。换句话说项目先声明“我管哪些目录”再谈这些目录之间怎么依赖、怎么构建。接着是 Catalog。v0.4.8 没有把所有依赖都集中起来而是只集中真正跨 workspace 共享、且兼容性明确的依赖catalog:types/node:22.20.1modelcontextprotocol/sdk:^1.29.0dotenv:17.2.3typescript:5.9.3vitest:^4.1.4zod:^4.3.6这里的取舍很重要。Catalog 可以理解为“全仓库共享版本表”但它不是“看起来整齐”的工具。如果一个依赖只属于 Webapp比如 Next.js、React、UI/editor 相关库就没有必要为了形式统一把它提升成全仓库策略。否则 Catalog 会从治理工具变成另一个隐式耦合点。内部依赖则使用workspace:*。它的意义是如果我声明依赖的是本地 workspace就必须解析到本地 workspace不能在本地包缺失时静默去 registry 找一个同名包。这类错误一旦混进 CI 或 Docker会非常难查。因为命令看起来成功了但运行的可能不是我们以为的内部包。dependency build scripts 也要显式治理v0.4.8 还处理了一个容易被忽略的问题dependency build scripts也就是依赖安装阶段自动执行的脚本。有些 npm 依赖在安装时会自动执行脚本用来下载二进制文件、生成本地构建产物或者做一些安装前检查。pnpm 10 对这类脚本有更明确的治理能力。AI Mind 没有采用“全量允许”也没有保留占位配置而是把当前发现到的 dependency build scripts 逐项标成允许或拒绝。比如 Prisma engines、esbuild、sharp、unrs-resolver 这类当前构建或运行确实需要的脚本被允许一些和当前链路无关或不希望安装阶段自动执行的脚本保持拒绝。这件事的价值不在配置本身而在默认策略变成了 fail closed默认拒绝以后如果新依赖带来了新的 install script它需要被看见、被解释、再被允许而不是悄悄进入安装链路。对一个 AI Native 项目来说这种边界尤其重要。后续项目会继续引入模型、工具、MCP、Agent、数据库能力依赖面只会变大。如果安装阶段没有明确策略供应链行为会变成一块很难审计的暗区。任务图不是把脚本搬到根目录pnpm 基线解决的是“依赖是否可信”。接下来才轮到 Turbo 解决“任务怎么跑”。这里容易踩的坑是把每个 package 里原来的 scripts 收集到根目录并不等于有了 task graph。真正的 task graph 要回答的是哪些任务依赖上游 workspace哪些任务可以并行哪些任务失败时应该定位到哪个 workspace。v0.4.8 新增turbo.jsontask graph、cache 和 outputs 配置把根目录的lint、typecheck、test、build接到同一套 task graph 上。这样日常开发和 CI 不再各自维护执行顺序。核心配置大致是这样的{tasks:{build:{dependsOn:[^build],outputs:[.next/**,!.next/cache/**,dist/**,build/**]},typecheck:{dependsOn:[^build,^typecheck],outputs:[]},lint:{outputs:[]}}}这里的^build表示当前 workspace 的 build 依赖上游 workspace 的 build。比如 Webapp 依赖ai-mind/stream-core那么 Webapp build 之前stream-core build 应该先完成。这个顺序不再写死在某个脚本里而是从 workspace dependency graph 推出来。根目录命令因此变成日常入口{scripts:{lint:turbo run lint,typecheck:turbo run typecheck,build:pnpm validate:workspace-boundaries turbo run build}}package-level scripts 仍然保留但它们的身份变了它们是诊断入口不是第二套 canonical orchestration。也就是说正常情况下从根目录跑失败后再用pnpm --filter或pnpm --dir缩小范围。这样既保留了调试灵活性又不会让项目长期维护两套“标准流程”。cache 只属于可证明可复用的任务Turbo 的 cache 能力很有用但它也容易被用过头。在 AI Mind 里有些任务天然适合进入 cache比如packages/stream-core的协议测试、Project Assistant Service 的稳定测试、纯类型检查和普通构建。它们的结果主要由源码、配置和 lockfile 决定。只要这些 inputs 没变复用上一次结果是合理的。但另一些任务不能这么处理。packages/database的 build 实际上会触发 Prisma Client 生成。数据库 migration、runtime checkpoint setup、UserMemory schema setup 这些步骤会改变外部状态。Webapp 的部分测试也会依赖数据库或可选外部服务。这些任务如果被普通 cache 恢复就会出现一个危险情况命令显示通过但它没有真的验证当前状态。对读者来说这比失败更糟因为失败至少会暴露问题而“假成功”会把问题推迟到更远的地方。所以 v0.4.8 里对数据库 build 做了明确处理{ai-mind/database#build:{cache:false,outputs:[]}}这段配置表达的是一个边界Prisma generation 可以作为构建前置被编排但它不是一个应该从通用 cache 里恢复的普通产物任务。它和普通 TypeScript build 不一样不能只用“源码没变”来判断是否安全复用。这也是整篇文章最想强调的取舍之一。Monorepo 治理不是把所有东西都塞进一个看起来漂亮的 task graph。恰恰相反治理的价值在于知道哪些东西不应该进 cache哪些状态变化必须显式发生哪些失败必须暴露在正确的位置。数据库 setup 保持显式而不是追求“全自动”AI Mind 的数据库链路包括 Prisma migration、Prisma Client generation以及 LangGraph checkpoint / chat memory / UserMemory 相关 runtime schema setup。这些步骤和普通 lint、typecheck、unit test 不一样。它们不是只读源码就能决定结果的任务而是会依赖数据库连接、迁移状态和运行时表结构。因此 v0.4.8 没有把它们隐藏在普通根命令里也没有把生产部署路径改造成新的工具链。数据库 setup 继续通过显式命令表达{db:setup:deploy:pnpm --filter ai-mind/database db:migrate:deploy pnpm --dir apps/webapp db:runtime-checkpoints:setup}这段命令的重点不是写法而是位置。它属于状态初始化不属于可复用 task cache。它应该被清楚地放在部署或集成验证的显式步骤里而不是让 Turbo 因为某个 cache hit 跳过它也不是让本地开发者误以为普通 test 已经覆盖了数据库状态。这个取舍让系统少了一点“全自动包装感”但多了很多可解释性。当数据库失败时我们能知道失败发生在 migration、Prisma generation、checkpoint setup还是后续 integration test。对于工程项目来说这比“一个 root test 神秘失败”要有价值得多。可解释的失败是工程治理的一部分。CI 和 Docker 也要服从同一套边界v0.4.8 另一个目标是减少本地、CI、Docker 三套流程的漂移。如果本地跑pnpm build是一套顺序CI 又手写一套顺序Dockerfile 里再维护第三套顺序时间一长一定会分叉。某个 package 新增依赖、某个 build 前置变化很可能只改了其中一处。所以 v0.4.8 让 CI 的普通 lint、typecheck、test、build 进入同一套 Turbo graphDocker builder 也使用根pnpm build不再手写每个 workspace 的 build 顺序。但这里仍然保留前面说的边界数据库 migration、checkpoint setup 这种 side-effectful 步骤仍然是显式有序步骤不进入可复用 cache。可以把这个设计理解成两层依赖与任务层 pnpm install - boundary validation - turbo lint/typecheck/test/build 状态初始化层 Prisma generation / migration / runtime setup - integration validation / deployment path第一层追求可复现、可并行、可以进入 cache。第二层追求显式、可定位、不可伪装。这两层不混在一起整个项目的验证语义才比较干净。基线建立之后还要继续收紧验证可信度v0.4.8 建立的是 Monorepo 基线依赖安装可复现任务执行有 task graphcache 边界开始变清楚。但基线搭好以后新的问题就会出现task graph 能保证“按顺序跑”但不能自动保证“workspace 边界没有被绕过”测试命令能统一执行但不能自动说明“哪些测试是稳定的哪些依赖数据库哪些依赖真实外部服务”。换句话说v0.4.8 解决的是“怎么统一跑”v0.4.9 继续追问的是“统一跑出来的结果能不能信”。所以 v0.4.9 沿着同一条线继续收紧。第一所有 workspace 身份统一成ai-mind/*namespace。根治理单元叫ai-mind/workspaceWebapp、Project Assistant Service、database、stream-core 都有唯一、私有、无歧义的身份。第二workspace boundary validator边界验证脚本从简单依赖检查升级为更完整的仓库契约。它会检查重复身份、非法依赖方向、循环依赖、未声明内部依赖、跨 workspace 相对路径 import以及没有经过 packageexports暴露的深层实现 import。也就是说测试代码和生产代码一样不能绕过另一个 workspace 的公开入口去读私有实现。第三测试被拆成 stable、integration、external 三条 lanestable不依赖数据库和真实外部服务可以进入 cache integration依赖 PostgreSQL / Prisma / runtime schema不进入 cache external依赖真实云服务或模型必须手动 opt-in不进入普通 CI这个拆分让 v0.4.8 的 cache 边界继续变得更严格不是所有 test 都叫 test。一个测试能不能进入 cache能不能进 PR CI失败时应该归到哪个问题必须由 lane 明确表达。CI 也因此拆成两个 jobstable-validation不启动 PostgreSQLstateful-integration依赖 stable 成功后才启动 PostgreSQL 并执行 integration lane。这不是为了让 CI 配置更复杂而是为了保证一个关键事实如果静态检查或 stable test 已经失败就不应该提前创建数据库状态更不应该让状态初始化日志混进无状态失败里。CI 的结构本身也是在表达工程边界。为什么没有引入更大的 Monorepo 体系这两版里我刻意没有引入 Nx、remote cache、affected-only execution、Changesets、npm publishing 或大规模 package extraction。原因很简单AI Mind 当前还没有到那个阶段。它是一个持续演进的 AI Native Runtime Skeleton但仍然是单产品仓库。当前最容易出问题的不是“包太多导致调度效率低”而是这些更基础的问题依赖边界不够明确根命令和 package 命令容易分裂CI、Docker、本地验证可能漂移数据库和外部服务测试可能被普通 cache 或普通 test 语义误导后续 Skill / MCP / Agent / 数据层继续增长时基础工程契约不够硬。所以 v0.4.8-v0.4.9 的取舍是小步治理。先用 pnpm 和 Turbo 建立事实边界再用 validator、test lane 和 CI job 边界继续收紧。等项目真的出现更多 package、独立发布需求、PR 验证耗时压力再单独评估 affected-only、remote cache 或发布自动化。工程治理最怕的不是慢一点而是过早引入一套项目还解释不了的复杂体系。工具一旦超过了项目当下的复杂度后续每次维护都会先向工具解释项目而不是用工具解释项目。当前结果与边界到 v0.4.8AI Mind 已经完成了 Monorepo pnpm / Turborepo 基线治理本地、CI、Docker 使用统一 Node.js / pnpm 基线frozen lockfile 成为依赖复现入口内部 package 依赖使用workspace:*Catalog 只集中共享且兼容的依赖dependency build scripts 使用显式 allow / deny 策略根lint、typecheck、test、build进入统一 Turbo task graphpackage-level scripts 保留为诊断入口数据库 setup 和生产部署契约保持显式不被普通 cache 隐藏。到 v0.4.9这条线继续变成更强的 repository contractworkspace 身份统一到ai-mind/*source import 只能通过声明依赖和 public exports测试分为 stable / integration / externalintegration 和 external 不进入 cacheexternal smoke 只手动触发CI 先无状态验证再状态集成验证。这些改变都没有修改 AI Mind 的聊天 Runtime、Tool、Skill、MCP、Agent、stream protocol、公开 API、数据库业务 schema 或 UI 行为。也就是说这两版真正做的是工程地基而不是产品能力扩展。这次治理给我的一个判断这次改造之后我对小型 Monorepo 的判断更明确了Monorepo 治理的第一步不是追求工具完整度而是把每个工具负责的边界讲清楚。pnpm 负责依赖Turbo 负责任务。Catalog 负责共享版本不负责制造统一感。cache 只属于可证明可复用的任务不属于数据库状态和外部服务结果。CI 不只是“跑命令”它还要表达哪些验证可以先跑哪些状态必须后置。对 AI Mind 来说这样的治理不会立刻带来一个新功能但它让后续继续加 Skill、MCP、Agent、持久化和更多 runtime 能力时仓库不会先从工程入口处散掉。这也是我觉得 v0.4.8-v0.4.9 值得单独写一篇的原因它不是一次工具迁移而是一次边界收口。项目地址 GitHubhttps://github.com/HWYD/ai-mind 线上体验https://ai.hwyblog.cloud/instant-mind如果这篇文章或者 AI Mind 项目对你有所帮助也欢迎给项目点个 Star⭐。你的支持会是我持续更新这个系列、继续整理项目实现过程和设计复盘的很大动力。
郑州网站建设
网页设计
企业官网