投稿日:2026/11/6
更新日:2026/11/6

vitest を 2 系から 5 系へ上げたら、テストが起動しなくなった。原因は、vitest の peer である vite の版を pnpm が範囲を確かめずに選び、古い vite 5 が残ったことだった。
peerDependencies とは何か、パッケージマネージャーがどの版をどう入れるのかを、手元で試した結果つきで整理する。
peerDependencies は「このパッケージは X を使うが、X は利用する側のプロジェクトと同じ1つを使う」という宣言。実行時には必ず要るが、いまは package.json に自分で書かなくてもパッケージマネージャーが自動で入れる(npm 7 以降、pnpm 8 以降)dependencies から peerDependencies に移る更新で、範囲外の古い版がそのまま残る検証環境: Node.js 22.23.0、pnpm 10.20.0 / 11.12.0、npm 10.9.8(2026-11-06 時点)
dependencies |
peerDependencies |
|
|---|---|---|
| 意味 | 自分専用の部品として使う | 利用する側と同じ1つを共有して使う |
| 版が合わないとき | 自分用に別の版をもう1つ入れて解決する | 2つ目は入れない。警告かエラーで知らせるだけ |
| 例 | vitest 2 系 → vite | react-dom → react、vitest 5 系 → vite |
共有が必要な代表例は React。react-dom とアプリが別々の react を読み込むと、フックが正しく動かない。だから react-dom は react を自分の部品として抱えず、peer として「利用する側の react を使う」と宣言している。
flowchart LR
subgraph Deps["dependencies:それぞれが自分用を持つ"]
App1["アプリ"] --> LibA["ライブラリA"]
App1 --> LibB["ライブラリB"]
LibA --> X1["X 1.0(A 専用)"]
LibB --> X2["X 2.0(B 専用)"]
end
subgraph Peer["peerDependencies:1つを共有する"]
App2["アプリ"] --> React["react(1つだけ)"]
App2 --> ReactDOM["react-dom"]
ReactDOM -.->|peer| React
end
vitest 5.0.5 の宣言(抜粋)。npm view vitest@5.0.5 peerDependencies peerDependenciesMeta で確かめられる。
{
"name": "vitest",
"version": "5.0.5",
"peerDependencies": {
"vite": "^6.4.0 || ^7.0.0 || ^8.0.0",
"jsdom": "*",
"@vitest/ui": "5.0.5"
},
"peerDependenciesMeta": {
"vite": { "optional": false },
"jsdom": { "optional": true },
"@vitest/ui": { "optional": true }
}
}
vite は必須の peer。無ければ vitest は動かないpeerDependenciesMeta で optional: true にしたものは任意の peer。DOM を使うテストを書く人だけが jsdom を入れる、という使い方半分だけ正しい。
| 思い込み | 実際 |
|---|---|
| インストールされていなければならない | 正しい。vitest は実行時に vite を読み込むので、無ければ動かない |
| プロジェクトの package.json に書かなければならない | いまは違う。必須の peer はパッケージマネージャーが自動で入れる |
| パッケージマネージャー | 必須の peer が無いとき | 範囲外の版があるとき |
|---|---|---|
| npm 3〜6 | 入れない。利用者が自分で package.json に書く | peer を無視して依存を組み立てる |
| npm 7 以降 | 範囲に合う版を自動で入れる | エラーで止まる(ERESOLVE) |
| pnpm 8 以降 | 範囲に合う版を自動で入れる(autoInstallPeers の既定が true) |
警告だけ出して、その版を使う |
npm 7 以降でも --legacy-peer-deps を付けると npm 3〜6 と同じ扱いに戻る。npm 10.9.8 で react-dom@19.2.0 だけを書いて install すると react も入り、--legacy-peer-deps を付けると react は入らなかった。
devDependencies に vitest: 5.0.5 を書いた最小のプロジェクトで試した。
| プロジェクトの状態 | pnpm 10.20.0 | npm 10.9.8 |
|---|---|---|
| vite を書いていない | 範囲に合う vite 8.3.6 が入る | — |
package.json に範囲外の vite: 5.4.21 を書いた |
2つ目は入れず 5.4.21 を使う。install は成功し、警告だけ出る | install がエラーで止まる |
| 任意の peer(jsdom・happy-dom) | 入らない | — |
pnpm の警告(install は成功する):
WARN Issues with peer dependencies found
.
└─┬ vitest 5.0.5
├── ✕ unmet peer vite@"^6.4.0 || ^7.0.0 || ^8.0.0": found 5.4.21
└─┬ @vitest/mocker 5.0.5
└── ✕ unmet peer vite@"^6.0.0 || ^7.0.0 || ^8.0.0": found 5.4.21
npm のエラー(抜粋。install は止まる):
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error Found: vite@5.4.21
npm error Could not resolve dependency:
npm error peer vite@"^6.4.0 || ^7.0.0 || ^8.0.0" from vitest@5.0.5
まとめると次の流れになる。
flowchart TD
Start["peer の X を解決する"] --> Has{"プロジェクトに X がある?"}
Has -->|ある| InRange{"X は peer の範囲に合う?"}
InRange -->|合う| Use["その X を使う"]
InRange -->|合わない| Keep["2つ目は入れず、その X を使う<br/>pnpm: 警告だけ / npm: エラーで止まる"]
Has -->|無い| Required{"必須の peer?"}
Required -->|必須| Install["範囲に合う版を自動で入れる"]
Required -->|任意| Skip["入れない<br/>使う人が自分で入れる"]
peer は「1つを共有する」ための仕組みなので、範囲外でも2つ目は入れない。合わないことを知らせるところまでがパッケージマネージャーの仕事で、どの版にするかは利用する側が決める。
あるリポジトリで、Dependabot が vitest を 2.1.9 から 5.0.5 に上げた。package.json に vite は書いておらず、pnpm は 10 系に固定していた。更新後、テストが起動しなくなった。
| vitest | vite の書かれ方 |
|---|---|
| 2.1.9 | dependencies: ^5.0.0 |
| 4.1.0 | dependencies と peerDependencies の両方: ^6.0.0 || ^7.0.0 || ^8.0.0-0 |
| 5.0.0 以降 | peerDependencies だけ: ^6.4.0 || ^7.0.0 || ^8.0.0 |
2.1.9 のころは vitest 専用の部品として vite 5.4.21 が入り、ロックファイルに記録されていた。5.0.5 では vite が peer になり、どの版を入れるかを pnpm が選ぶことになった。
# vitest 2.1.9 で一度 install して、ロックファイルに vite 5.4.21 を残す
echo '{"private":true,"devDependencies":{"vitest":"2.1.9"}}' > package.json
npx pnpm@10.20.0 install
# vitest だけ 5.0.5 に上げて install し直す
echo '{"private":true,"devDependencies":{"vitest":"5.0.5"}}' > package.json
npx pnpm@10.20.0 install
grep -E "^ vitest@5" pnpm-lock.yaml
| pnpm | 更新後のロックファイル | install | テストの実行 |
|---|---|---|---|
| 10.20.0 | vitest@5.0.5(vite@5.4.21)(範囲外のまま) |
成功(警告だけ) | 起動前に落ちる |
| 11.12.0 | vitest@5.0.5(vite@8.3.6) |
成功(警告なし) | 通る |
10.20.0 のときのテストの実行結果(パスは省略):
⎯⎯⎯⎯⎯⎯⎯ Startup Error ⎯⎯⎯⎯⎯⎯⎯⎯
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './module-runner' is not defined by "exports" in .../node_modules/vite/package.json
vitest 5 は vite/module-runner を読み込む。これは vite 6.0.0 から公開されていて、5.4.21 には無いので起動前に落ちる。
修正の PR(pnpm/pnpm#12932)の説明によると、pnpm は自動で入れる peer の版を決めるとき、まずロックファイルやワークスペースにすでにある版(候補)を使い回そうとする。このとき:
@vitest/ui: "5.0.5" のような指定)のときだけだった。^6.4.0 || ... のような普通の範囲では、範囲を見ずに候補の中で一番新しい版を選んでいた。候補が 5.4.21 しか無かったので 5.4.21 が選ばれ、警告だけ出してロックファイルに記録されたflowchart TD
Peer["vitest 5.0.5 の peer<br/>vite 6.4.0 以上の 6・7・8 系"] --> Cand["候補:ロックファイルにある vite 5.4.21"]
Cand --> Ver{"pnpm の版"}
Ver -->|"10.20.0"| Old["範囲を見ずに候補の一番新しい版を選ぶ<br/>→ 5.4.21(警告だけ出して記録)"]
Ver -->|"11.12.0"| New["範囲に合う候補が無い<br/>→ レジストリから 8.3.6 を選ぶ"]
| 方法 | 効果 | 注意 |
|---|---|---|
| pnpm を 11.12.0 以上に上げる | 範囲に合う版を選ぶようになる(再現手順で確認済み) | 11.0.0 から .npmrc で読む設定が認証・レジストリなどに限られ、package.json の pnpm フィールドは読まなくなった。ほかの設定は pnpm-workspace.yaml へ移す |
| package.json に vite を直接書く | 版を自分で決められる。pnpm の選び方に左右されない | 範囲外の版を書けば、やはり警告だけで通る。vitest を上げるときは vite も一緒に上げる |
strictPeerDependencies を有効にする |
範囲外の peer があると install が失敗する | 後述のとおり、失敗するのは最初の1回だけ |
| 依存の更新 PR でテストを実行する | 起動しないことに気づける | 最後の安全網。今回もこれで気づいた |
pnpm の unmet peer は警告なので、install もロックファイルの更新も成功する。何が選ばれたかはロックファイルで分かる。vitest@5.0.5(vite@5.4.21) のように、括弧の中に peer として使われた版が記録される。
pnpm 10.20.0 の再現手順で、2回目の install に --config.strict-peer-dependencies=true を付けて試した。
# 1回目:終了コード 1
ERR_PNPM_PEER_DEP_ISSUES Unmet peer dependencies
# ただし、ロックファイルは vitest@5.0.5(vite@5.4.21) に書き換わったまま残る
# 2回目:終了コード 0
Lockfile is up to date, resolution step is skipped
--frozen-lockfile を付けた install も終了コード 0 だった。ロックファイルが最新なら版の解決そのものを飛ばすので、範囲外の peer が記録されていても検査されない。
pnpm config get auto-install-peers は undefined と出る10.20.0 で何も設定していないと undefined と表示されるが、既定で有効。実際に効いている値は、ロックファイル冒頭の settings.autoInstallPeers: true で分かる。
古い版が「自分専用の部品」としてロックファイルに残っていて、それが peer の候補として使い回される。依存の更新でこの移動があるときは、更新後のロックファイルで peer の版(括弧の中)を確かめる。
ロックファイルが無い状態から入れると vite 8.3.6 が入る(検証の表の1行目)。不具合はロックファイルにある版を使い回すときにだけ起きる。
jsdom を使うテストにするなら、自分で入れる。pnpm のドキュメントによると、ワークスペースのどこかに範囲に合う版がすでにあれば、それが使われる。
.npmrc・package.json の pnpm フィールドの扱いの変更auto-install-peers が既定で有効になった版autoInstallPeers・strictPeerDependencies・任意の peer の扱い