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.yaml        ← CLI 用。Turbopack はこれを見つけてルートと誤認
/web/pnpm-lock.yaml    ← web 用(本当はこっちが起点であってほしい)

web/ の中でビルドしていても、Turbopack は親方向に pnpm-lock.yaml を探し、リポジトリ直下のものを先に見つけて「ここがルートだ」と判定する。結果、.next の基準がリポジトリ直下になり、Vercel の取り違えにつながった。

足りないのではなく、ルート側に lockfile が多すぎたのが原因だった。単独リポジトリに Next.js を置いていれば絶対に起きない。「既存の Node プロジェクトの配下に、後から Next.js アプリをネストした」構成が引き金だ。

効かなかった対処(同じ罠を踏む人へ)

最初に2つ試して、どちらも症状が1ミリも変わらなかった。これ自体が大事なヒントだった。

  1. ルートの pnpm-workspace.yaml を消す → 無関係。Turbopack が見るのは workspace.yaml ではなく lockfile。
  2. 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。