前端工程

pnpm + Turborepo + Changesets Monorepo 实战:缓存、版本发布与 CI

系统搭建工作区协议、任务图、确定性缓存、远程缓存信任边界、Changesets 版本意图、版本 PR、可信发布和 CI。

TY
Tycho
技术博主
• 2026-09-28 • 32 分钟阅读 • 4 次浏览
pnpm + Turborepo + Changesets Monorepo 实战:缓存、版本发布与 CI

一、目标架构与工具职责

本文构建一个 pnpm Workspace + Turborepo + Changesets 的 TypeScript Monorepo:pnpm 管依赖和工作区协议,Turborepo 管任务图与缓存,Changesets 管版本意图、Changelog 和发布。三者职责不能混淆。

apps/web -----+
apps/admin ---+--> packages/ui --> packages/tokens
services/api -+--> packages/config
       |
       v
Turborepo task graph -> local/remote cache -> CI
Changesets -> version PR -> build/pack -> registry publish

二、初始化与版本固定

当前 pnpm 文档以 pnpm-workspace.yaml 为工作区入口;Changesets 新版要求较新的 Node/pnpm。生产仓库通过 packageManager、Corepack 和 lockfile 固定版本。

mkdir platform && cd platform
corepack enable
pnpm init
pnpm add -Dw turbo typescript vitest eslint @changesets/cli
pnpm changeset init
{
  "name": "@acme/platform",
  "private": true,
  "packageManager": "pnpm@12.7.0",
  "engines": {"node": ">=24"},
  "scripts": {
    "build": "turbo run build",
    "test": "turbo run test",
    "lint": "turbo run lint",
    "typecheck": "turbo run typecheck"
  }
}

版本仅为示例,落地时应选择团队验证过的具体版本并提交 pnpm-lock.yaml。

三、定义 Workspace 和 Catalog

