Back to Blog
4 min read

Why I Upgraded Vite in a Laravel Project — And What Broke Along the Way

The Upgrade That Seemed Routine

I run a Laravel-based starter kit called DataAnno Fil Starter — a lean, opinionated foundation for full-stack apps with modern tooling. When Vite 7 dropped, I figured: "Just bump the version, run the tests, ship it." Famous last words.

The upgrade wasn’t just about new features. I wanted better tree-shaking, improved TypeScript support, and faster cold starts. But more than that, I needed to stay current. Outdated tooling becomes tech debt fast, especially in starter kits where hygiene sets the tone for downstream projects.

So I ran the numbers:

npm install vite@^7 laravel-vite-plugin@^1.0

Simple, right? Wrong.

The build passed. The dev server started. But HMR stopped working. Assets 404’d in dev. And the production manifest was missing half its entries. I was flying blind — until I dug deeper.

What Actually Broke (And Why)

HMR Died Silently

The first red flag: editing a Vue component no longer triggered hot reloads. The browser didn’t refresh. No errors in the console. Vite’s terminal log showed file changes, but no updates pushed.

Turns out, Vite 7 tightened how it handles server origins and WebSocket connections. My vite.config.js was using an older pattern:

export default defineConfig({
  plugins: [
    laravel([/* ... */]),
  ],
  server: {
    host: 'localhost',
    hmr: {
      host: 'localhost'
    }
  }
})

But with Laravel’s dev server typically running on http://localhost:8000, and Vite serving assets on http://localhost:5173, the HMR client was trying to connect to ws://localhost:5173 — which got blocked by mixed-content policies or CORS-like restrictions in the browser's WebSocket handshake.

The fix? Explicitly set the HMR client protocol to match Laravel’s server:

server: {
  host: 'localhost',
  hmr: {
    protocol: 'ws',
    host: 'localhost',
    port: 5173
  }
}

This forced the client to use ws:// instead of inferring wss://, aligning with my local dev setup.

Manifest Generation Went Quiet

Next: production builds. The manifest.json was generated, but only half the chunks were listed. Critical CSS and async JS bundles were missing. This meant Laravel’s @vite directive couldn’t resolve them — resulting in broken asset links in production.

After isolating the config, I realized the issue wasn’t with my code — it was a breaking change in how laravel-vite-plugin v1 handles dynamic imports and entry points. The plugin now respects Vite’s internal chunking logic more strictly, but that meant dynamically imported modules weren’t being registered as top-level entries.

I had two options:

  1. Manually declare all dynamic imports as entries (not scalable)
  2. Re-architect how I split bundles

I chose door #2. I consolidated lazy-loaded chunks into named entry points and updated the plugin config:

laravel({
  input: [
    'resources/js/app.js',
    'resources/js/admin.js',
    'resources/css/app.css'
  ],
  refresh: true,
})

This gave Vite clearer boundaries and ensured every entry was tracked in the manifest.

Lessons Learned (And How I’ll Avoid This Next Time)

1. Never Assume Minor = Safe

Vite 6 to 7 is a minor version bump, but it included architectural shifts in asset handling and HMR. The laravel-vite-plugin also moved from v0 to v1 — which should’ve been a hint. Major version jumps in ecosystem plugins are rarely just marketing.

2. Test HMR Like You Test Builds

I had CI check for successful builds and lint passes — but nothing verifying HMR actually works. Now, I run a smoke test that spins up the dev server, edits a file via script, and checks for a WebSocket update event. It’s not perfect, but it catches 80% of HMR regressions.

3. Lock Down Dependency Pairs

Vite and laravel-vite-plugin are tightly coupled. Going forward, I treat them like a single unit. My package.json now includes a comment:

"devDependencies": {
  "vite": "^7.0.0",
  "laravel-vite-plugin": "^1.0.0" // Must match Vite v7
}

And I use npm audit-style checks in CI to flag mismatched pairs.

4. Fall Back to Mix Patterns When Stuck

When debugging, I borrowed a trick from Laravel Mix: isolate the asset pipeline. I temporarily hardcoded asset URLs in Blade templates and bypassed @vite to confirm the issue was tooling, not routing. That clarity saved hours.

Final Thoughts

Upgrading Vite 6 to 7 in a Laravel project wasn’t just a version bump — it was a stress test of my assumptions. HMR, asset resolution, and manifest integrity all broke in subtle ways. But the payoff? Faster builds, cleaner output, and a more maintainable foundation for HomeForged and other tools in the pipeline.

If you’re running Laravel with Vite, don’t delay the upgrade — but don’t rush it either. Test HMR. Verify your manifest. And treat laravel-vite-plugin like the critical dependency it is.

Because in full-stack PHP/JS projects, the build tool isn’t just plumbing — it’s the bridge between worlds.

Newer post

How I Stabilized HomeForged After a Major Refactor Without Breaking Production

Older post

Debugging the Tree: How I Restored Hierarchical State in HomeForged’s Visual Builder