Repository: laravel/horizon (language: php)

Issue:
Subresource Integrity failures when using ASSET_URL
### Horizon Version

5.24.3

### Laravel Version

10.48.9

### PHP Version

8.3.6

### Redis Driver

PhpRedis

### Redis Version

6.0.2

### Database Driver & Version

_No response_

### Description

Related #1421

I have a deployment using a CDN via `ASSET_URL=https:://cdn.domain.com` but I'm running into CORs issues.

Chrome reports:

> Subresource Integrity: The resource 'https://cdn.domain.com/vendor/horizon/styles.css' has an integrity attribute, but the resource requires the request to be CORS enabled to check the integrity, and it is not. The resource has been blocked because the integrity cannot be enforced.

CORs is confusing but I resolve for the main site using AppServiceProvider with the following, but all my customisations via the Vite facade appear to be ignored within horizon including createAssetPathsUsing which I hoped could be an alternative to exclude horizon paths from using the CDN altogether. 

```
Vite::useStyleTagAttributes(['crossorigin' => 'anonymous'])
  ->useScriptTagAttributes(['crossorigin' => 'anonymous'])
```

I'm not sure how to fix it but it looks like https://github.com/laravel/horizon/blob/bf3c4a87d91b745b973b35a161d6c672df4f47ac/resources/views/layout.blade.php#L1-L21 may create it's own instance ignoring all customisations, perhaps that should somehow clone and extend the users defaults or at least have a way to apply customisations?

For now I've reverted to 5.24.2 which doesn't have integrity attributes so loads without issue.

### Steps To Reproduce

Access assets via a different domain, i.e ASSET_URL=https://cdn.domain.com and try to access the horizon dashboard.


Discussion / hints:
Something else I've noticed is vite is configured to not have hashes in filenames,presumably to keep git cleaner but assets will be non-cache-busting.

Not sure if there is a way to get vite to handle it, or createAssetPathsUsing appending Vite::manifestHash() to the query string?

Fix patch:
diff --git a/package-lock.json b/package-lock.json
index 108c0a1b..4d9433ac 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,7 +20,6 @@
                 "sass": "^1.74.1",
                 "sql-formatter": "^4.0.2",
                 "vite": "^5.2.8",
-                "vite-plugin-manifest-sri": "^0.2.0",
                 "vue": "^2.7.16",
                 "vue-json-pretty": "^1.9.5",
                 "vue-router": "^3.6.5"
@@ -1572,12 +1571,6 @@
                 "picomatch": "^2.3.1"
             }
         },
-        "node_modules/vite-plugin-manifest-sri": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmjs.org/vite-plugin-manifest-sri/-/vite-plugin-manifest-sri-0.2.0.tgz",
-            "integrity": "sha512-Zt5jt19xTIJ91LOuQTCtNG7rTFc5OziAjBz2H5NdCGqaOD1nxrWExLhcKW+W4/q8/jOPCg/n5ncYEQmqCxiGQQ==",
-            "dev": true
-        },
         "node_modules/vue": {
             "version": "2.7.16",
             "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz",
@@ -2571,12 +2564,6 @@
                 "picomatch": "^2.3.1"
             }
         },
-        "vite-plugin-manifest-sri": {
-            "version": "0.2.0",
-            "resolved": "https://registry.npmjs.org/vite-plugin-manifest-sri/-/vite-plugin-manifest-sri-0.2.0.tgz",
-            "integrity": "sha512-Zt5jt19xTIJ91LOuQTCtNG7rTFc5OziAjBz2H5NdCGqaOD1nxrWExLhcKW+W4/q8/jOPCg/n5ncYEQmqCxiGQQ==",
-            "dev": true
-        },
         "vue": {
             "version": "2.7.16",
             "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz",
diff --git a/package.json b/package.json
index a1ece21b..8830d78e 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,6 @@
         "sass": "^1.74.1",
         "sql-formatter": "^4.0.2",
         "vite": "^5.2.8",
-        "vite-plugin-manifest-sri": "^0.2.0",
         "vue": "^2.7.16",
         "vue-json-pretty": "^1.9.5",
         "vue-router": "^3.6.5"
diff --git a/public/build/manifest.json b/public/build/manifest.json
index 30ab3175..8f075158 100644
--- a/public/build/manifest.json
+++ b/public/build/manifest.json
@@ -1,29 +1,25 @@
 {
   "resources/img/favicon.png": {
-    "file": "favicon.png",
-    "src": "resources/img/favicon.png",
-    "integrity": "sha384-tqnRilkeRgqFt3SUYaxuaQs14WOwuU8Gvk3sqRZmnyWZVhr1Kk19Ecr7dFMb4HZo"
+    "file": "favicon.png?id=1542bfe8a0010dcbee710da13cce367f",
+    "src": "resources/img/favicon.png"
   },
   "resources/js/app.js": {
-    "file": "app.js",
+    "file": "app.js?id=69a73e4fc0356dae6adf8cc33985b1d5",
     "name": "app",
     "src": "resources/js/app.js",
     "isEntry": true,
     "css": [
       "app.css"
-    ],
-    "integrity": "sha384-EV5vlraT2g7leIzueltC7I+UzR7uBT4ndQF5b1G9I+kUrQ4XL0DREuRw/XiU/U3P"
+    ]
   },
   "resources/sass/styles-dark.scss": {
-    "file": "styles-dark.css",
+    "file": "styles-dark.css?id=4147954938fc149998024ad0251831e4",
     "src": "resources/sass/styles-dark.scss",
-    "isEntry": true,
-    "integrity": "sha384-/sLOxh+NTFEdcZ8svIuVTv/lSL65X3QGIXhExXAhntQYWjiez1CQbv4ICbtwRfd8"
+    "isEntry": true
   },
   "resources/sass/styles.scss": {
-    "file": "styles.css",
+    "file": "styles.css?id=452cf65324025421cc414ccb7744885f",
     "src": "resources/sass/styles.scss",
-    "isEntry": true,
-    "integrity": "sha384-4HOmv1E51xgqbUYzCYXaRXPRja5nEho6eq/L/CKs0LlidzTGNTk81VtCAHqLiYSC"
+    "isEntry": true
   }
 }
