Claude Codeとpnpm workspaceで始める実用モノレポ構築
Claude Codeとpnpm workspaceでモノレポを小さく始め、依存関係、filter、CI、失敗例まで実例で解説。
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/webとapps/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=falseとworkspace:*をセットで考えることです。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.mdやpackages/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/uiがapps/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/uiやpackages/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.yaml、workspace:*、小さな共通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点検」を毎回挟ませるだけで、不要な共通化と循環依存の早期発見にかなり効きます。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。