はじめに
Cloudflare Workers へのデプロイ時、ビルドが npm ci の段階で失敗しました。
npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync. Please update your lock file with `npm install` before continuing.
npm error Missing: typescript@5.9.3 from lock file
ローカルでは npm install も npm run build も通っているのに、CI だけが止まる状態です。
この記事は解決方法の紹介ではなく、原因を突き止めるまでに使ったコマンドと、その出力の読み方をまとめたものです。同種のエラーは原因がプロジェクトごとに違うため、「何を打てば犯人が分かるか」を知っておくほうが応用が利くと考えました。
対象読者は、npm ci 関連のエラーに遭遇したが、どこから調べればいいか分からない方です。
問題
環境
- Windows 11 / PowerShell
- Node.js 22.16.0 / npm 10.9.2
- Vite + React + Vitest
- デプロイ先:Cloudflare Workers
エラー全文
2026-07-28T12:26:24.770929Z Installing project dependencies: npm clean-install --progress=false
2026-07-28T12:26:27.048687Z npm error code EUSAGE
2026-07-28T12:26:27.049104Z npm error `npm ci` can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync.
2026-07-28T12:26:27.049196Z npm error Missing: typescript@5.9.3 from lock file
何が分からなかったか
package.json の TypeScript 指定はこうなっています。
"typescript": "~6.0.2"
~6.0.2 は >=6.0.2 <6.1.0 の意味で、5.9.3 はこの範囲に入りません。自分が書いた指定からは絶対に出てこない数字が要求されている、という点が最初の謎でした。
「Node のバージョンが違う環境で作った lock を別環境で npm ci したせいか」とも考えましたが、これも後述の調査で否定されます。
解決方法
※前述の通り、この章では原因特定に使ったコマンドと出力の読み方を扱います。
前提:npm ci と npm install の違い
なぜローカルは通って CI だけ落ちるのか。両者は挙動が根本的に違います。
npm install |
npm ci |
|
|---|---|---|
| lock ファイル | 必要なら書き換える | 読むだけ。絶対の正とする |
| 不整合時 | 解決してインストール | エラーで停止 |
| node_modules | 差分更新 | 最初に全削除してから再構築 |
npm install は不整合を裏で吸収するため問題が表面化しません。npm ci は妥協しないので、そこで初めて露呈します。「ローカルで動く」は CI が通る保証にならない、というのが出発点になります。
コマンド1:npm ls typescript
依存ツリーの中で、TypeScript がどこに・どのバージョンで存在するかを表示します。今回はこのコマンドの出力に答えが全部書かれていました。
npm ls typescript
出力(抜粋)
mahjong-scoring@0.0.0 C:\work\20260702_mahjong_scoring
├─┬ typescript-eslint@8.63.0
│ ├─┬ @typescript-eslint/eslint-plugin@8.63.0
│ │ ├─┬ @typescript-eslint/type-utils@8.63.0
│ │ │ └── typescript@6.0.3 deduped invalid: "^5.0.0" from node_modules/vite-tsconfig-paths/node_modules/tsconfck
│ │ ├─┬ ts-api-utils@2.5.0
│ │ │ └── typescript@6.0.3 deduped invalid: "^5.0.0" from node_modules/vite-tsconfig-paths/node_modules/tsconfck
│ │ └── typescript@6.0.3 deduped
...
├── typescript@6.0.3
└─┬ vite-tsconfig-paths@6.1.1
└─┬ tsconfck@3.1.6
└── typescript@6.0.3 deduped invalid: "^5.0.0" from node_modules/vite-tsconfig-paths/node_modules/tsconfck
npm error code ELSPROBLEMS
npm error invalid: typescript@6.0.3 C:\work\20260702_mahjong_scoring\node_modules\typescript
見方
出力が長いので圧倒されますが、注目すべきキーワードは3つだけです。
deduped
同じパッケージを上位(node_modules/typescript)で共有している、という意味。実体は1つで、そこを参照しているだけ。正常な状態を示す語なので、これ単体は問題ではありません。
invalid: "^5.0.0" from <パス> ← ここが本命
分解するとこうなります。
-
"^5.0.0"… 要求されているバージョン範囲 -
from node_modules/vite-tsconfig-paths/node_modules/tsconfck… その要求を出している者 -
invalid… 実際に置かれている 6.0.3 がその範囲外
^5.0.0 は >=5.0.0 <6.0.0 なので、6.0.3 は満たしません。**「tsconfck が TypeScript 5 系を要求しているのに、共有されている実体は 6.0.3」**と npm が明示してくれています。
ELSPROBLEMS
npm ls が問題を検出して非ゼロ終了しただけのコード。別種のエラーが追加で起きたわけではないので、慌てなくて大丈夫です。
ここで分かったこと
CI が要求してきた 5.9.3 の出どころが、自分の package.json ではなく tsconfck の ^5.0.0 という要求だと判明しました。5.9.3 は ^5.0.0 を満たす最新版として npm が選んだ数字だったわけです。
補足オプション
npm ls typescript --all # 深さ制限を外して全階層を表示
コマンド2:npm why tsconfck
tsconfck は入れた覚えがないパッケージだったので、どこから来たのかを逆引きします。
npm why tsconfck
出力
tsconfck@3.1.6 dev
node_modules/vite-tsconfig-paths/node_modules/tsconfck
tsconfck@"^3.0.3" from vite-tsconfig-paths@6.1.1
node_modules/vite-tsconfig-paths
dev vite-tsconfig-paths@"^6.1.1" from the root project
見方
下から上に読みます。
- 最下行
from the root project… 自分のpackage.jsonにvite-tsconfig-paths@"^6.1.1"が書かれている - その上
tsconfck@"^3.0.3" from vite-tsconfig-paths@6.1.1… そのvite-tsconfig-pathsがtsconfckを連れてきた - 最上行
tsconfck@3.1.6 dev… 結果として 3.1.6 が入った。devは開発時のみ使う依存の意味
図にするとこうなります。
自プロジェクト
└ vite-tsconfig-paths@6.1.1 ← 自分で入れた(直接依存)
└ tsconfck@3.1.6 ← 上が連れてきた(推移的依存)
└ typescript ^5.0.0 を要求 ← 衝突の原因
ここで分かったこと
犯人は1人だけと確定しました。tsconfck は自分で選んだものではなく vite-tsconfig-paths の付属品なので、直接バージョンを操作することはできません。手を入れるべきは1つ上の vite-tsconfig-paths だと分かります。
分岐が複数あると対応が複雑になりますが、今回は単線だったので判断が楽でした。
全体像の整理
ここまでの調査で、なぜ CI だけ落ちるのかがつながりました。
-
ローカルの npm は妥協した。
node_modules/typescriptに 6.0.3 を1つだけ置き、tsconfck の要求にはinvalidの印を付けて済ませた。だから動いているように見えた -
CI の
npm ciは妥協しない。「tsconfck の下に TypeScript 5 系をネストして置くべき」と理想ツリーを再計算する。ところが lock ファイルにその5.9.3のエントリが存在しない →Missing: typescript@5.9.3 from lock file
つまり本質は、同じ lock ファイルをローカルと CI が別々に解釈していたことでした。lock ファイルが「妥協した状態」で作られていたため、CI が求める情報が欠けていたわけです。
なお「エントリ」とは、lock ファイル内の1パッケージ分のレコードのこと。packages オブジェクトのキーと値のペアを指します。
"packages": {
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-...",
"dev": true
}
}
npm ci は package.json から理想ツリーを計算し、その各要素に対応するエントリが lock にあるかを突き合わせます。無ければ Missing: X@version from lock file となります。
おわりに
今回使ったコマンドを、目的別に整理します。
| 目的 | コマンド | 見るべき箇所 |
|---|---|---|
| どこに何が入っているか | npm ls <pkg> |
invalid: "範囲" from パス |
| なぜ入っているのか | npm why <pkg> |
下から上に読む依存の連鎖 |
得られた教訓は以下です。
-
npm lsのinvalid表示に答えが書いてある。要求バージョン範囲と要求元のパスがそのまま出力されるので、エラーメッセージより情報量が多い -
npm whyで依存の連鎖をたどる。身に覚えのないパッケージ名が出てきたら、まずこれ -
推移的依存の衝突は
package.jsonを見ても分からない。~6.0.2と書いてあるのに5.9.3を要求される、という一見矛盾した状況が起きる -
エラーメッセージの数字を、自分の書いた指定と突き合わせる。今回は「
~6.0.2の範囲に5.9.3は入らない」という気づきが、犯人が他にいると判断する最初の手がかりになった -
デプロイ前にローカルで
npm ciを通す。npm installで開発し、コミット前にnpm ciで検証する流れにすれば、この種の CI 落ちは事前に検出できる
TypeScript 6 のようなメジャーバージョンが出た直後は、周辺エコシステムが追随しきっておらず、この種の衝突が起きやすくなります。--force や --legacy-peer-deps で押し切るとローカルだけ動いて CI で落ちる状態を作り込むことになるので、まずは原因の特定を優先するのが結局は近道でした。
参考
- npm ci | npm Docs
- npm ls | npm Docs
- npm explain (why) | npm Docs
- package-lock.json | npm Docs
- dominikg/tsconfck - GitHub
JISOUのメンバー募集中!
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
興味のある方は、ぜひホームページをのぞいてみてください!
▼▼▼
https://projisou.jp