# pnpm-workspace.yaml
packages:
  - apps/*
  - services/*
  - packages/*

catalog:
  typescript: ^6.0.0
  vitest: ^4.0.0

sharedWorkspaceLockfile: true
disallowWorkspaceCycles: true

内部依赖使用 workspace:,确保本地缺包时安装直接失败,而不是意外从公共 registry 下载同名包。

{
  "name": "@acme/web",
  "dependencies": {
    "@acme/ui": "workspace:^"
  },
  "devDependencies": {
    "typescript": "catalog:"
  }
}
pnpm install --frozen-lockfile
pnpm list -r --depth 0
pnpm --filter @acme/web why @acme/ui

四、为包建立清晰边界

共享包必须有明确入口和 exports,禁止应用跨目录引用源码内部路径。每个包只声明实际使用的依赖。

{
  "name": "@acme/ui",
  "version": "0.1.0",
  "type": "module",
  "exports": {
    ".": {"types": "./dist/index.d.ts", "import": "./dist/index.js"},
    "./button": {"types": "./dist/button.d.ts", "import": "./dist/button.js"}
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsc -p tsconfig.build.json",
    "test": "vitest run",
    "typecheck": "tsc --noEmit"
  }
}
pnpm --filter @acme/ui build
pnpm --filter @acme/web... test
pnpm --filter ...@acme/web lint

省略号过滤语法需由团队文档统一,CI 中先用 --dry-run 确认影响范围。

五、配置 Turborepo 任务图

声明依赖和真实输出

dependsOn: ["^build"] 表示先构建依赖包。outputs 必须覆盖真实产物,否则缓存命中后目录不完整;无产物任务不要伪造输出。

{
  "$schema": "https://turborepo.com/schema.json",
  "ui": "tui",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["$TURBO_DEFAULT$", ".env.example"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "lint": {"outputs": []},
    "typecheck": {"dependsOn": ["^build"], "outputs": []},
    "dev": {"cache": false, "persistent": true}
  }
}
pnpm turbo run build --dry=json > .artifacts/task-graph.json
pnpm turbo run build --summarize
pnpm turbo run test --filter='...[origin/main]'

六、让缓存正确而不是只追求命中率

所有隐式输入都要进入哈希

缓存键必须包含所有影响输出的源码、配置、环境变量和依赖版本。缺少环境变量会产生“命中但结果错误”的危险缓存。

{
  "globalEnv": ["NODE_ENV"],
  "globalPassThroughEnv": ["CI"],
  "tasks": {
    "build": {
      "env": ["PUBLIC_API_BASE_URL"],
      "passThroughEnv": ["SENTRY_AUTH_TOKEN"]
    }
  }
}

Token 只透传,不应成为日志或产物。缓存恢复后运行轻量验收,例如确认 manifest、bundle 和 source map 策略。

rm -rf apps/web/dist
pnpm turbo run build --filter=@acme/web
test -f apps/web/dist/manifest.json
pnpm turbo run build --filter=@acme/web

第二次应显示 cache hit,且产物可运行。

七、远程缓存与信任边界

远程缓存能在 CI/开发者间共享产物,但缓存写权限等同于构建供应链权限。使用短期凭据、受保护分支写入、PR 只读或隔离命名空间,并验证签名能力。

pnpm turbo login
pnpm turbo link
TURBO_TOKEN="$TURBO_TOKEN" TURBO_TEAM="$TURBO_TEAM" \
  pnpm turbo run build

不要在 fork PR 暴露写 Token。发生依赖或构建器安全事件时,应具备按 scope 清理远程缓存和强制无缓存重建的流程。

八、Changesets 记录发布意图

每个影响公开包的变更在 PR 内创建 changeset,明确 major/minor/patch 和面向用户的描述。纯测试、内部重构若不影响发布可不创建,避免用空文件满足门禁。

pnpm changeset
pnpm changeset status
---
"@acme/ui": minor
"@acme/web": patch
---

为 Button 新增 loading 状态,并升级 Web 应用以使用新属性。

九、配置版本与内部依赖传播

{
  "$schema": "https://unpkg.com/@changesets/config/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@acme/web", "@acme/admin"]
}

公开包需显式 access: public 或包级 publishConfig.access。私有应用放入 ignore 前要确认它不会和发布包一起出现在同一个 changeset,避免发布失败。

十、CI:安装、影响分析和验证

name: verify
on: [pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: {fetch-depth: 0}
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo run lint typecheck test build --filter='...[origin/main]'
      - run: pnpm changeset status --since=origin/main

Actions 应固定到完整 commit SHA 以降低供应链风险;这里用标签便于阅读。CI 需要完整基准分支历史,否则 changed filter 会误判。

十一、版本 PR 与可信发布

先验证发布包再推送

推荐由机器人维护一个版本 PR:合并普通 changeset 后更新版本与 changelog;合并版本 PR 后执行 build、pack、内容检查和 publish。优先使用 registry trusted publishing/OIDC,避免长期 npm token。

pnpm changeset version
pnpm install --lockfile-only
pnpm -r build
pnpm -r test
pnpm -r pack --pack-destination .artifacts/packs
tar -tf .artifacts/packs/acme-ui-*.tgz | sort
pnpm changeset publish
git push --follow-tags

发布前检查 tarball 不含 .env、测试密钥、源码之外的内部文件;files 和 .npmignore 都要测试。

十二、循环依赖和幽灵依赖排障

pnpm 的严格依赖隔离能暴露未声明依赖。不要通过提升所有包到根依赖掩盖问题。

pnpm install --frozen-lockfile
pnpm -r exec node -p "require.resolve('typescript/package.json')"
pnpm list -r --depth Infinity > .artifacts/dependency-tree.txt
pnpm why some-package -r

工作区出现 cycle 警告时,提取共享类型到更低层包,或通过接口反转依赖;不要设置 ignoreWorkspaceCycles=true 后继续发布。

十三、缓存故障与可复现诊断

缓存异常时比较两次构建输入摘要、环境、lockfile 和产物哈希。先本地 --force 重建,再隔离远程缓存,避免直接删除全团队缓存。

pnpm turbo run build --force --summarize
find apps packages -path '*/dist/*' -type f -print0 | sort -z | xargs -0 sha256sum \
  > .artifacts/dist-sha256.txt
git status --short

相同提交、相同环境产生不同哈希时,检查时间戳、随机数、绝对路径、网络下载和未声明环境变量。

十四、验收与官方资料

验收:干净 clone 可 frozen install;工作区依赖使用 workspace:;循环依赖阻断;任务图顺序正确;删除产物后缓存可完整恢复;环境变化会导致正确 miss;PR 不泄露缓存写凭据;Changeset 能正确传播内部版本;pack 内容最小;发布有回滚和 deprecate 流程。

  • pnpm Workspace 与 workspace protocol 官方文档。
  • Turborepo Caching、Task Configuration 与 CI 官方文档。
  • Changesets Getting Started、Configuration、Versioning/Publishing 与 Automation 文档。

性能收益只有在构建仍然确定、缓存边界可信、发布物经过内容检查时才有价值。

TY

Tycho

热爱分享技术知识,帮助开发者成长。

评论 (0)

评论功能当前已关闭
暂无评论,快来抢沙发吧!