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

Claude Codeとpnpm workspaceで始める実用モノレポ構築

Claude Codeとpnpm workspaceでモノレポを小さく始め、依存関係、filter、CI、失敗例まで実例で解説。

Claude Codeとpnpm workspaceで始める実用モノレポ構築

pnpm workspaceは「リポジトリを増やさない」ための現実的な選択肢

claudecode-lab.comを運営しているMasaです。

小さなSaaSやメディア運営でも、コードはすぐ分かれます。Webアプリ、管理画面、共通UI、設定、メール送信、記事生成スクリプト。最初は1つのアプリで足りますが、同じButtonやZodスキーマを何度もコピーし始めたら黄色信号です。

そこでpnpm workspaceを使います。workspaceは「複数のpackageを1つのGitリポジトリで管理する仕組み」です。公式のpnpm Workspaceでも、モノレポや複数プロジェクトを1つのworkspaceにまとめられると説明されています。pnpm-workspace.yamlがルートにあると、pnpmはそのディレクトリ群を1つの作業単位として扱います。

Claude Codeを組み合わせる価値は、雛形生成そのものよりもレビューにあります。workspace:*が抜けていないか、依存方向が逆になっていないか、CIで全packageを無駄にbuildしていないか。人間が見落としやすい「足場」をClaude Codeに何度も確認させると、モノレポの事故が減ります。

この記事では、pnpm 11.5.0を前提に、小さなTypeScriptモノレポを作ります。関連する全体像はClaude Codeモノレポ管理、依存関係の考え方はClaude Code依存関係管理も合わせて読むと理解しやすいです。


完成形: 小さく始める4 package構成

最初から巨大なplatform repoを作る必要はありません。私は次の4つから始めるのが一番失敗しにくいと感じています。

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
      package.json
    admin/
      src/main.ts
      package.json
  packages/
    config/
      src/index.ts
      package.json
    ui/
      src/index.ts
      package.json
  pnpm-workspace.yaml
  package.json
  tsconfig.base.json
  .npmrc
  CLAUDE.md

ユースケースは少なくとも3つあります。1つ目はpackages/uiにフォーム部品や表示ロジックを置き、apps/webapps/adminから使うケース。2つ目はpackages/configに環境変数名、APIエンドポイント、feature flag名を集約し、設定の食い違いを防ぐケース。3つ目は後でpackages/contractsを追加し、APIの型やZodスキーマをフロントとサーバーで共有するケースです。

最初からpackages/everythingを作らないのが大事です。共通化は便利ですが、どこからでも触れる巨大packageを作ると、変更影響が読めなくなります。Claude Codeには「共通化して」ではなく「この2つのアプリで重複しているが、UIだけを抽出し、業務ロジックは残して」と依頼します。


最小構成をコピペで作る

まずルートに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",
    "lint": "pnpm -r --if-present lint",
    "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では、workspaceの曖昧な解決を避けます。

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

ここで重要なのはlink-workspace-packages=falseworkspace:*をセットで考えることです。pnpm公式は、workspace:プロトコルを使うとローカルworkspace package以外に解決しない、と説明しています。つまり@acme/uiをregistryから誤って取ってくる事故を防げます。

共通設定packageは小さく始めます。

{
  "name": "@acme/config",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    }
  }
}
export const appConfig = {
  productName: "Acme Workspace",
  supportEmail: "support@example.com",
  publicSiteUrl: "https://example.com"
} as const;

packages/ui@acme/configをworkspace依存として参照します。

