Laravel の Vite manifest not found を原因別に切り分ける
デプロイ直後に Vite manifest not found が出たとき、検索して見つかる答えはほぼ npm run build の一択である。だが Docker で 6 ケースを実測したところ、ビルドで直るのは 4 つある原因のうち 1 つだけだった。
検証環境
- Laravel 13.23.0
- PHP 8.4.24
- Node 22.23.2
- Cases reproduced 6 — two of them return HTTP 200
- Key finding npm run build does not remove public/hot
- OS Docker Desktop (php:8.4-cli-bookworm)
デプロイ直後に Vite manifest not found が出たとき、検索して見つかる答えはほぼ npm run build の一択である。だが Docker で 6 ケースを実測したところ、ビルドで直るのは 4 つある原因のうち 1 つだけだった。
npm run buildを実行してください。
さらに厄介なのは、4 つの原因のうち 2 つはエラーを一切出さないことである。HTTP 200 が返り、ログにも何も残らず、画面だけが崩れる。
ビルドは実際に走らせている。manifest.json を手書きしてそれらしく見せてはいない。
スクリプトは laravel-error-lab に置いてある。
検証環境
| Laravel | 13.23.0 |
| PHP | 8.4.24 |
| Node | 22.23.2 |
| 実行環境 | Docker Desktop / php:8.4-cli-bookworm |
正常系から一度に 1 つだけ、ディスク上の状態を変えて計測している。
6 ケースの実測結果
| ケース | 変更点 | HTTP | 例外 |
|---|---|---|---|
| A | (正常系) | 200 | — |
| B | public/build ごと削除 |
500 | ViteManifestNotFoundException |
| C | public/hot が残っている(build はある) |
200 | なし |
| D | public/hot が残っていて build も無い |
200 | なし |
| E | manifest が public/build/.vite/ にだけある |
500 | ViteManifestNotFoundException |
| F | @vite() が manifest に無いエントリを参照 |
500 | ViteException |
npm run build が答えになるのは B だけである。
例外は 2 種類ある — 混同されている
「manifest 関連のエラー」とひとくくりにされがちだが、投げられるクラスもメッセージも違う。
B / E:
Illuminate\Foundation\ViteManifestNotFoundException
Vite manifest not found at: /app/public/build/manifest.json
F:
Illuminate\Foundation\ViteException
Unable to locate file in Vite manifest: resources/js/does-not-exist.js.
F は「manifest は読めた。だがその中に指定されたエントリが無い」という意味で、原因も対処もまったく別である。@vite() に書いたパスと vite.config.js の input が食い違っているときに出る。ビルドし直しても直らない。
エラーメッセージを最後まで読めば区別できるが、どちらも「Vite manifest」で始まるので、検索するとき同じ言葉になってしまう。
B と E は見分けがつかない
E は manifest ファイル自体は存在している。public/build/.vite/manifest.json にある。それでもエラーは B と一字一句同じで、public/build/manifest.json が無いと言われる。
Laravel が見る場所は固定である。
Illuminate/Foundation/Vite.php
56: protected $manifestFilename = 'manifest.json';
975: protected function manifestPath($buildDirectory)
977: return public_path($buildDirectory.'/'.$this->manifestFilename);
laravel-vite-plugin を使っていれば manifest は public/build/manifest.json に置かれる。素の Vite(Vite 5 以降)は .vite/manifest.json に出力するので、プラグインを外した / 独自の Vite 設定を組んだ場合にこの状態になる。
「ファイルはあるのに無いと言われる」ので、パスを実際に確認するまで気付けない。
ls -la public/build/manifest.json public/build/.vite/manifest.json
最も厄介なのは C — エラーが出ない
ここが本題である。
public/hot というファイルが残っていると、HTTP 200 が返り、例外は一切発生しない。ページは正常にレンダリングされる。
そのページが読み込もうとしているものはこれである。
<script type="module" src="http://127.0.0.1:5173/@vite/client"></script>
<link rel="stylesheet" href="http://127.0.0.1:5173/resources/css/app.css" />
<script type="module" src="http://127.0.0.1:5173/resources/js/app.js"></script>
127.0.0.1:5173 は Vite の開発サーバである。本番にそんなものは動いていない。ブラウザから取りに行くと:
http://127.0.0.1:5173/@vite/client -> connection refused (nothing is listening)
サーバのログには何も残らない。ステータスは 200。監視も素通りする。 見えるのは「なぜか CSS が当たっていないページ」だけである。
なぜこうなるのか
Illuminate/Foundation/Vite.php
239: return $this->hotFile ?? public_path('/hot');
1223: return is_file($this->hotFile());
isRunningHot() は public/hot というファイルが存在するかどうかだけを見る。中身も、開発サーバが実際に生きているかも確認しない。
true が返れば、manifest は一切読まれない。だから D(hot ファイルあり + build 無し)でも 200 が返る。ビルド成果物の有無は関係なくなる。
public/hot は npm run dev が作る。正常に終了すれば消えるが、Ctrl+C の効かない止め方をしたり、プロセスが強制終了されたりすると残る。それがそのまま git add されてデプロイに乗ることもある。
npm run build はこれを直せない
定番の助言が効かないことを実測した。
after 'npm run build' with a hot file present: STILL THERE
ビルドは public/hot を削除しない。 何度ビルドし直しても状況は変わらない。必要なのはこれである。
rm public/hot
public/hot は .gitignore に入れておくべきファイルでもある。Laravel の標準 .gitignore には含まれているが、独自に書き換えている場合は確認する価値がある。
切り分け手順
ディスクを 3 回見るだけで、どのケースかが決まる。
# 1. hot ファイルが残っていないか(残っていたらこれが原因。エラーは出ない)
ls -la public/hot
# 2. manifest がどこにあるか
ls -la public/build/manifest.json public/build/.vite/manifest.json
# 3. manifest の中身と @vite() の指定が一致しているか
cat public/build/manifest.json | head -20
| 見えたもの | ケース | 対処 |
|---|---|---|
public/hot が存在する |
C / D | rm public/hot |
public/build/manifest.json が無い |
B | npm run build |
.vite/manifest.json にだけある |
E | laravel-vite-plugin を使うか、出力先を合わせる |
| manifest はあるがエントリが無い | F | @vite() のパスと vite.config.js の input を一致させる |
エラーが出ていないのに CSS が効かない場合は、まず 1 を見る。 500 が出ている場合はメッセージのクラス名まで読む — ViteManifestNotFoundException なら 2、ViteException なら 3 である。
デプロイで再発させないために
C は「開発中のファイルが本番に紛れ込む」タイプの事故なので、デプロイ側で潰せる。
# デプロイスクリプトの、ビルドの前後どちらかに
rm -f public/hot
1 行で、C と D の両方が構造的に起きなくなる。
よくある質問
Q. npm run build を実行しても直りません
public/hot が残っている可能性が高いです。ビルドはこのファイルを削除しません(実測済み)。ls -la public/hot で確認し、あれば rm public/hot してください。
このケースはそもそも Vite manifest not found が出ません。HTTP 200 が返り、CSS と JS だけが読み込まれない状態になります。
Q. public/hot とは何ですか
npm run dev が作るファイルで、中身は Vite 開発サーバの URL です。Laravel はこのファイルが存在するかどうかだけを見て、あれば manifest を読まずに開発サーバへ向けます(Vite.php:1223)。開発サーバが実際に動いているかは確認しません。
npm run dev が正常終了すれば消えますが、強制終了すると残ります。
Q. manifest.json はあるのに「見つからない」と言われます
Laravel が見るのは public/build/manifest.json の 1 か所だけです(Vite.php:977)。素の Vite 5 以降は .vite/manifest.json に出力するため、laravel-vite-plugin を使っていない場合に食い違います。
ls -la public/build/.vite/manifest.json で確認してください。エラーメッセージは「ファイルが無い」場合と完全に同一なので、パスを見るまで区別できません。
Q. Unable to locate file in Vite manifest は同じ問題ですか
違います。こちらは ViteException で、manifest の読み込み自体は成功しています。@vite() に書いたパスが manifest の中に無いという意味です。
@vite(['resources/js/app.js']) のパスと vite.config.js の input が一致しているかを確認してください。ビルドし直しても直りません。
Q. 本番で急にスタイルが崩れましたが、エラーは何も出ていません
public/hot を疑ってください。このファイルがあると HTTP 200 が返り、例外も出ず、サーバのログにも何も残りません。ブラウザの開発者ツールで、アセットの取得先が localhost:5173 になっていないか確認するのが早い判別法です。
再現
git clone https://github.com/codelift-dev/laravel-error-lab
cd laravel-error-lab
docker compose build
docker compose run --rm lab bash vite-manifest.sh # 6 ケースの再現
docker compose run --rm lab bash vite-manifest-detail.sh # 例外の正確な文言
Docker 内で完結する。ホストに PHP も Node も要らない。
Laravel 13.23.0 / Node 22.23.2 で確認している。Vite.php の行番号は将来ずれる可能性があるが、isRunningHot() が hot ファイルの存在だけを見る構造と、manifest のパスが public/build/manifest.json 固定である点は Laravel 9 以降変わっていない。
関連記事
- Laravel の 419 Page Expired は原因を特定できる 419 Page Expired の対処法を検索すると、だいたい同じリストが出てくる。「@csrf を確認」「セッションドライバを確認」「php artisan config:clear」「APP_KEY を確認」——。
- Laravel 12→13 アップグレードを実際に走らせて検証した Laravel 12 から 13 へのアップグレード記事は既に大量にあるが、そこに書かれている注意点の多くは実際に走らせずに書かれている。Docker で本物の Laravel 12 アプリを 13 に上げて実測したところ、広く言われている 4 つの主張のうち 3 つが再現しなかった。