Vercelの「ENOENT .next/package.json」— 犯人はNext.js 16 Turbopackのroot誤検知だった
Vercelの「ENOENT .next/package.json」— 犯人はNext.js 16 Turbopackのroot誤検知だった
1つのリポジトリに「CLI ツール(リポジトリ直下)」と「Next.js 16 アプリ(web/ 配下)」を同居させていた。Vercel の Root Directory を web に指定してデプロイしたら、ビルドは成功して見えるのに deployment だけが失敗するという不可解な状態にハマった。結論と、たどり着くまでの切り分けを残しておく。
結論から
真因は Next.js 16 の Turbopack が「ワークスペースのルート」を lockfile から推定する仕様だった。リポジトリ直下にも(CLI 用の)pnpm-lock.yaml があるせいで、web/ の中でビルドしているのに Turbopack がリポジトリ直下をルートと誤判定し、.next の置き場所を取り違える。Vercel は本来あるべき web/.next ではなくリポジトリ直下の .next を探しに行き、無いので落ちる。
直し方は1ファイル。web/next.config.ts に turbopack.root を web 自身に固定するだけ。
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
root: __dirname, // web/ に固定。リポジトリ直下の lockfile による誤推定を無効化
},
};
export default nextConfig;
症状 — ログにエラーが出ない
Vercel のビルドログは、どこまで見ても成功にしか見えない。
✓ Compiled successfully
Build Completed in /vercel/output [29s]
status ● Error ← なぜ?
✓ Compiled successfully も TypeScript チェックもページ生成も通っているのに、最後だけ Error。ログ本文には一行もエラーが無い。 ここで手が止まる人は多いと思う。
切り分け — ログに無いなら API から取る
Vercel のビルドログに出ないエラーは、デプロイオブジェクトの errorMessage フィールドに入っている。REST API で直接取りに行く。
curl -s "https://api.vercel.com/v13/deployments/<deployment_id>" \
-H "Authorization: Bearer $VERCEL_TOKEN" | jq '{readyState, errorCode, errorMessage, errorStep}'
返ってきた実体:
errorStep: buildStep
errorCode: ENOENT
errorMessage: ENOENT: no such file or directory, lstat '/vercel/path0/.next/package.json'
/vercel/path0 はリポジトリのルート。ビルドは /vercel/path0/web で走って web/.next を作っているのに、Vercel は ルートの .next を見ている。プロジェクト設定(framework=nextjs、rootDirectory=web)は正しいのに、だ。
Build Completed は「next build が web で成功した」という意味でしかなく、その後の成果物を集める段でルートを取り違えて落ちていた。これが「ビルド成功に見えるのに deployment は失敗」のからくり。
なぜルートを見てしまうのか
Next.js 16 の公式ドキュメント(Turbopack の Root directory)にこう書いてある。Turbopack は次のファイルを上方向に探して、見つかった場所をプロジェクトのルートと判定する:
pnpm-lock.yamlpackage-lock.jsonyarn.lockbun.lock/bun.lockb
今回の構成はこうだった。
/pnpm-lock.yaml ← CLI 用。Turbopack はこれを見つけてルートと誤認
/web/pnpm-lock.yaml ← web 用(本当はこっちが起点であってほしい)
web/ の中でビルドしていても、Turbopack は親方向に pnpm-lock.yaml を探し、リポジトリ直下のものを先に見つけて「ここがルートだ」と判定する。結果、.next の基準がリポジトリ直下になり、Vercel の取り違えにつながった。
足りないのではなく、ルート側に lockfile が多すぎたのが原因だった。単独リポジトリに Next.js を置いていれば絶対に起きない。「既存の Node プロジェクトの配下に、後から Next.js アプリをネストした」構成が引き金だ。
効かなかった対処(同じ罠を踏む人へ)
最初に2つ試して、どちらも症状が1ミリも変わらなかった。これ自体が大事なヒントだった。
- ルートの
pnpm-workspace.yamlを消す → 無関係。Turbopack が見るのはworkspace.yamlではなく lockfile。 outputFileTracingRootを書き換える → 無関係。Next.js 16 では root 推定の主役がturbopack.rootに移っており、outputFileTracingRoot(バンドルに含めるファイルの収集基準)は別系統。
「いじっても症状が全く変わらないノブ」は、それが効くノブではない、というシグナル。 反応しない場所を触り続けるより、「では何が効くのか」を公式ドキュメントで確認する方が速い。
持ち帰れる原則
単独で動くはずの Next.js アプリを、独自の lockfile を持つ別プロジェクトの配下にネストすると、Turbopack がワークスペースのルートを誤検知する。 Next.js 16 でルートを固定するノブは
turbopack.root(outputFileTracingRootではない)。
デプロイの「直った」を、確認せずに口にしない。 ログに出ないエラーは API から実体を取り、修正を push したら、デプロイが本当に成功するまで状態を見届けてから結論を出す。
補足:Vercel の Root Directory 設定自体は今回正しかった(変更不要)。直すべきは Vercel 側ではなく、web/ の中で走る next build(Turbopack)の探索範囲の方だった。「どちら側の問題か」を取り違えると、設定画面を延々いじって直らない時間が生まれる。
出典:Next.js 公式 — turbopack config / Root directory、output config、Vercel 公式 — Configuring a Build。