Claude Code 活用による npm から pnpm 移行で遭遇したハマりポイントと解決策
こんにちは。アノテーションの及川です。
クラスメソッドではAI駆動開発を推進するための取り組みを行っており、クラスメソッド社員も実際に日常的にAIを使って開発を行っています。
弊社メンバーが日常的に実践するAI駆動開発のナレッジやTipsを共有するために、AI駆動開発 Advent Calendar 2025を開催しています。
本ブログは、この企画の4日目の記事になります。
もしAI駆動開発の最先端を知りたい方は、この1ヶ月間、ぜひ本アドベントカレンダーをチェックしてみてください。
はじめに
先日、モノレポ構成のプロジェクト(以前こちらの記事でご紹介した作成した TODO アプリ)を npm から pnpm に移行する機会がありました。
今回は Claude Code と共に、計画・参考資料・進捗・手順のドキュメントを作成しながら、ステップに分けて移行作業を進めました。
基本的な移行手順は公式ドキュメント通りに進められた一方で、GitHub Actions での CI/CD 対応で予想外のハマりポイントがいくつかありました。
今回は、その経験を共有したいと思います。
この記事で紹介すること
- npm から pnpm への基本的な移行手順
- phantom dependencies 問題と対処法
- GitHub Actions での CI/CD 対応ポイント
- Husky フック対応での注意点
Claude Code との共創
今回の移行作業は、Claude Code と対話しながら進めました。
依頼時のプロンプト
Claude Code には、以下のような依頼を行いました。
- 本プロジェクトを npm から pnpm に安全に移行すること
- 既存のソースコード、テストコードに影響を与えないこと
- 各ステップについて 1 ステップごとに確実に実施すること
- 移行手順を漏れなくドキュメントに記載すること
コンテキストとして提示したファイル
Claude Code がプロジェクトを円滑に理解して進めれるように、以下のファイルを事前に用意しコンテキストとして提示しました。
| ファイル | 内容 |
|---|---|
| 計画.md | 移行の目的と依頼事項 |
| 参考.md | 参考 URL(pnpm 公式ドキュメント、移行ブログ記事など) |
| 進捗.md | 各 Step の進捗状況を記録 |
| 手順.md | 実施した内容、背景、実行コマンド、修正ファイルを詳細に記録 |
以下は、計画.md の抜粋です。
## 目的
- 本プロジェクトを npm から pnpm に安全に移行すること
- 既存のソースコード、テストコードに影響を与えないこと
## 依頼事項
- ステップ毎に確実に実施
1. 現状の実装コードの調査
2. 参考資料の参照・確認
3. 安全な npm→pnpm 移行のための手順を計画
4. 計画に従い、ステップバイステップで実施
5. 移行手順を手順.md ファイルに漏れなく記載
以下は、手順.md の抜粋です。各ステップを以下のフォーマットで記録しました。
## Step N: ステップのタイトル
### 目的
このステップで達成すること
### 公式ドキュメント参照
- **pnpm xxx**: https://pnpm.io/xxx
### 実施内容
#### N.1 作業項目
**背景・理由**:
- なぜこの変更が必要なのか
- 公式ドキュメントからの引用
**修正ファイル**: `対象ファイル名`
**変更前**:
(変更前のコード)
**変更後**:
(変更後のコード)
**結果**:
- ✅ 完了
このフォーマットで記録することで、後から「なぜこの変更をしたのか」が明確になり、振り返りなどの整理に役立ちました。
ポイント
- ステップバイステップ: 一度に全部やらせず、1 ステップごとに確認しながら進める
- ドキュメント化: 移行手順を記録させることで、後から振り返りやすくなる
- 公式ドキュメントの提示: 公式ドキュメントの設定例を参照させることで、正しい設定方法を学習させる
pnpm とは
pnpm は npm や yarn の代替となるパッケージマネージャーです。主な特徴は以下の通りです(詳細は公式ドキュメントをご参照ください)。
- 高速なインストール: ハードリンクを活用した効率的なパッケージ管理
- ディスク容量の節約: 同じパッケージを複数プロジェクトで共有
- 厳格な依存関係解決: phantom dependencies を許容しない
特に 3 つ目の「厳格な依存関係解決」は、npm からの移行時に最もハマりやすいポイントだと思います。
基本的な移行手順
1. pnpm-workspace.yaml の作成
npm workspaces から pnpm workspaces へ移行するには、pnpm-workspace.yaml を作成します。
packages:
- apps/*
- packages/*
2. package.json の更新
ルートの package.json を以下のように更新します。
{
"engines": {
"node": ">=22.x",
"pnpm": ">=10.x"
},
"packageManager": "pnpm@10.0.0"
}
packageManager フィールドは Corepack と連携し、チーム全員が同じバージョンの pnpm を使用できるようにします。
また、workspaces フィールドは pnpm では使用しないため削除します。
3. pnpm only の制約を追加
pnpm 以外のパッケージマネージャーをブロックするため、preinstall スクリプトを追加します。
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
4. ロックファイルの変換
pnpm import コマンドで既存の package-lock.json から pnpm-lock.yaml を生成します。
# 既存の node_modules を削除
rm -rf node_modules */node_modules
# ロックファイルを変換
pnpm import
# 古いロックファイルを削除
rm package-lock.json
# 依存関係をインストール
pnpm install
5. npm コマンドを pnpm コマンドに変換
| npm | pnpm |
|---|---|
npm run <script> --workspaces |
pnpm -r <script> |
npm run <script> -w <workspace> |
pnpm --filter <workspace> <script> |
npm ci |
pnpm install --frozen-lockfile |
phantom dependencies 問題
問題の概要
pnpm への移行後、以下のようなエラーが発生することがあります。
Error: Cannot find module 'ajv/dist/core'
これは phantom dependencies 問題と呼ばれるものです。
You’ll find that pnpm combats this problem by enforcing stricter package access rules - a package can only access its explicitly declared dependencies. This limits the risk associated with phantom dependencies, providing you with a more predictable and secure environment.
npm と pnpm の違い
npm では、依存関係がホイスティング(巻き上げ)されるため、直接依存していないパッケージにもアクセスできてしまいます(暗黙的な依存)。
一方、pnpm は厳格な依存関係解決を行うため、明示的に依存宣言されていないパッケージにはアクセスできません。
# npm の場合(ホイスティング)の例
node_modules/
ajv/ <- 誰でもアクセス可能
# pnpm の場合(厳格)の例
node_modules/
.pnpm/
ajv@x.x.x/
node_modules/
ajv/ <- 直接依存しているパッケージのみアクセス可能
解決策
エラーで要求されているモジュールを、明示的に devDependencies に追加します。
{
"devDependencies": {
"ajv": "8.17.1"
}
}
私の場合、以下のパッケージを追加する必要がありました。
ajv: OpenAPI バリデーターの間接依存esbuild: AWS CDK の Lambda バンドリングに必要
GitHub Actions での CI/CD 対応
1. pnpm のセットアップ
GitHub Actions で pnpm を使用するには、pnpm/action-setup を追加します。
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: "package.json"
cache: pnpm # npm から pnpm に変更
2. node_modules キャッシュは使わない
ここが最大のハマりポイントでした。
当初、以下のように node_modules をキャッシュしていました。
# 問題のある設定
- name: Cache node_modules
uses: actions/cache@v4
with:
path: "**/node_modules"
key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }}
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: pnpm install --frozen-lockfile
この設定には問題があります。
問題点:
- キャッシュがヒットすると
pnpm installがスキップされる - 新しい依存関係がキャッシュに含まれていない
- phantom dependencies 修正後も古いキャッシュが使われ続ける
解決策:
node_modules キャッシュを廃止し、毎回 pnpm install を実行するようにしました。
# 改善後の設定
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: "package.json"
cache: pnpm # pnpm ストアをキャッシュ
- name: Install dependencies
run: pnpm install --frozen-lockfile
actions/setup-node の cache: pnpm オプションにより、pnpm ストアがキャッシュされます。これにより、pnpm install は高速に実行されつつ、最新の依存関係が確実にインストールされます。
3. paths-filter への pnpm-lock.yaml 追加
CI の変更検知に dorny/paths-filter を使用している場合、pnpm-lock.yaml を追加しておくと、依存関係の変更時に CI が確実にトリガーされます。
filters: |
app:
- "src/**"
- "package.json"
- "pnpm-lock.yaml" # 追加
Husky フック対応
npx から pnpm への変換
Husky フック内の npx コマンドも pnpm に変換する必要があります。
# 変更前
npx lint-staged
# 変更後
pnpm exec lint-staged
pnpm dlx vs pnpm exec の違い
ここで注意が必要なのは、pnpm dlx と pnpm exec の違いです。
| コマンド | 動作 |
|---|---|
npx <pkg> |
ローカルにあれば使用、なければダウンロード |
pnpm dlx <pkg> |
レジストリから取得して一時的に実行(依存関係には追加しない) |
pnpm exec <pkg> |
プロジェクトの依存関係にあるパッケージを実行 |
pnpm dlx はレジストリからパッケージを取得し、依存関係としてインストールせずに一時的に実行するコマンドです。一方、pnpm exec はプロジェクトの node_modules/.bin にあるコマンドを実行します。devDependencies にインストール済みのパッケージを使いたい場合は pnpm exec を使用します。
pre-commit フックで lint-staged を使う場合、pnpm exec lint-staged とすることで、devDependencies で pinned されたバージョンが確実に使用されます。
ゼロデイ攻撃対策
pnpm には、公開から一定期間が経過していないパッケージのインストールを防止する minimumReleaseAge 設定があります。
設定例
# pnpm-workspace.yaml
minimumReleaseAge: 20160 # 14 days (14 * 24 * 60 = 20160 minutes)
この設定例により、公開から 14 日未満のパッケージバージョンはインストールされなくなります。悪意のあるパッケージは公開後、通常数時間〜数日以内に検出・削除されるため、この待機期間でサプライチェーン攻撃のリスクを少しでも軽減できます。
まとめ
npm から pnpm への移行で遭遇したハマりポイントをまとめると、以下の通りです。
移行時のチェックリスト
- pnpm-workspace.yaml を作成
- package.json の packageManager フィールドを追加
- workspaces フィールドを削除
- preinstall スクリプトで pnpm only 制約を追加
- pnpm import でロックファイルを変換
- npm/npx コマンドを pnpm コマンドに変換
- GitHub Actions で pnpm/action-setup を追加
- node_modules キャッシュを廃止
- Husky フックの npx を pnpm exec に変更
- minimumReleaseAge でゼロデイ攻撃対策
特に注意すべきポイント
- phantom dependencies: エラーが出たら明示的に依存関係を追加
- GitHub Actions キャッシュ: node_modules ではなく pnpm ストアをキャッシュ
- pnpm dlx vs pnpm exec: 用途に応じて使い分ける
pnpm の厳格な依存関係解決は、最初はハマりやすいですが、一度対応すれば依存関係の管理が明確になり、予期せぬバグを防ぐことができます。
この記事が誰かのお役に立てば幸いです。
アノテーション株式会社について
アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。当社は様々な職種でメンバーを募集しています。「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。








