skip to content

How does Laravel's Vite class choose between the dev server and built assets, and what breaks when public/hot or the manifest is wrong?

level: middleimportance: must knowfreq 46%

answer

  1. is_file on the hot file
  2. hot file holds the dev server URL
  3. plugin deletes it on exit
  4. ViteManifestNotFoundException vs ViteException
  5. manifest cached in a static array

basics

~20 s

Laravel's Vite class calls isRunningHot(), which is just is_file() on public/hot. If the file exists, every tag points at the URL inside it; otherwise tags come from public/build/manifest.json. A stale hot file or a missing manifest breaks every page.

solid answer

~40 s

`Vite::isRunningHot()` returns `is_file(public_path('hot'))` unless you changed the path with `Vite::useHotFile()` or the plugin's `hotFile` option. laravel-vite-plugin writes the dev server URL into that file when the server starts listening and deletes it when the process exits, so a hard-killed process or a copied-up file leaves it behind — and then production HTML points at `localhost:5173`, which visitors cannot reach. Without a hot file, `Vite` reads `public/build/manifest.json` (build directory `build`, file `manifest.json`, both overridable); a missing file throws `ViteManifestNotFoundException`, and an entry that is not a manifest key throws `ViteException`. The decoded manifest is cached in a static array per PHP process, which only matters for long-lived workers.

code

php · 13 lines
php
<?php

use Illuminate\Support\Facades\Vite;

// true only while a hot file exists at Vite::hotFile()
Vite::isRunningHot();

// the URL laravel-vite-plugin wrote into the hot file, or null
Vite::devServerUrl();

// versioned URL from public/build/manifest.json;
// throws ViteException if the key is missing
Vite::asset('resources/js/app.js');

go deeper

for a junior

Remember that public/hot means dev server and public/build/manifest.json means built assets; both are gitignored.

for a middle

Walk through isRunningHot, who writes and deletes the hot file, the manifest keys, and which exception each broken state throws.

for a senior

Diagnose unstyled production pages from the HTML: dev-server URLs mean a stale hot file, an exception page means a missing build or entry.

for a principal

Push the build into the pipeline so servers never run npm, and treat the manifest as a release artifact tied to one code version.