\ No newline at end of file
diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php
index 3352864a..e61d348d 100644
--- a/resources/views/layout.blade.php
+++ b/resources/views/layout.blade.php
@@ -1,23 +1,8 @@
 @php
-use Illuminate\Support\Facades\Vite;
-use Illuminate\Foundation\Vite as ViteFoundation;
+$nonExistentFileName = public_path('/vendor/horizon/nonExistentFile');
+$previousHotFile = Vite::hotFile();
 
-$nonExistentFileName = '/vendor/horizon/nonExistentFile';
-
-$vite = new ViteFoundation();
-$vite->useHotFile($nonExistentFileName);
-
-$viteDataSchemeLight = new ViteFoundation();
-$viteDataSchemeLight->useHotFile($nonExistentFileName);
-$viteDataSchemeLight->useStyleTagAttributes([
-'data-scheme' => 'light',
-]);
-
-$viteDataSchemeDark = new ViteFoundation();
-$viteDataSchemeDark->useHotFile($nonExistentFileName);
-$viteDataSchemeDark->useStyleTagAttributes([
-'data-scheme' => 'dark',
-]);
+Vite::useHotFile($nonExistentFileName);
 @endphp
 <!DOCTYPE html>
 <html lang="en">
@@ -26,7 +11,7 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <meta name="csrf-token" content="{{ csrf_token() }}">
-    <link rel="shortcut icon" href="{{ $vite->asset('resources/img/favicon.png', 'vendor/horizon') }}">
+    <link rel="shortcut icon" href="{{ Vite::asset('resources/img/favicon.png', 'vendor/horizon') }}">
 
     <title>Horizon{{ config('app.name') ? ' - ' . config('app.name') : '' }}</title>
 
@@ -34,8 +19,10 @@
     <link rel="preconnect" href="https://fonts.bunny.net">
     <link href="https://fonts.bunny.net/css?family=figtree:300,400,500,600" rel="stylesheet" />
 
-    {{ $viteDataSchemeLight('resources/sass/styles.scss', 'vendor/horizon') }}
-    {{ $viteDataSchemeDark('resources/sass/styles-dark.scss', 'vendor/horizon') }}
+    <link rel="preload" as="style" href="{{ Vite::asset('resources/sass/styles.scss', 'vendor/horizon') }}" />
+    <link rel="stylesheet" href="{{ Vite::asset('resources/sass/styles.scss', 'vendor/horizon') }}" data-scheme="light" />
+    <link rel="preload" as="style" href="{{ Vite::asset('resources/sass/styles-dark.scss', 'vendor/horizon') }}" />
+    <link rel="stylesheet" href="{{ Vite::asset('resources/sass/styles-dark.scss', 'vendor/horizon') }}" data-scheme="dark" />
 </head>
 <body>
 <div id="horizon" v-cloak>
@@ -163,6 +150,9 @@
     window.Horizon = @json($horizonScriptVariables);
 </script>
 
-{{ $vite('resources/js/app.js', 'vendor/horizon') }}
+@vite('resources/js/app.js', 'vendor/horizon')
 </body>
 </html>
+@php
+Vite::useHotFile($previousHotFile);
+@endphp
diff --git a/vite.config.mjs b/vite.config.mjs
index aa696725..7d37b9c5 100644
--- a/vite.config.mjs
+++ b/vite.config.mjs
@@ -1,7 +1,41 @@
 import vue2 from "@vitejs/plugin-vue2";
 import { defineConfig } from "vite";
 import laravel from "laravel-vite-plugin";
-import manifestSRI from "vite-plugin-manifest-sri";
+import { createHash } from "node:crypto";
+import { resolve } from "node:path";
+import { readFileSync, writeFileSync } from "node:fs";
+
+function manifestQueryParam() {
+    return {
+        name: "vite-horizon-manifest-query-param",
+        apply: "build",
+        enforce: "post",
+        writeBundle({ dir }) {
+            const manifestPath = resolve(dir, "manifest.json");
+            const manifest = readFileSync(manifestPath, "utf-8");
+
+            if (manifest) {
+                const parsedManifest = JSON.parse(manifest);
+
+                for (const property in parsedManifest) {
+                    const fileContent = readFileSync(
+                        resolve(dir, parsedManifest[property].file),
+                        "utf-8"
+                    );
+
+                    parsedManifest[property].file += `?id=${createHash("md5")
+                        .update(fileContent)
+                        .digest("hex")}`;
+                }
+
+                writeFileSync(
+                    manifestPath,
+                    JSON.stringify(parsedManifest, null, 2)
+                );
+            }
+        },
+    };
+}
 
 const config = defineConfig({
     plugins: [
@@ -11,7 +45,7 @@ const config = defineConfig({
             "resources/js/app.js",
         ]),
         vue2(),
-        manifestSRI(),
+        manifestQueryParam(),
     ],
     resolve: {
         alias: {


Test patch: