Use Cases (更新: 2026/6/2)

用 Claude Code 和 pnpm workspace 搭建实用 Monorepo

用 Claude Code 配合 pnpm workspace 设计小型 monorepo,覆盖依赖、filter、CI 和常见坑。

用 Claude Code 和 pnpm workspace 搭建实用 Monorepo

pnpm workspace 解决的是“不要到处复制代码”

我是 claudecode-lab.com 的 Masa。

一个产品刚开始通常只有一个应用,但很快会长出后台、公共 UI、配置、脚本、邮件任务和测试工具。真正的信号不是“文件多”,而是同一个按钮文案、API 路径、环境变量名、Zod schema 被复制到多个目录。复制一次很快,半年后改一次就很慢。

pnpm workspace 是把多个 package 放在同一个 Git 仓库里管理的方式。官方 pnpm Workspace 文档说明,workspace 需要在根目录放置 pnpm-workspace.yaml,用它把多个项目联合成一个工作区。对小团队来说,这比一开始就拆成很多仓库更容易维护。

Claude Code 的价值不是“帮我生成一个 monorepo”,而是持续检查边界。它可以帮你找出内部依赖没有写 workspace:*apps/*packages/* 反向引用、CI 没有用 filter 导致每次都跑全量这些问题。先把基础搭好,再让 Claude Code 做审查,效果比让它一次性重构整个仓库稳定得多。

这篇文章以 pnpm 11.5.0 为例。更完整的背景可以参考Claude Code monorepo 管理依赖管理指南


目标结构: 4 个 package 就足够开始

我建议从两个应用加两个共享 package 开始。边界简单,Claude Code 也容易理解。

flowchart LR
  web["apps/web\n@acme/web"] --> ui["packages/ui\n@acme/ui"]
  web --> config["packages/config\n@acme/config"]
  admin["apps/admin\n@acme/admin"] --> ui
  admin --> config
acme-workspace/
  apps/web/src/main.ts
  apps/web/package.json
  apps/admin/src/main.ts
  apps/admin/package.json
  packages/config/src/index.ts
  packages/config/package.json
  packages/ui/src/index.ts
  packages/ui/package.json
  pnpm-workspace.yaml
  package.json
  .npmrc
  CLAUDE.md

三个常见用例很适合这个结构。第一,把通用显示逻辑、标签和轻量 UI helper 放进 packages/ui,让 Web 和后台共用。第二,把产品名、公开 URL、feature flag 名称放进 packages/config,避免两个应用配置不一致。第三,后续添加 packages/contracts,把 API 类型或 Zod schema 同时给前端和服务端使用。

不要一开始就创建 packages/commonpackages/everything。共享代码应该表示“从任何调用方看语义都一样”,不是把不好归类的逻辑丢进去。给 Claude Code 的指令也要明确,比如“只抽出重复的 UI helper,业务判断留在 app 内”。


最小配置可以直接复制

根目录先放 pnpm-workspace.yaml。官方 pnpm-workspace.yaml 文档说明,packages 可以包含目录,也可以用 ! 排除目录。

packages:
  - "apps/*"
  - "packages/*"

catalog:
  typescript: ^5.8.3

package.json 只负责调度各 package。

{
  "name": "acme-workspace",
  "private": true,
  "packageManager": "pnpm@11.5.0",
  "scripts": {
    "check:web": "pnpm --filter @acme/web build",
    "build": "pnpm -r --sort --if-present build",
    "test": "pnpm -r --if-present test",
    "changed:test": "pnpm --filter \"...[origin/main]\" --if-present test"
  },
  "devDependencies": {
    "typescript": "catalog:"
  }
}

tsconfig.base.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true
  }
}

.npmrc 推荐从严格模式开始,尤其是库 package 较多时。

link-workspace-packages=false
save-workspace-protocol=rolling
shared-workspace-lockfile=true
strict-peer-dependencies=true
auto-install-peers=false

关键点是 workspace:*。pnpm 文档说明,使用 workspace: 协议时,pnpm 不会把依赖解析到本地 workspace 之外。这样可以避免内部包名和 registry 上的包名冲突。

packages/config/package.json:

{
  "name": "@acme/config",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    }
  }
}

packages/config/src/index.ts:

export const appConfig = {
  productName: "Acme Workspace",
  supportEmail: "support@example.com",
  publicSiteUrl: "https://example.com"
} as const;

packages/ui 通过 workspace 协议引用 config。

{
  "name": "@acme/ui",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "dependencies": {
    "@acme/config": "workspace:*"
  },
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    }
  }
}
import { appConfig } from "@acme/config";

export function renderPrimaryButton(label: string): string {
  return `[${appConfig.productName}] ${label}`;
}

应用侧只声明自己需要的内部依赖。

{
  "name": "@acme/web",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsc -p tsconfig.json"
  },
  "dependencies": {
    "@acme/config": "workspace:*",
    "@acme/ui": "workspace:*"
  }
}

apps/web/tsconfig.json:

{
  "extends": "../../tsconfig.base.json",
  "include": ["src"]
}
import { appConfig } from "@acme/config";
import { renderPrimaryButton } from "@acme/ui";

console.log(appConfig.publicSiteUrl);
console.log(renderPrimaryButton("Start trial"));

执行:

corepack pnpm install
corepack pnpm --filter @acme/web build
corepack pnpm -r --sort --if-present build

先让 Claude Code 理解 package 边界

Claude Code 官方有一页monorepos and large codebases。重点是: 大仓库里不要让无关文件占满上下文。pnpm workspace 也是一样。根目录可以启动 Claude Code,但任务必须说明范围。

CLAUDE.md 只写全局规则:

# Acme Workspace

This repository is a pnpm workspace.

Packages:
- apps/web: customer-facing TypeScript app
- apps/admin: internal admin app
- packages/ui: shared UI helpers
- packages/config: shared runtime constants

Rules:
- Use pnpm, not npm or yarn.
- Add internal dependencies with workspace:*.
- Run focused commands with pnpm --filter before full workspace commands.
- Do not move business logic into packages/ui.

如果某个 package 有独立规则,就放在那个目录的 CLAUDE.md。Claude Code 的 memory 文档说明,CLAUDE.md 是持久指令,不是临时提示。因此它应该短、具体、能长期维护。

claude -p "
Inspect this pnpm workspace. Do not edit files yet.
List the package graph, scripts, and risky dependency directions.
Then propose the smallest change needed to share UI helpers between apps/web and apps/admin.
"

这个提示先审查,再提案,能减少 Claude Code 过度抽象的概率。


filter 是日常开发和 CI 的核心

pnpm Filtering 用来限制命令作用的 package 范围。

pnpm --filter @acme/web build
pnpm --filter @acme/web... build
pnpm --filter ...@acme/ui test
pnpm --filter "...[origin/main]" --if-present test

最容易错的是 ... 的方向。@acme/web... 表示 web 加它依赖的包。...@acme/ui 表示 ui 加依赖 ui 的包。修改 UI 后只跑 @acme/ui...,很可能漏掉 web/admin 的测试。

CI 可以只跑受影响 package:

name: workspace-check

on:
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: corepack enable
      - run: corepack prepare pnpm@11.5.0 --activate
      - run: pnpm install --frozen-lockfile
      - run: pnpm --filter "...[origin/main]" --if-present test
      - run: pnpm --filter "...[origin/main]" --if-present build

常见坑和修复方式

第一个坑是内部包不用 workspace:*

{
  "dependencies": {
    "@acme/ui": "^0.1.0"
  }
}

应该改成:

{
  "dependencies": {
    "@acme/ui": "workspace:*"
  }
}

第二个坑是共享包变成业务垃圾桶。packages/ui 不应该发 API 请求,也不应该判断 billing 计划。第三个坑是循环依赖,例如 packages/ui import apps/web。这类问题会破坏构建顺序,也让 Claude Code 难以判断修改影响。

我会定期让 Claude Code 做一次边界审查:

claude -p "
Check this workspace for circular dependencies and misplaced imports.
Focus on packages/* importing from apps/*, duplicated config values,
and dependencies that should be workspace:*.
Return findings with file paths and minimal fixes.
"

如果要发布内部库,再加入 Changesets:

pnpm add -Dw @changesets/cli
pnpm changeset init
pnpm changeset
pnpm changeset version
pnpm -r publish --access public

pnpm 文档也提示,workspace 内 package 的版本管理通常交给 Changesets 或 Rush。不是所有 private app 都需要发布流程,apps/* 通常应该保持 private: true


总结和验证结果

pnpm workspace 的核心不是复杂工具链,而是把共享 UI、配置、类型和测试从“复制文件”变成明确依赖。Claude Code 最适合做边界审查、最小改动提案和 CI 失败分析。

下一步可以阅读 CLAUDE.md 最佳实践Claude Code 测试策略。如果需要团队导入或规则设计,可以从 Claude Code training 开始。

本文示例按 Windows、Node.js 22、Corepack、pnpm 11.5.0 检查。实际使用中,workspace:* 缺失和 filter 方向写反是最常见的问题。把“先让 Claude Code 输出 package graph,再修改文件”作为固定步骤,可以明显减少无用抽象和循环依赖。

#claude-code #pnpm #workspace #monorepo #typescript #ci
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。