vite-patterns
Use when configuring or optimizing Vite (laravel-vite-plugin) asset bundling in a Laravel app — entrypoints, the @vite Blade directive, HMR, env vars, aliases, manifests, code splitting, and productio
Files1
View on GitHubSKILL.md.claude/skills/vite-patterns/SKILL.md
GitHubConstraints
- Apply
@rules/laravel/laravel.md - Apply
@rules/laravel/livewire.mdand@rules/laravel/filament.mdwhen bundling assets for those layers - Apply
@rules/php/core-standards.mdfor any PHP touched (Blade config exposure, service providers) - This stack uses
laravel-vite-pluginonly. Never introduce React/Vue plugins, SSR frameworks, library mode, Bun, or Next.js. - Secrets never go into
VITE_-prefixed vars — those are inlined into the public bundle. - Keep examples to
npm,php artisan serve, andnpm run dev/npm run build.
Scope
Asset bundling and dev-server patterns for Laravel apps using the official
laravel-vite-plugin. Covers vite.config.js, the @vite Blade directive,
HMR in development, env vars, aliases, manifests, production builds, code
splitting, and bundling for Livewire / Filament / Alpine.
Modes
This skill runs in one of two modes, selected by the caller via MODE (default configure):
configure(default) — full Vite work: editvite.config.js, wire entrypoints and the@vitedirective into Blade, set up HMR, aliases, env exposure, the manifest, and the production build. Every section below behaves as written unless it is explicitly flagged forMODE=cr.cr(read-only lens — invoked by@skills/code-review/SKILL.md,code-review-github,code-review-jira, andcode-review-bugsnagwhen the diff touches an asset build surface) — never modify a config file, a Blade layout, or any other project file, never author a test, never stage / commit / push, never run fixers or checkers, and never chain a follow-up review. Scope the analysis to the lines added or modified by the PR diff and return the findings as markdown only, carrying the reproducer fields the CR folds into its standard Critical / Moderate buckets. Every instruction below that would touch a file — add, wire, alias, split, expose, or any other such verb — is emitted as a written proposal carrying a concrete config or Blade snippet, never applied to the project.
What this lens owns in a CR: how the bundle is built and loaded — entrypoint declaration and the
@vitedirective that loads it, HMR and dev-server configuration,VITE_-prefixed env exposure,resolve.aliaspaths, the manifest and cache-busting, and code splitting in the production build. It defers the markup a view renders — component composition, state placement, accessibility semantics, and design tokens — to the three frontend lenses (@skills/frontend-patterns/SKILL.md,@skills/frontend-a11y/SKILL.md,@skills/design-system/SKILL.md), and never raises a finding one of them owns. It defers what the changed<head>promises a crawler — the canonical, the robots directive, the on-page tags, and the JSON-LD payload — to@skills/seo/SKILL.mdwithMODE=cr. Those two meet on the preload and the asset a layout loads first: this lens owns how that bundle is built and split, the SEO lens owns whether the head asks for the largest contentful element early enough, and neither restates the other. The lens judges the Vite setup the project already has. Never raise a finding whose only fix is to adopt a plugin or a framework the project does not use — React, Vue, an SSR framework, library mode, or Bun: the toolchain is a project decision, not a defect on the diff.
Use when
- Setting up or editing
vite.config.jswith thelaravel()plugin. - Wiring entrypoints and the
@vite([...])directive into Blade layouts. - Getting HMR / hot reload working in local development.
- Exposing config to the client via
VITE_env vars or Blade-side config. - Adding
resolve.aliaspaths, code splitting, or dynamic imports. - Understanding the manifest, cache-busting, and the production build.
- Bundling JS/CSS that Livewire, Filament, or Alpine depend on.
- Building assets in CI before deploy.
How it works
- Dev mode (
npm run dev) runs a Vite dev server that serves source files as native ESM and pushes HMR updates. Thelaravel-vite-pluginwrites apublic/hotfile; the@vitedirective detects it and points<script>/<link>tags at the dev server instead of built files. - Build mode (
npm run build) bundles, hashes, and writes assets topublic/build/plus amanifest.json.@vitereads the manifest and emits the hashed URLs. Cache-busting is automatic via the content hash in filenames. - Env vars prefixed
VITE_are statically inlined into the client bundle viaimport.meta.env. Everything else stays server-side.
vite.config.js — the laravel() plugin
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
],
refresh: true, // full-page reload on Blade/route/PHP changes
}),
],
});
inputlists every entrypoint. Add more for admin panels or per-section bundles (resources/js/admin.js).refresh: truetriggers a full reload when Blade views, routes, or PHP config change. Pass an array of globs to watch extra paths:
laravel({
input: ['resources/js/app.js'],
refresh: ['resources/views/**', 'app/Livewire/**'],
}),
The @vite Blade directive
Load entrypoints in your layout <head>:
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html>
<head>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
{{ $slot }}
</body>
</html>
- No
@viteReactRefreshis needed — this is a Blade/Livewire/Alpine stack, not React. Do not add it. - In dev,
@viteemits a script pointing at the running dev server. In production it resolves hashed URLs from the manifest. Same directive, both modes — you write it once. - For assets referenced from JS (images, fonts), import them so Vite fingerprints
them; for Blade-referenced static assets use
Vite::asset('resources/...').
@vite + Tailwind
Tailwind v4 compiles through its Vite plugin plus the CSS entrypoint:
// vite.config.js
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [
laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true }),
tailwindcss(),
],
});
/* resources/css/app.css */
@import "tailwindcss";
// resources/js/app.js
import './bootstrap';
@vite(['resources/css/app.css', ...]) handles HMR for Tailwind classes in dev
and outputs a hashed, purged stylesheet in the production build.
HMR / hot reload in development
Run two processes:
php artisan serve # serves the Laravel app
npm run dev # Vite dev server + HMR
- The dev server writes
public/hot. Addpublic/hotandpublic/buildto.gitignore. - Edits to JS/CSS hot-swap without a full reload; with
refreshenabled, Blade/PHP edits trigger a full-page reload. - Behind a custom domain or container, expose the host and the HMR port:
laravel({ input: ['resources/js/app.js'], refresh: true }),
// server config:
server: {
host: '0.0.0.0',
hmr: { host: 'localhost' },
},
Environment variables
Only VITE_-prefixed vars reach the client bundle via import.meta.env:
// resources/js/app.js
const apiUrl = import.meta.env.VITE_API_URL;
const mode = import.meta.env.MODE; // 'development' | 'production'
# .env
VITE_API_URL="${APP_URL}/api"
VITE_is not a security boundary — these values are inlined into the shipped JS. Put only public values (public URLs, feature flags, public keys) here. API tokens, DB credentials, and signing keys stay server-side.- For values the client needs but that depend on per-request state, prefer passing them from Blade instead of baking them at build time:
<script>
window.AppConfig = @json(['locale' => app()->getLocale(), 'csrf' => csrf_token()]);
</script>
Aliases (resolve.alias)
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
plugins: [laravel({ input: ['resources/js/app.js'], refresh: true })],
resolve: {
alias: {
'@': fileURLToPath(new URL('./resources/js', import.meta.url)),
},
},
});
Then import Foo from '@/components/Foo';. Keep the alias list small — add an
entry only when a real import path needs it.
Manifest & production build
npm run build
- Outputs hashed files to
public/build/assets/pluspublic/build/manifest.json. @vitereads the manifest to emit the correct hashed URLs — no manual versioning. The content hash in each filename is the cache-busting mechanism; changed files get new hashes, unchanged files keep theirs so browsers reuse cached copies.- Commit neither
public/buildnorpublic/hot; build assets in deploy/CI.
Code splitting & dynamic import
Vite splits dynamically imported modules into separate chunks automatically:
// load a heavy module only when needed
button.addEventListener('click', async () => {
const { renderChart } = await import('./chart.js');
renderChart(data);
});
Group stable vendor code into its own chunk to improve cache reuse across deploys:
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['alpinejs', 'axios'],
},
},
},
},
Avoid splitting every dependency into its own chunk — that produces many tiny requests. Group by stability instead.
Prefetching
For routes/modules likely needed soon, hint the browser with a dynamic import behind an idle callback so the chunk is fetched ahead of interaction:
requestIdleCallback?.(() => import('./chart.js'));
This warms the chunk cache without blocking the initial render.
Bundling for Livewire / Filament / Alpine
- Alpine: in a Livewire app, Livewire already bundles and starts Alpine — register custom
Alpine components in an
alpine:initlistener and never import or start a second copy. Only on a view that loads no Livewire, import Alpine and start it once:
// resources/js/app.js — only for views without Livewire
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
- Livewire: Livewire ships its own JS; keep your
@vitebundle additive (custom Alpine components, hooks) and let Livewire manage its own assets per@rules/laravel/livewire.md. Do not bundle a second Alpine copy — Livewire already includes one; if you import Alpine yourself, follow Livewire's guidance to avoid a duplicate instance. - Filament: Filament publishes and serves its own compiled assets; use a
Filament theme + its asset pipeline for panel styling rather than forcing it
through your app entrypoint (
@rules/laravel/filament.md). Reserve your Vite bundle for front-end (non-panel) views.
Building for production in CI
npm ci
npm run build # writes public/build + manifest.json
Run npm run build in CI before deploying; ship public/build/. Missing
manifest entries surface at render time as a Vite manifest not found
exception, so the build step must succeed before the app boots in production.
Done when
vite.config.jsdeclares every entrypoint via thelaravel()plugin withrefreshconfigured for the watched paths.- Layouts load assets through
@vite([...]); no@viteReactRefreshpresent. npm run dev+php artisan servegive working HMR locally;public/hotandpublic/buildare gitignored.- Only
VITE_-prefixed (public) vars are inlined client-side; secrets stay server-side. npm run buildproduces a hashedpublic/build/+manifest.json, and CI runs the build before deploy.- Livewire/Filament keep their own asset pipelines; Alpine is started exactly once.
Output Humanization
- Use blader/humanizer for all skill outputs to keep the text natural and human-friendly.