Claude Media
Claude Codeでpnpm workspaceを扱う — --filterで対象パッケージだけ検証する

Claude Codeでpnpm workspaceを扱う — --filterで対象パッケージだけ検証する

pnpmワークスペースのモノレポで全パッケージのtestを走らせず、--filterで変更したパッケージと依存元だけを検証させる手順。CLAUDE.mdの書き分けと権限ルールの組み方も扱います。

pnpmワークスペースのモノレポでClaude Codeに検証させるときは、pnpm --filter <セレクタ> testで対象を絞ります。パッケージが数十あるリポジトリで毎回全体のテストを回すと、待ち時間もログの量も増え、Claudeが読むべき失敗が埋もれます。--filterは、パッケージ名・ディレクトリ・依存関係・gitの変更差分のどれからでも対象を選べるpnpmのセレクタです。

この記事では、--filterの書き方を早見表にしたうえで、Claudeに守らせる約束をCLAUDE.mdへ書く位置、許可ルールの組み方、フィルタが何も選ばないときの切り分けを順に扱います。Turborepoのタスクグラフで絞る場合はClaude CodeでTurborepoのモノレポを設定する、Nxのaffectedで絞る場合はClaude CodeでNxのaffectedをフックから使う設計が対応する内容です。

--filterの基本形 — 名前・依存・ディレクトリ・差分で選ぶ

セレクタは--filter(短縮は-F)に渡します。基本の書式はpnpm --filter <package_selector> <command>です。

pnpm --filter "@babel/core" test
pnpm --filter "@babel/*" test
pnpm --filter "*core" test

1行目は名前の完全一致、2行目と3行目はパターンで複数パッケージを選びます。スコープは省略でき、--filter=coreならcoreが見つからないときに@babel/coreが選ばれます。ただしワークスペースに@babel/coreと@types/coreのように同名のパッケージが複数あると、スコープなしの指定は何も選びません。

Claudeに渡すコマンドは、スコープ付きの正式名で書くのが安全です。名前の衝突を気にせず済み、あとから同名のパッケージが増えても検証の対象が変わりません。

くらべる

全体を回す場合と--filterで絞る場合

待ち時間とログが増える

フィルタなしで全体を検証

全パッケージのtestが走り、無関係な失敗や長い出力がClaudeのコンテキストを使います。変更の影響を調べたいのに、読む量だけが増えます。

結果が小さく、原因を追いやすい

--filterで対象を絞る

編集したパッケージとその依存元だけを走らせます。出力が短くなり、失敗した場合も原因の範囲が狭くなります。

セレクタ早見表

依存の向きを示す...(省略記号)が、最も取り違えやすい部分です。パッケージ名の後ろに付けると「依存先」、前に付けると「依存元」を含めます。

書き方選ばれるもの使いどころ
foo選ばれるものfooだけ使いどころ1パッケージの検証
foo...選ばれるものfooと、fooが依存するパッケージ(直接・間接)使いどころfooを動かすのに必要な範囲を確かめる
foo^...選ばれるものfooの依存先だけ(foo自身は含まない)使いどころ土台側の無事を確認する
...foo選ばれるものfooと、fooに依存するパッケージ(直接・間接)使いどころfooを変えたときの影響範囲
...^foo選ばれるものfooの依存元だけ(foo自身は含まない)使いどころ利用側だけを回し直す
./packages/**選ばれるものそのディレクトリ配下のパッケージ使いどころディレクトリ単位の検証
[origin/main]選ばれるもの指定のコミットやブランチ以降に変更があったパッケージ使いどころ差分だけの検証

共有ライブラリを直した直後に確かめたいのは、そのライブラリの利用側です。sharedを変えたなら、pnpm --filter "...shared" testでsharedと依存元をまとめて検証します。逆にshared...では、sharedが依存する下流側が対象になり、利用側は含まれません。

変更したパッケージだけを検証する

編集のたびにパッケージ名を渡すのは手間なので、gitの差分を基準にする書き方を使います。

# 変更したパッケージと、その依存元の test
pnpm --filter "...[origin/main]" test
 
# 変更したパッケージだけ(依存元は含めない)
pnpm --filter "[origin/main]" test
 
# packages/ 配下の変更に限る
pnpm --filter "...{packages/**}[origin/main]" test

[<since>]はコミットやブランチを指定し、それ以降に変更のあったパッケージを選びます。前後に...を付けると、依存先や依存元も含められます。ブランチ名は使っているリポジトリのものに読み替えます。pnpmのページの例はorigin/masterです。

押さえておく条件が3つあります。

  • この書き方にはGit 2.24以上が必要です
  • v12.7.0以降は、指定したブランチそのものではなく、現在のブランチが分岐した時点のコミット(マージベース)と比較します。指定ブランチ側にだけ入った新しいコミットの変更は、選択に入りません。コミット前の変更は含まれます
  • マージベースがないshallow cloneでは、指定先と直接比較します。CIでこのセレクタを使うなら、履歴の取得深度に注意が必要です

パッケージが変更扱いになる条件は、ファイルの編集だけではありません。pnpm-workspace.yamlのcatalogで、そのパッケージが使っているバージョンが変わった場合も変更扱いです。gitがリネームとして検出した移動で、ファイルが外へ出ていったパッケージも含まれます。

READMEの編集やテストだけの変更を依存元に波及させない

差分基準のフィルタには、波及を抑える2つのオプションがあります。

# テストファイルだけの変更なら依存元を含めない
pnpm --filter="...[origin/main]" --test-pattern="test/*" test
 
# READMEの変更は「変更」に数えない
pnpm --filter="...[origin/main]" \
  --changed-files-ignore-pattern="**/README.md" run build

