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
{
    $manifestPathPrimary = base_path('public/build/.vite/manifest.json');
    $manifestPathFallback = base_path('public/build/manifest.json');
    $manifestPath = file_exists($manifestPathPrimary) ? $manifestPathPrimary : (file_exists($manifestPathFallback) ? $manifestPathFallback : null);

    // Step 1: If pre-compiled manifest exists, ALWAYS use it.
    // This avoids port conflicts and 200ms timeouts when the dev server is offline.
    if ($manifestPath) {
        $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 2: Fallback to dev server only if there is no compiled build
    $devServerUrl = $_ENV['VITE_DEV_SERVER_URL'] ?? 'http://localhost:5173';

    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 3: Ultimate fallback for missing assets
    $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 and dev server is offline. -->";
}

Step-by-Step Breakdown

Step 1: Look for Pre-built Manifest

$manifestPathPrimary = base_path('public/build/.vite/manifest.json');
$manifestPathFallback = base_path('public/build/manifest.json');
$manifestPath = file_exists($manifestPathPrimary) ? $manifestPathPrimary : (file_exists($manifestPathFallback) ? $manifestPathFallback : null);

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.

Why check the manifest first? By checking for manifest.json before checking the dev server, DALT completely avoids port 5173 collisions. If a user runs composer create-project and the assets are already pre-built, the framework will load them instantly. It also avoids a 200ms fsockopen() timeout penalty on every page load in production!

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).

If the manifest exists, it immediately generates the <script> and <link> tags and returns them.

Step 2: Check Dev Server (Fallback)

If the manifest is missing (e.g., you deleted it to develop locally), DALT checks if you are running npm run dev.

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. The 0.2 is a timeout in seconds (200ms).

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 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 and dev server is offline. -->

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

  • Checks manifest first to skip the costly 200ms fsockopen() timeout in production.
  • Prevents Port 5173 collisions when running multiple projects.
  • Automatic dev/production switching (just delete the public/build directory to use the dev server).
  • Cache busting in production (no stale files).
  • Fallback to static files (graceful degradation).
  • 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