DALT.PHP
Framework Deep Dive6. Views & Frontend

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:

  1. Vite client connects via WebSocket
  2. When you edit a file, Vite sends an update
  3. The browser hot-reloads without full page refresh
  4. 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:

  1. Vite bundles and minifies your code
  2. Adds content hashes to filenames (for cache busting)
  3. Generates a manifest.json that maps entry files to built files
  4. 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?

  1. Vite client: /@vite/client

    • Connects to Vite via WebSocket
    • Listens for file changes
    • Triggers hot module replacement (HMR)
  2. 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:

  1. Reads the manifest JSON
  2. Looks up your entry file
  3. Generates <link> tags for CSS
  4. Generates <script> tags for JS
  5. 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 import and export
  • 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.css

Why?

Without hashes:

<script src="/build/app.js"></script>

Problem:

  • You deploy a new version
  • User's browser has the old app.js cached
  • 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 dev

This starts Vite's dev server on http://localhost:5173.

Now when you visit your PHP app:

  1. PHP calls vite('.dalt/resources/js/app.js')
  2. vite_is_dev_server_running() returns true
  3. Scripts point to localhost:5173
  4. Vite serves files with hot reload

Building for Production

npm run build

This:

  1. Bundles your JavaScript
  2. Processes your CSS (Tailwind, etc.)
  3. Minifies everything
  4. Adds content hashes
  5. Generates manifest.json
  6. Outputs to public/build/

Now when you visit your PHP app:

  1. PHP calls vite('.dalt/resources/js/app.js')
  2. vite_is_dev_server_running() returns false
  3. Reads manifest.json
  4. 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

  1. Start dev server: npm run dev
  2. Visit your app
  3. View page source
  4. Look for: <script type="module" src="http://localhost:5173/..."></script>
  5. Edit a CSS file
  6. Browser should update without full reload

In Production

  1. Build assets: npm run build
  2. Stop dev server
  3. Visit your app
  4. View page source
  5. Look for: <script type="module" src="/build/assets/app-abc123.js"></script>
  6. Check that files load (no 404s in browser console)

On this page