## The decision is one file check `Illuminate\Foundation\Vite` keeps no configuration file of its own. Every call — `@vite`, `Vite::asset()`, `@viteReactRefresh` — starts from `isRunningHot()`: - `hotFile()` returns the custom path set by `Vite::useHotFile()`, or `public_path('/hot')`. - `isRunningHot()` is literally `is_file($this->hotFile())`. - `devServerUrl()` returns the file's contents, trimmed, or `null` when not hot. So the **hot file** is a flag and a message at once: its existence says "a dev server is running", and its body says where. ## Who writes and removes the hot file **laravel-vite-plugin** owns the file's lifecycle during `npm run dev`: 1. When the dev server starts listening, the plugin resolves the reachable URL (honouring `server.origin`, HMR host and port, TLS from Herd or Valet) and writes it, plus any base path, into the hot file. 2. It registers exit handlers: on normal exit, `SIGINT`, `SIGTERM` or `SIGHUP`, it deletes the file. 3. The file's default location is `${publicDirectory}/hot`; the `hotFile` option moves it, and the PHP side must then be told with `Vite::useHotFile()`. The skeleton's `.gitignore` lists `/public/hot`, so it should never travel with the code. It still escapes in three common ways: - the dev server was killed hard (`kill -9`, a crashed container) so the exit handler never ran; - a deploy copies the working directory by `rsync` or archive from a laptop where `npm run dev` was running; - someone ran `npm run dev` on a server. The plugin refuses to start the dev server when it sees `LARAVEL_FORGE`, `LARAVEL_VAPOR`, `LARAVEL_ENVOYER` or `CI` in the environment, unless `LARAVEL_BYPASS_ENV_CHECK=1` is set. With a stale hot file, production HTML contains `<script type="module" src="http://[::1]:5173/@vite/client">` and entry URLs on the same origin. Visitors' browsers cannot reach them, so the page renders unstyled with dead JavaScript, and the server logs show nothing wrong. ## The manifest path When the hot file is absent, the class reads the **manifest**: | Piece | Default | Change it with | |---|---|---| | build directory | `build` (under `public`) | `Vite::useBuildDirectory()`, second `@vite` argument, plugin `buildDirectory` | | manifest file name | `manifest.json` | `Vite::useManifestFilename()` | | full path | `public/build/manifest.json` | the two above | The plugin sets Vite's `build.manifest` to `manifest.json` and `outDir` to `public/build`, so the file lands where the PHP side expects rather than at Vite's own default location. Each manifest key is a **source path**; each value holds the output `file`, its `css`, its static `imports` and its `dynamicImports`. `@vite` walks `imports` recursively to emit preload links; `Vite::asset()` returns the `file` URL through Laravel's `asset()` helper, so `ASSET_URL` prefixes it. ## The two failure exceptions - **`ViteManifestNotFoundException`** — "Vite manifest not found at: …". No hot file and no manifest: the build never ran on this machine, ran into a different directory, or the deploy excluded `public/build`. - **`ViteException`** — "Unable to locate file in Vite manifest: …". The manifest exists but lacks the key: the name passed to `@vite` or `Vite::asset()` is not an `input` (or `assets`) entry, has a typo, or the manifest is from an older build that predates the entry. `ViteManifestNotFoundException` extends `ViteException`, so a handler that catches the parent sees both. ## The per-process cache The decoded manifest is stored in a **static** `$manifests` array keyed by path, so a page with several lookups decodes the JSON once. Under PHP-FPM each request starts a fresh process state, so a new build is picked up immediately. Under a long-lived worker such as Octane, the first decoded manifest stays in memory until the worker restarts; Octane's `FlushVite` listener resets preloaded assets and fonts between requests but not that static cache. ## Reading the symptom The three broken states look different from the outside, which makes them quick to tell apart: - **Stale hot file**: the page renders with HTTP 200 but no styles and no working JavaScript; view-source shows dev-server URLs; the browser console shows failed connections, not 404s. - **Missing manifest**: the page itself fails with an exception page (in debug mode) or a 500, because the directive throws during rendering. - **Missing entry**: the same exception class family, but the message names the source path that was not found, pointing straight at the `input` list or a typo. ## Checks worth doing - `ls public/hot` on a misbehaving server: if it exists in production, delete it. - `php artisan tinker` then `Vite::isRunningHot()` and `Vite::devServerUrl()` to see what the app believes. - Compare the `@vite` names with `input` in `vite.config.js`.

  • What does the plugin's ssr option change about where the build lands and which manifest is written?
    With `ssr: 'resources/js/ssr.js'`, running `vite build --ssr` compiles that entry into `bootstrap/ssr` (the `ssrOutputDirectory` default) instead of `public/build`, and writes `ssr-manifest.json` rather than `manifest.json`. The server bundle never lands under `public`, so browsers cannot download it, and `@vite` never reads it.
  • Why would a fresh build go unnoticed by an app running under Octane but be picked up at once under PHP-FPM?
    The `Vite` class caches the decoded manifest in a static array per process. PHP-FPM discards that state after each request, so the next request reads the new file. An Octane worker keeps its static array for its whole life, so it keeps emitting the old hashed names until the workers are reloaded.

The hot file is a note on an office door saying the owner is working at desk 12 today. Visitors trust the note without checking; if the owner leaves without taking it down, everyone keeps walking to an empty desk.

saying these in an interview costs you the question

  • Laravel reads APP_ENV to decide whether to use the Vite dev server
  • The hot file is created by php artisan serve
  • A missing manifest makes @vite fall back to unhashed files in public/js
  • Committing public/hot is harmless because production ignores it
  • Every request re-reads the manifest from disk even under long-lived workers