Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

stratboy's avatar

Loading page-specific assets/scripts

Hi, I'm currenty using @vite to load shared scripts from the head section of the main layout component. I then want to load page-specific scripts using @stack/@push. But there, if I use @vite again, it will inject the server client again, so twice. Therefore I came up with another solution. It works but I don't know if it's the best way (for sure, I've lost HMR for the page-specific script, for example..):

The helper (App/Helpers/AssetHelpers.php) reads the Vite manifest and lets to retrieve the versioned asset:

namespace App\Helpers;

use Illuminate\Support\Facades\Log;

class AssetHelpers{

  private $assets_manifest = null;

  private static function get_assets_manifest($to_json = true){
    $manifest_path = public_path('build/manifest.json');
    $contents = '';

    if(is_file($manifest_path)){
      $handle = fopen($manifest_path, 'rb');
  
      if ($handle) {
        try {
          if (flock($handle, LOCK_SH)) {
            clearstatcache(true, $manifest_path);
            $contents = fread($handle, filesize($manifest_path) ?: 1);
            flock($handle, LOCK_UN);
          }
        } finally {
          fclose($handle);
        }
      }
    }

    if(!empty($contents) && $to_json) return json_decode($contents, true);
    return $contents;
  }

  public static function get_asset_from_manifest($filename = null){
    $assets_map = AssetHelpers::get_assets_manifest();
    return isset($assets_map[$filename]) ? asset('build/' . $assets_map[$filename]['file']) : '';
  }
}

Then I share a function to all views from the boot() method of the appServiceProvider:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Log;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        //
    }

    public function boot(): void
    {
        View::composer('*', function ($view) {
            $lr_get_asset = function ($asset_path = null) { return AssetHelpers::get_asset_from_manifest($asset_path); };
            $view->with('lr_get_asset', $lr_get_asset);
        });
    }
}

And call it from views like this:

@push('scripts')
    <script src="{{ $lr_get_asset('resources/js/seed-create.js') }}"></script>
@endpush

By logging some from inside, I see that apparently AssetHelpers::get_asset_from_manifest() is not called once per request, but only when $lr_get_asset gets called from a view. So for now I'm happy. But not sure I'm doing the best way. And maybe I should cache the $assets_map json or save it in the session, but that's not a problem.

What do you think? How would you accomplish the same task instead?

0 likes
2 replies
LaryAI's avatar
Level 58

Your approach to loading page-specific assets in a Laravel application using Vite is quite reasonable. You've created a helper to read the Vite manifest and retrieve the correct versioned asset path, and you're sharing a function across all views to use this helper. This allows you to push page-specific scripts onto the stack without reloading the Vite client multiple times.

However, there are a few improvements and considerations you might want to take into account:

  1. Caching the Manifest: Since the manifest file doesn't change between requests, you can cache its contents to avoid reading the file from the disk on every request. This can be done using Laravel's cache system.

  2. Error Handling: Ensure that your code gracefully handles any errors that might occur when reading the manifest file or if the asset is not found in the manifest.

  3. Hot Module Replacement (HMR): To retain HMR functionality, you might want to check if the application is in development mode and handle the asset inclusion differently in that case.

Here's an updated version of your AssetHelpers class with caching implemented:

namespace App\Helpers;

use Illuminate\Support\Facades\Cache;

class AssetHelpers
{
    public static function get_assets_manifest()
    {
        return Cache::remember('assets_manifest', 3600, function () {
            $manifest_path = public_path('build/manifest.json');
            if (is_file($manifest_path)) {
                return json_decode(file_get_contents($manifest_path), true);
            }
            return [];
        });
    }

    public static function get_asset_from_manifest($filename = null)
    {
        $assets_map = self::get_assets_manifest();
        return isset($assets_map[$filename]) ? asset('build/' . $assets_map[$filename]['file']) : '';
    }
}

In your AppServiceProvider, you can check for the environment and decide whether to use the cached manifest or read it directly for HMR:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        View::composer('*', function ($view) {
            $lr_get_asset = function ($asset_path = null) {
                if (app()->environment('local')) {
                    // Clear the cache in local development to support HMR
                    Cache::forget('assets_manifest');
                }
                return AssetHelpers::get_asset_from_manifest($asset_path);
            };
            $view->with('lr_get_asset', $lr_get_asset);
        });
    }
}

And in your views, you can continue to use the @push directive as you have been:

@push('scripts')
    <script src="{{ $lr_get_asset('resources/js/seed-create.js') }}"></script>
@endpush

This solution should provide a more efficient way to handle assets and maintain HMR support during local development. Remember to handle any potential exceptions and provide fallbacks where necessary.

stratboy's avatar

Is file_get_contents a better approach then using fopen/read as I did?

Please or to participate in this conversation.