Vite Integration
How the vite() helper loads JavaScript and CSS in development and production
What is Vite?
Vite is a build tool for modern web development. It:
- Serves your JavaScript and CSS during development (with hot reload)
- Bundles and optimizes them for production
- Handles modern features like Vue, React, TypeScript, etc.
In DALT.PHP, the vite() helper function bridges PHP and Vite.
The vite() Helper
You call it in your layout's <head>:
<?= vite('.dalt/resources/js/app.js') ?>This single line handles both development and production automatically.
How It Works: Two Modes
Development Mode (npm run dev)
When Vite's dev server is running on http://localhost:5173:
<script type="module" src="http://localhost:5173/@vite/client"></script>
<script type="module" src="http://localhost:5173/.dalt/resources/js/app.js"></script>What happens:
- Vite client connects via WebSocket
- When you edit a file, Vite sends an update
- The browser hot-reloads without full page refresh
- CSS changes apply instantly
Production Mode (npm run build)
When Vite has built your assets:
<link rel="stylesheet" href="/build/assets/app-abc123.css">
<script type="module" src="/build/assets/app-xyz789.js"></script>What happens:
- Vite bundles and minifies your code
- Adds content hashes to filenames (for cache busting)
- Generates a
manifest.jsonthat maps entry files to built files - The
vite()helper reads this manifest
The Complete vite() Function
Let's walk through the actual code:
function vite(string $entryPath): string
{
// Can be overridden via VITE_DEV_SERVER_URL in .env (useful for containers / alternate ports)
$devServerUrl = $_ENV['VITE_DEV_SERVER_URL'] ?? 'http://localhost:5173';
// Step 1: Check if dev server is running
if (vite_is_dev_server_running($devServerUrl)) {
$client = '<script type="module" src="' . $devServerUrl . '/@vite/client"></script>';
$entry = '<script type="module" src="' . $devServerUrl . '/' . ltrim($entryPath, '/') . '"></script>';
return $client . "\n" . $entry;
}
// Step 2: Look for manifest.json
$manifestPathPrimary = base_path('public/build/.vite/manifest.json');
$manifestPathFallback = base_path('public/build/manifest.json');
$manifestPath = file_exists($manifestPathPrimary) ? $manifestPathPrimary : $manifestPathFallback;
if (!file_exists($manifestPath)) {
// Step 3: Fallback to static files
$fallback = [];
$cssCandidates = [
'public/app.css',
'public/js/app.css',
'public/css/style.css',
];
$jsCandidates = [
'public/app.js',
'public/js/app.js',
];
foreach ($cssCandidates as $cssPath) {
if (file_exists(base_path($cssPath))) {
$href = '/' . ltrim(str_replace('public/', '', $cssPath), '/');
$fallback[] = '<link rel="stylesheet" href="' . htmlspecialchars($href) . '">';
break;
}
}
foreach ($jsCandidates as $jsPath) {
if (file_exists(base_path($jsPath))) {
$src = '/' . ltrim(str_replace('public/', '', $jsPath), '/');
$fallback[] = '<script defer src="' . htmlspecialchars($src) . '"></script>';
break;
}
}
if ($fallback) {
return implode("\n", $fallback);
}
return "<!-- Vite manifest not found. Run 'npm run build'. -->";
}
// Step 4: Read manifest and generate tags
$manifest = json_decode(file_get_contents($manifestPath), true);
if (!isset($manifest[$entryPath])) {
return "<!-- Vite entry '$entryPath' not present in manifest. -->";
}
$tags = [];
if (!empty($manifest[$entryPath]['css'])) {
foreach ($manifest[$entryPath]['css'] as $cssFile) {
$tags[] = '<link rel="stylesheet" href="/build/' . $cssFile . '">';
}
}
if (!empty($manifest[$entryPath]['file'])) {
$tags[] = '<script type="module" src="/build/' . $manifest[$entryPath]['file'] . '"></script>';
}
return implode("\n", $tags);
}Step-by-Step Breakdown
Step 1: Check Dev Server
if (vite_is_dev_server_running($devServerUrl)) {
$client = '<script type="module" src="' . $devServerUrl . '/@vite/client"></script>';
$entry = '<script type="module" src="' . $devServerUrl . '/' . ltrim($entryPath, '/') . '"></script>';
return $client . "\n" . $entry;
}How does it check if the dev server is running?
function vite_is_dev_server_running(string $url): bool
{
$host = parse_url($url, PHP_URL_HOST) ?: 'localhost';
$port = (int) (parse_url($url, PHP_URL_PORT) ?: 5173);
$connection = @fsockopen($host, $port, $errno, $errstr, 0.2);
if (is_resource($connection)) {
fclose($connection);
return true;
}
return false;
}This tries to open a socket connection to localhost:5173:
- If successful → dev server is running
- If it fails → dev server is not running
The @ suppresses warnings (since we expect it to fail in production).
The 0.2 is a timeout in seconds (200ms) - we don't want to wait long.
What are the two scripts?
-
Vite client:
/@vite/client- Connects to Vite via WebSocket
- Listens for file changes
- Triggers hot module replacement (HMR)
-
Your entry file:
.dalt/resources/js/app.js- Your actual JavaScript code
- Vite processes it on-the-fly
Step 2: Look for Manifest
$manifestPathPrimary = base_path('public/build/.vite/manifest.json');
$manifestPathFallback = base_path('public/build/manifest.json');
$manifestPath = file_exists($manifestPathPrimary) ? $manifestPathPrimary : $manifestPathFallback;Vite can put the manifest in two locations:
public/build/.vite/manifest.json(newer Vite versions)public/build/manifest.json(older versions)
We check both for compatibility.
What's in the manifest?
{
".dalt/resources/js/app.js": {
"file": "assets/app-abc123.js",
"css": [
"assets/app-xyz789.css"
]
}
}It maps:
- Entry file (
.dalt/resources/js/app.js) - To built file (
assets/app-abc123.js) - And its CSS (
assets/app-xyz789.css)
The hashes (abc123, xyz789) change when the content changes, forcing browsers to download the new version (cache busting).
Step 3: Fallback to Static Files
if (!file_exists($manifestPath)) {
// Try to find static CSS/JS files
$cssCandidates = [
'public/app.css',
'public/js/app.css',
'public/css/style.css',
];
// ...
}If there's no manifest (you haven't run npm run build), it looks for plain CSS/JS files in common locations.
This is a safety net for:
- Projects without a build step
- Quick prototypes
- When you forget to build
If it finds files, it returns:
<link rel="stylesheet" href="/app.css">
<script defer src="/app.js"></script>If not, it returns a helpful comment:
<!-- Vite manifest not found. Run 'npm run build'. -->Step 4: Read Manifest and Generate Tags
$manifest = json_decode(file_get_contents($manifestPath), true);
if (!isset($manifest[$entryPath])) {
return "<!-- Vite entry '$entryPath' not present in manifest. -->";
}
$tags = [];
if (!empty($manifest[$entryPath]['css'])) {
foreach ($manifest[$entryPath]['css'] as $cssFile) {
$tags[] = '<link rel="stylesheet" href="/build/' . $cssFile . '">';
}
}
if (!empty($manifest[$entryPath]['file'])) {
$tags[] = '<script type="module" src="/build/' . $manifest[$entryPath]['file'] . '"></script>';
}
return implode("\n", $tags);This:
- Reads the manifest JSON
- Looks up your entry file
- Generates
<link>tags for CSS - Generates
<script>tags for JS - Returns them as a string
ELI5: How vite() Works
Imagine you're ordering food:
Development (dev server running):
- You order from a food truck (Vite dev server)
- They make it fresh on the spot
- If you change your order, they quickly adjust
- Fast and flexible
Production (built assets):
- You order from a restaurant with a menu (manifest.json)
- They've pre-made popular dishes (built files)
- You look at the menu to see what's available
- Fast to serve, but can't change on the fly
Fallback (no build):
- You look in your fridge for leftovers (static files)
- Not ideal, but better than nothing
What type="module" Means
<script type="module" src="..."></script>This tells the browser:
- This is an ES module (modern JavaScript)
- It can use
importandexport - It's automatically deferred (doesn't block page load)
- It has its own scope (doesn't pollute global namespace)
Without type="module", you'd use old-style scripts:
<script src="..."></script>Cache Busting with Hashes
When Vite builds, it adds hashes to filenames:
app-abc123.js
app-xyz789.cssWhy?
Without hashes:
<script src="/build/app.js"></script>Problem:
- You deploy a new version
- User's browser has the old
app.jscached - They see the old version until cache expires
With hashes:
<script src="/build/app-abc123.js"></script>When you deploy:
- New version has a different hash:
app-def456.js - Browser sees it as a new file
- Downloads it immediately
- No stale cache issues
Development Workflow
Starting Development
npm run devThis starts Vite's dev server on http://localhost:5173.
Now when you visit your PHP app:
- PHP calls
vite('.dalt/resources/js/app.js') vite_is_dev_server_running()returnstrue- Scripts point to
localhost:5173 - Vite serves files with hot reload
Building for Production
npm run buildThis:
- Bundles your JavaScript
- Processes your CSS (Tailwind, etc.)
- Minifies everything
- Adds content hashes
- Generates
manifest.json - Outputs to
public/build/
Now when you visit your PHP app:
- PHP calls
vite('.dalt/resources/js/app.js') vite_is_dev_server_running()returnsfalse- Reads
manifest.json - Scripts point to
/build/assets/app-abc123.js
What's Good Here
- Automatic dev/production switching (no config needed)
- Hot module replacement in development (fast feedback)
- Cache busting in production (no stale files)
- Fallback to static files (graceful degradation)
- Helpful error messages (tells you to run
npm run build) - Socket check is fast (200ms timeout)
- Supports both old and new Vite manifest locations
Configuration Note
The dev server URL can be configured via:
$_ENV['VITE_DEV_SERVER_URL'] ?? 'http://localhost:5173'So if you run Vite on a different host/port (or HTTPS), you don’t have to change code—just set the environment variable.
Common Issues
"Vite manifest not found"
Problem: You see this comment in your HTML source.
Solution: Run npm run build to generate the manifest.
Assets Not Loading in Production
Problem: CSS/JS files return 404.
Causes:
- Forgot to run
npm run build - Build output is in wrong directory
- Web server isn't serving
/build/directory
Solution: Check that public/build/ exists and contains files.
Hot Reload Not Working
Problem: You edit a file but the browser doesn't update.
Causes:
- Dev server isn't running (
npm run dev) - Port 5173 is blocked by firewall
- Browser console shows WebSocket errors
Solution: Check dev server is running and accessible.
Wrong Assets Loaded
Problem: Old CSS/JS is loaded after deploying.
Cause: Manifest wasn't regenerated.
Solution: Always run npm run build before deploying.
Testing Vite Integration
In Development
- Start dev server:
npm run dev - Visit your app
- View page source
- Look for:
<script type="module" src="http://localhost:5173/..."></script> - Edit a CSS file
- Browser should update without full reload
In Production
- Build assets:
npm run build - Stop dev server
- Visit your app
- View page source
- Look for:
<script type="module" src="/build/assets/app-abc123.js"></script> - Check that files load (no 404s in browser console)