--test-patternは、変更されたファイルがテストに関するものかを判定するグロブです。該当するとき、そのパッケージの依存元は含まれません。--changed-files-ignore-patternは、指定したパターンに一致するファイルを変更の検出から外します。ドキュメントだけを直したClaudeの作業が、ビルドの全体再実行につながるのを避けられます。

devDependenciesを辿りたくないときは--filter-prodを使います。--filterと同じ動作で、依存先を選ぶ際にdevDependenciesを除外します。

除外と複数指定の組み立て方

先頭に!を付けたセレクタは除外になります。

# foo以外の全パッケージ
pnpm --filter=!foo test
 
# libディレクトリ配下以外
pnpm --filter=!./lib test

zshなど一部のシェルでは!をエスケープして\!と書く必要があります。引用符で囲んだ"!./lib"のように、シェルに解釈させない書き方にしておくと、Claudeが生成したコマンドが環境差で壊れにくくなります。

複数の--filterは、1つでも当てはまるパッケージが対象です。v12.7.0以降は指定した順に評価され、あとの包含が先の除外を打ち消せます。最初のセレクタが除外のときは、ワークスペースの全パッケージから出発します。

# fooの依存元、bar、bazの依存先をまとめて対象にする
pnpm --filter ...foo --filter bar --filter baz... test
 
# packages/ 配下を外したうえで、@scope/core だけ戻す
pnpm --filter "!./packages/**" --filter @scope/core test

CLAUDE.mdに検証コマンドの約束を書く

ここまでの書式をClaudeが毎回思い出す保証はないので、リポジトリに書いておきます。置き場所は、CLAUDE.mdの読み込みタイミングで決まります。

Claude Codeは起動時に、作業ディレクトリから上位の階層にあるCLAUDE.mdを読みます。サブディレクトリのCLAUDE.mdは起動時には読まれず、Claudeがそのディレクトリのファイルを読んだり編集したりした時点で追加されます。pnpmワークスペースなら、次の分担になります。

分担

CLAUDE.mdの置き場所

  • ルートのCLAUDE.md

    どのパッケージから作業を始めても効かせたい約束。差分基準の検証コマンドと、全体検証を回す場面の線引きを書きます。

  • packages/api/CLAUDE.mdなど

    そのパッケージ固有の検証。正式なパッケージ名と、依存元まで確かめるコマンドを書きます。

ルートに置く例です。pnpm-workspace.yamlのpackagesに合わせて、パスやブランチ名を読み替えます。

# 検証ルール(pnpmワークスペース)
 
- コードを変えたら、まず変更したパッケージと依存元だけを検証する
  `pnpm --filter "...[origin/main]" test`
- 特定のパッケージだけ確かめたいときは、正式名で指定する
  `pnpm --filter @acme/api test`
- 共有パッケージ(`@acme/shared`)を変えたら、依存元も回す
  `pnpm --filter "...@acme/shared" test`
- フィルタなしの全体検証は、PRを出す直前に1回だけ
- フィルタ付きコマンドはシェルに解釈されないよう必ず引用符で囲む

パッケージ側の例です。

# @acme/api
 
- 検証: `pnpm --filter @acme/api test`
- 他パッケージが使う公開関数を変えたら:
  `pnpm --filter "...@acme/api" test`

ルートのCLAUDE.mdは、/compactのあともディスクから読み直されて再投入されます。検証の約束はルートに寄せておけば、長いセッションでも残ります。パッケージ側のCLAUDE.mdは、圧縮後に再びそのパッケージのファイルを読んだ時点で入り直します。

CLAUDE.mdは1ファイル200行未満が目安です。@pathによるimportは整理には効きますが、起動時に読み込まれるのでコンテキストの節約にはなりません。検証ルールが増えてきたら、パッケージ別の記述をルートへ積み上げず、各パッケージのCLAUDE.mdへ分けます。階層の全体設計はClaude Codeモノレポ設計、書き方のパターンはClaude CodeのCLAUDE.mdを実用に引き上げる10のパターンにまとめています。

どのCLAUDE.mdが効いているかは、セッション中の/contextの「Memory files」で見られます。サブディレクトリのCLAUDE.mdは起動時に読まれないため、この欄には出ません。読み込まれると、ターミナルにLoadedの行が表示されます。

Claudeに検証ループを回させる

約束を書いたら、依頼の側は短くて済みます。ただし「テストして」だけでは、Claudeが全体検証を選ぶことがあります。範囲を指定した依頼文にしておくと安定します。