{
  "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

@acme/adminも同じ形で作れば、UIと設定を共有した2つ目のアプリになります。


Claude Codeには「package境界」を先に教える

Claude Codeの公式ドキュメントにはmonorepos and large reposのガイドがあります。大きいリポジトリでは、rootの指示だけで全部を読ませると、関係ないpackageの情報がcontextに入りすぎます。pnpm workspaceでも同じです。

rootの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 running full workspace commands.
- Do not move business logic into packages/ui.

packageごとの癖は、そのディレクトリのCLAUDE.mdに分けます。Claude Codeのmemory documentationでは、CLAUDE.mdは永続的な指示として読み込まれると説明されています。私はrootに全部書かず、apps/web/CLAUDE.mdpackages/ui/CLAUDE.mdへ寄せます。

# packages/ui

This package contains presentation helpers only.

Allowed:
- Small formatting helpers
- Shared component labels
- Accessibility-focused UI utilities

Not allowed:
- API calls
- Billing logic
- Feature flag decisions

Claude Codeへの最初の依頼は、編集ではなく確認から始めると安全です。

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

この「先に点検して、最小差分を提案して」という形にすると、Claude Codeがいきなり大きな抽象化を作る確率が下がります。


filterを使うとCIと日常作業が軽くなる

pnpmのFilteringは、workspaceの一部だけにコマンドをかけるための機能です。Claude Codeに任せるときも、最初から全packageでbuildさせるのではなく、影響範囲を絞ります。

# webだけ検査
pnpm --filter @acme/web build

# webと、その依存packageをbuild
pnpm --filter @acme/web... build

# uiを使っているアプリ側も含めてtest
pnpm --filter ...@acme/ui test

# mainとの差分に関係するpackageだけtest
pnpm --filter "...[origin/main]" --if-present test

落とし穴は...の向きです。@acme/web...はwebとwebが依存する側を選びます。...@acme/uiはuiとuiに依存している側を選びます。UIを変更したのに@acme/ui...だけtestすると、依存元のweb/adminを見落とすことがあります。

GitHub Actionsでは次のように絞れます。

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 lint
      - run: pnpm --filter "...[origin/main]" --if-present test
      - run: pnpm --filter "...[origin/main]" --if-present build

小さいrepoでは全件実行でも問題ありません。ただ、記事サイトやSaaSを続けていると、packageは必ず増えます。最初からfilter前提のscript名にしておくと、CI費用と待ち時間を抑えられます。


よくある失敗と具体的な回避策

1つ目は、内部packageを通常のsemverで書く失敗です。

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

これはregistryに同名packageがある場合や、将来publishした後に混乱します。workspace内部なら次にします。

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

2つ目は、共通packageにアプリ固有の処理を入れる失敗です。packages/uiにbillingの判定やadmin専用のAPI callを入れると、web側の依存が濁ります。共通packageは「どのアプリから呼ばれても同じ意味になるもの」だけにします。

3つ目は、Claude Codeをrootから起動して全packageを同時に直させる失敗です。大きな変更では便利に見えますが、関係ないpackageのCLAUDE.mdや古いコードまで読んで、提案が膨らみます。作業がpackages/uiだけならそのディレクトリから始めるか、依頼文で対象を明示します。

4つ目は、循環依存です。pnpm公式も、workspace依存にcycleがあるとscriptのtopological orderを保証できないと注意しています。packages/uiapps/webをimportし始めたら設計が逆です。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 suggested minimal fixes.
"

releaseが必要ならChangesetsを足す

全部privateならrelease管理は不要です。ただしpackages/uipackages/configを社内registryやnpmに出すなら、バージョン管理を後回しにしない方が安全です。pnpm公式も、workspace内packageのversioningはpnpm単体の組み込み解決ではなく、ChangesetsやRushの利用を案内しています。

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

公開前には、workspace:*がpublish時に通常のversion rangeへ変換されることを理解しておきます。内部だけで使うpackageはprivate: trueのままにしてください。

Claude Codeにはrelease PRのレビューも任せられます。

claude -p "
Review this Changesets release PR.
Check that only publishable packages are versioned,
workspace dependencies are valid,
and apps/* packages remain private.
Do not change files unless you find a blocking issue.
"

まとめ: まず境界、次に自動化

pnpm workspaceは、モノレポを難しくする道具ではありません。むしろ、共通UI、設定、型、テストを「コピーではなく依存関係」として扱うための小さな土台です。Claude Codeは、その土台が崩れていないかを何度も確認する相棒として使うと効果が出ます。

始める順番は、pnpm-workspace.yamlworkspace:*、小さな共通package、CLAUDE.md、filter付きCIです。いきなり高度なビルドシステムや大量の生成コードを入れる必要はありません。

次にやるなら、CLAUDE.mdベストプラクティスで指示ファイルを整え、Claude Codeテスト戦略でworkspace全体の確認コマンドを固めてください。チームでClaude Codeとpnpm workspaceの運用ルールを作りたい場合は、Claude Code研修・相談から相談できます。

この記事で紹介した内容を実際に試した結果

この記事の構成は、Windows環境、Node.js 22、Corepack、pnpm 11.5.0を前提に確認しました。workspace:*を外すと内部packageの解決が曖昧になり、...@acme/ui@acme/ui...を取り違えるとUI変更時にアプリ側testを漏らしやすいことも再確認しました。実務では、Claude Codeに「編集前のpackage graph点検」を毎回挟ませるだけで、不要な共通化と循環依存の早期発見にかなり効きます。

#claude-code #pnpm #workspace #monorepo #typescript #ci
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。