packages/shared の formatDate を変更しました。
CLAUDE.md の検証ルールに従って、shared とその依存元だけ test を実行し、
失敗したら原因を読んで修正してください。

Claudeの検証は、おおむね次の流れになります。以下は、CLAUDE.mdに書いた約束どおりに動かしたときの想定手順です。

手順

フィルタ付きの検証ループ

  1. 1

    編集したパッケージの名前を確かめる

    package.jsonのnameを読み、スコープ付きの正式名を使います。

  2. 2

    そのパッケージだけ検証する

    pnpm --filter @acme/shared testで、まず最小の範囲を回します。

  3. 3

    依存元まで広げる

    公開されている関数や型を変えたなら、pnpm --filter "...@acme/shared" testで利用側を確かめます。

  4. 4

    差分基準で取りこぼしを拾う

    pnpm --filter "...[origin/main]" testで、ほかに変更したパッケージがないか確認します。

  5. 5

    最後に全体を一度だけ回す

    PRを出す前に、フィルタなしの検証を1回だけ実行します。

出力を読ませる場合は、失敗したパッケージ名とエラー行だけを取り出して直させます。--filterで範囲が絞られているぶん、どのパッケージの失敗かを探す手間が減ります。

権限ルールを組む

検証のたびに許可ダイアログが出ると、ループが止まります。.claude/settings.jsonにBashの許可ルールを足します。

{
  "permissions": {
    "allow": [
      "Bash(pnpm --filter * test)",
      "Bash(pnpm --filter * run build)"
    ]
  }
}

ルールの*は任意のテキストに一致します。pnpm --filter * testと書いても、*の位置にfoo execのような別のサブコマンドが入る形まで許可しうる点には注意が必要です。Bash(pnpm --filter *)のように末尾を*にすると、addやexecを含むあらゆる--filter付きコマンドが対象になります。許可の範囲を広げすぎたくないチームでは、testとbuildに限定した前者を基本にし、依存を足すaddは許可に入れずに都度確認します。

&&や;でつないだ複合コマンドは、サブコマンドごとに別々に照合されます。pnpm --filter x test && rm -rf distのように、許可済みのコマンドの後ろに別のコマンドを付けても、後ろのコマンドは許可されません。NODE_ENV=testのような既知の環境変数の代入が先頭にあっても、ルールは一致します。

--filterが何も選ばないときの切り分け

フィルタは、書き間違えても気づきにくい仕組みです。次の落とし穴が実際に起きやすい箇所です。

落とし穴

同名のパッケージがあるのにスコープを省いた

--filter=coreは@babel/coreを選べますが、ワークスペースに@babel/coreと@types/coreの両方があると何も選びません。Claudeには正式名を使わせます。

落とし穴

{packages/*}が1階層しか見ない

{<dir>}はグロブとして評価されます。{packages/*}が選ぶのはpackages/の1つ下のプロジェクトで、さらに深い階層にapps/web/uiのようなパッケージがあるなら**が要ります。以前の「サブツリー全体に一致する」挙動に戻すには、pnpm-workspace.yamlにlegacyDirFiltering: trueを書きます。

落とし穴

ドット始まりのディレクトリが拾われない

*と?は、ドットで始まる名前には一致しません。packages/*はpackages/.cacheを飛ばします。ドット始まりのディレクトリのパッケージを選ぶなら、そのディレクトリ名を明示します。

落とし穴

引用符なしの...やブラケットをシェルが先に解釈する

...[origin/main]のようなセレクタは、シェルによってはグロブとして展開されます。zshの!の扱いも含め、セレクタは必ず引用符で囲みます。CLAUDE.mdに書くコマンド例が引用符付きなら、Claudeも引用符付きで実行します。

何も選ばれていない状態を失敗として扱いたいときは、--fail-if-no-matchを付けます。一致するパッケージがなければCLIが失敗します。failIfNoMatchという設定で恒久的に有効にもできます。フラグなしで一致が0件のときの終了コードは、フィルタのページに記載がありません。Claudeが「何も実行されなかったが成功した」と読み違えるのを避けるには、このフラグを検証コマンドに含めておくのが無難です。

pnpm --filter "@acme/api" --fail-if-no-match test

ほかのツールとの使い分け

--filterはpnpm単体で完結し、導入するものがありません。タスクの依存順やキャッシュまで管理したいときは、ビルドオーケストレーターを足す段階です。Turborepoならタスクグラフ、Nxならaffectedで同じ「影響範囲だけ」を実現でき、設定と対応するCLAUDE.mdの書き方は先の2記事にあります。

サンドボックスの中でpnpmが固まる場合は、--filterの問題とは別の原因です。Claude Codeのsandbox内でpnpmが固まる原因と回避で切り分けられます。

まとめ

全体の検証は最後に1回で足ります。それ以外は、編集したパッケージ(--filter name)、その利用側(--filter ...name)、差分で見つかる残り(--filter "...[origin/main]")の3段で十分に回ります。この3つをルートのCLAUDE.mdに書き、testとbuildに限った許可ルールを足せば、Claudeは毎回同じ範囲で検証を繰り返します。

この記事を共有:XはてブLinkedIn