diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index 46deeb8f0041..380fa74ed192 100644 --- a/src/Illuminate/Foundation/Vite.php +++ b/src/Illuminate/Foundation/Vite.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Support\Htmlable; use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; +use Illuminate\Support\Js; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; @@ -96,6 +97,20 @@ class Vite implements Htmlable */ protected static $manifests = []; + /** + * The prefetching strategy to use. + * + * @var null|'waterfall'|'aggressive' + */ + protected $prefetchStrategy = null; + + /** + * The number of assets to load concurrently when using the "waterfall" strategy. + * + * @var int + */ + protected $prefetchConcurrently = 3; + /** * Get the preloaded assets. * @@ -266,6 +281,47 @@ public function usePreloadTagAttributes($attributes) return $this; } + /** + * Use the "waterfall" prefetching strategy. + * + * @param int|null $concurrency + * @return $this + */ + public function useWaterfallPrefetching(?int $concurrency = null) + { + return $this->usePrefetchStrategy('waterfall', [ + 'concurrency' => $concurrency ?? $this->prefetchConcurrently + ]); + } + + /** + * Use the "aggressive" prefetching strategy. + * + * @return $this + */ + public function useAggressivePrefetching() + { + return $this->usePrefetchStrategy('aggressive'); + } + + /** + * Set the prefetching strategy. + * + * @param 'waterfall'|'aggressive'|null $strategy + * @param array $config + * @return $this + */ + public function usePrefetchStrategy($strategy, $config = []) + { + $this->prefetchStrategy = $strategy; + + if ($strategy === 'waterfall') { + $this->prefetchConcurrently = $config['concurrency'] ?? $this->prefetchConcurrently; + } + + return $this; + } + /** * Generate Vite tags for an entrypoint. * @@ -363,7 +419,122 @@ public function __invoke($entrypoints, $buildDirectory = null) ->sortByDesc(fn ($args) => $this->isCssPath($args[1])) ->map(fn ($args) => $this->makePreloadTagForChunk(...$args)); - return new HtmlString($preloads->join('').$stylesheets->join('').$scripts->join('')); + $base = $preloads->join('').$stylesheets->join('').$scripts->join(''); + + if ($this->prefetchStrategy === null || $this->isRunningHot()) { + return new HtmlString($base); + } + + $discoveredImports = []; + + return collect($entrypoints) + ->flatMap(fn ($entrypoint) => collect($manifest[$entrypoint]['dynamicImports'] ?? []) + ->map(fn ($import) => $manifest[$import]) + ->filter(fn ($chunk) => str_ends_with($chunk['file'], '.js') || str_ends_with($chunk['file'], '.css')) + ->flatMap($f = function ($chunk) use (&$f, $manifest, &$discoveredImports) { + return collect([...$chunk['imports'] ?? [], ...$chunk['dynamicImports'] ?? []]) + ->reject(function ($import) use (&$discoveredImports) { + if (isset($discoveredImports[$import])) { + return true; + } + + return ! $discoveredImports[$import] = true; + }) + ->reduce( + fn ($chunks, $import) => $chunks->merge( + $f($manifest[$import]) + ), collect([$chunk])) + ->merge(collect($chunk['css'] ?? [])->map( + fn ($css) => collect($manifest)->first(fn ($chunk) => $chunk['file'] === $css) ?? [ + 'file' => $css, + ], + )); + }) + ->map(function ($chunk) use ($buildDirectory, $manifest) { + return collect([ + ...$this->resolvePreloadTagAttributes( + $chunk['src'] ?? null, + $url = $this->assetPath("{$buildDirectory}/{$chunk['file']}"), + $chunk, + $manifest, + ), + 'rel' => 'prefetch', + 'fetchpriority' => 'low', + 'href' => $url, + ])->reject( + fn ($value) => in_array($value, [null, false], true) + )->mapWithKeys(fn ($value, $key) => [ + $key = (is_int($key) ? $value : $key) => $value === true ? $key : $value, + ])->all(); + }) + ->reject(fn ($attributes) => isset($this->preloadedAssets[$attributes['href']]))) + ->unique('href') + ->values() + ->pipe(fn ($assets) => with(Js::from($assets), fn ($assets) => match ($this->prefetchStrategy) { + 'waterfall' => new HtmlString($base.<< + window.addEventListener('load', () => window.setTimeout(() => { + const makeLink = (asset) => { + const link = document.createElement('link') + + Object.keys(asset).forEach((attribute) => { + link.setAttribute(attribute, asset[attribute]) + }) + + return link + } + + const loadNext = (assets, count) => window.setTimeout(() => { + if (count > assets.length) { + count = assets.length + + if (count === 0) { + return + } + } + + const fragment = new DocumentFragment + + while (count > 0) { + const link = makeLink(assets.shift()) + fragment.append(link) + count-- + + if (assets.length) { + link.onload = () => loadNext(assets, 1) + link.error = () => loadNext(assets, 1) + } + } + + document.head.append(fragment) + }) + + loadNext({$assets}, {$this->prefetchConcurrently}) + })) + + HTML), + 'aggressive' => new HtmlString($base.<< + window.addEventListener('load', () => window.setTimeout(() => { + const makeLink = (asset) => { + const link = document.createElement('link') + + Object.keys(asset).forEach((attribute) => { + link.setAttribute(attribute, asset[attribute]) + }) + + return link + } + + const fragment = new DocumentFragment + {$assets}.forEach((asset) => fragment.append(makeLink(asset))) + document.head.append(fragment) + })) + + HTML), + })); } /** diff --git a/tests/Foundation/FoundationViteTest.php b/tests/Foundation/FoundationViteTest.php index 950fc2c21495..8f19c8e7d379 100644 --- a/tests/Foundation/FoundationViteTest.php +++ b/tests/Foundation/FoundationViteTest.php @@ -6,6 +6,7 @@ use Illuminate\Foundation\ViteException; use Illuminate\Foundation\ViteManifestNotFoundException; use Illuminate\Support\Facades\Vite as ViteFacade; +use Illuminate\Support\Js; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; @@ -1297,6 +1298,360 @@ protected function makeViteManifest($contents = null, $path = 'build') file_put_contents(public_path("{$path}/manifest.json"), $manifest); } + public function testItCanPrefetchEntrypoint() + { + $manifest = json_decode(file_get_contents(__DIR__.'/fixtures/prefetching-manifest.json')); + $buildDir = Str::random(); + $this->makeViteManifest($manifest, $buildDir); + app()->usePublicPath(__DIR__); + + $html = (string) ViteFacade::withEntryPoints(['resources/js/app.js'])->useBuildDirectory($buildDir)->usePrefetchStrategy('waterfall')->toHtml(); + + $expectedAssets = Js::from([ + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ConfirmPassword-CDwcgU8E.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/GuestLayout-BY3LC-73.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/TextInput-C8CCB_U_.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/PrimaryButton-DuXwr-9M.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ApplicationLogo-BhIZH06z.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/_plugin-vue_export-helper-DlAUqK2U.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ForgotPassword-B0WWE0BO.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Login-DAFSdGSW.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Register-CfYQbTlA.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ResetPassword-BNl7a4X1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/VerifyEmail-CyukB_SZ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Dashboard-DM_LxQy2.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/AuthenticatedLayout-DfWF52N1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Edit-CYV2sXpe.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/DeleteUserForm-B1oHFaVP.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdatePasswordForm-CaeWqGla.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdateProfileInformationForm-CJwkYwQQ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Welcome-D_7l79PQ.js", 'fetchpriority' => 'low'], + ]); + $this->assertSame(<< + + HTML, $html); + + $this->cleanViteManifest($buildDir); + } + + public function testItHandlesSpecifyingPageWithAppJs() + { + $manifest = json_decode(file_get_contents(__DIR__.'/fixtures/prefetching-manifest.json')); + $buildDir = Str::random(); + $this->makeViteManifest($manifest, $buildDir); + app()->usePublicPath(__DIR__); + + $html = (string) ViteFacade::withEntryPoints(['resources/js/app.js', 'resources/js/Pages/Auth/Login.vue'])->useBuildDirectory($buildDir)->usePrefetchStrategy('waterfall')->toHtml(); + + $expectedAssets = Js::from([ + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ConfirmPassword-CDwcgU8E.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ForgotPassword-B0WWE0BO.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Register-CfYQbTlA.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ResetPassword-BNl7a4X1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/VerifyEmail-CyukB_SZ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Dashboard-DM_LxQy2.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/AuthenticatedLayout-DfWF52N1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Edit-CYV2sXpe.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/DeleteUserForm-B1oHFaVP.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdatePasswordForm-CaeWqGla.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdateProfileInformationForm-CJwkYwQQ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Welcome-D_7l79PQ.js", 'fetchpriority' => 'low'], + ]); + $this->assertStringContainsString(<<cleanViteManifest($buildDir); + } + + public function testItCanSpecifyWaterfallChunks() + { + $manifest = json_decode(file_get_contents(__DIR__.'/fixtures/prefetching-manifest.json')); + $buildDir = Str::random(); + $this->makeViteManifest($manifest, $buildDir); + app()->usePublicPath(__DIR__); + + $html = (string) ViteFacade::withEntryPoints(['resources/js/app.js'])->useBuildDirectory($buildDir)->useWaterfallPrefetching(concurrency: 10)->toHtml(); + + $expectedAssets = Js::from([ + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ConfirmPassword-CDwcgU8E.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/GuestLayout-BY3LC-73.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/TextInput-C8CCB_U_.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/PrimaryButton-DuXwr-9M.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ApplicationLogo-BhIZH06z.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/_plugin-vue_export-helper-DlAUqK2U.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ForgotPassword-B0WWE0BO.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Login-DAFSdGSW.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Register-CfYQbTlA.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ResetPassword-BNl7a4X1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/VerifyEmail-CyukB_SZ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Dashboard-DM_LxQy2.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/AuthenticatedLayout-DfWF52N1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Edit-CYV2sXpe.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/DeleteUserForm-B1oHFaVP.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdatePasswordForm-CaeWqGla.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdateProfileInformationForm-CJwkYwQQ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Welcome-D_7l79PQ.js", 'fetchpriority' => 'low'], + ]); + $this->assertStringContainsString(<<cleanViteManifest($buildDir); + } + + public function testItCanPrefetchAggressively() + { + $manifest = json_decode(file_get_contents(__DIR__.'/fixtures/prefetching-manifest.json')); + $buildDir = Str::random(); + $this->makeViteManifest($manifest, $buildDir); + app()->usePublicPath(__DIR__); + + $html = (string) ViteFacade::withEntryPoints(['resources/js/app.js'])->useBuildDirectory($buildDir)->useAggressivePrefetching()->toHtml(); + + $expectedAssets = Js::from([ + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ConfirmPassword-CDwcgU8E.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/GuestLayout-BY3LC-73.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/TextInput-C8CCB_U_.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/PrimaryButton-DuXwr-9M.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ApplicationLogo-BhIZH06z.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/_plugin-vue_export-helper-DlAUqK2U.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ForgotPassword-B0WWE0BO.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Login-DAFSdGSW.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Register-CfYQbTlA.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ResetPassword-BNl7a4X1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/VerifyEmail-CyukB_SZ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Dashboard-DM_LxQy2.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/AuthenticatedLayout-DfWF52N1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Edit-CYV2sXpe.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/DeleteUserForm-B1oHFaVP.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdatePasswordForm-CaeWqGla.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdateProfileInformationForm-CJwkYwQQ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Welcome-D_7l79PQ.js", 'fetchpriority' => 'low'], + ]); + + $this->assertSame(<< + + HTML, $html); + + $this->cleanViteManifest($buildDir); + } + + public function testAddsAttributesToPrefetchTags() + { + $manifest = json_decode(file_get_contents(__DIR__.'/fixtures/prefetching-manifest.json')); + $buildDir = Str::random(); + $this->makeViteManifest($manifest, $buildDir); + app()->usePublicPath(__DIR__); + + $html = (string) tap(ViteFacade::withEntryPoints(['resources/js/app.js'])->useBuildDirectory($buildDir)->usePrefetchStrategy('waterfall'))->useCspNonce('abc123')->toHtml(); + + $expectedAssets = Js::from([ + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ConfirmPassword-CDwcgU8E.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/GuestLayout-BY3LC-73.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/TextInput-C8CCB_U_.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/PrimaryButton-DuXwr-9M.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ApplicationLogo-BhIZH06z.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/_plugin-vue_export-helper-DlAUqK2U.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ForgotPassword-B0WWE0BO.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Login-DAFSdGSW.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Register-CfYQbTlA.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ResetPassword-BNl7a4X1.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/VerifyEmail-CyukB_SZ.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Dashboard-DM_LxQy2.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/AuthenticatedLayout-DfWF52N1.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Edit-CYV2sXpe.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/DeleteUserForm-B1oHFaVP.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdatePasswordForm-CaeWqGla.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdateProfileInformationForm-CJwkYwQQ.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Welcome-D_7l79PQ.js", 'nonce' => 'abc123', 'fetchpriority' => 'low'], + ]); + $this->assertStringContainsString(<<cleanViteManifest($buildDir); + } + + public function testItNormalisesAttributes() + { + $manifest = json_decode(file_get_contents(__DIR__.'/fixtures/prefetching-manifest.json')); + $buildDir = Str::random(); + $this->makeViteManifest($manifest, $buildDir); + app()->usePublicPath(__DIR__); + + $html = (string) tap(ViteFacade::withEntryPoints(['resources/js/app.js']))->useBuildDirectory($buildDir)->usePrefetchStrategy('waterfall')->usePreloadTagAttributes([ + 'key' => 'value', + 'key-only', + 'true-value' => true, + 'false-value' => false, + 'null-value' => null, + ])->toHtml(); + + $expectedAssets = Js::from([ + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ConfirmPassword-CDwcgU8E.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/GuestLayout-BY3LC-73.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/TextInput-C8CCB_U_.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/PrimaryButton-DuXwr-9M.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ApplicationLogo-BhIZH06z.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/_plugin-vue_export-helper-DlAUqK2U.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ForgotPassword-B0WWE0BO.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Login-DAFSdGSW.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Register-CfYQbTlA.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ResetPassword-BNl7a4X1.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/VerifyEmail-CyukB_SZ.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Dashboard-DM_LxQy2.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/AuthenticatedLayout-DfWF52N1.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Edit-CYV2sXpe.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/DeleteUserForm-B1oHFaVP.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdatePasswordForm-CaeWqGla.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdateProfileInformationForm-CJwkYwQQ.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Welcome-D_7l79PQ.js", 'key' => 'value', 'key-only' => 'key-only', 'true-value' => 'true-value', 'fetchpriority' => 'low'], + ]); + + $this->assertStringContainsString(<<cleanViteManifest($buildDir); + } + + public function testItPrefetchesCss() + { + $manifest = json_decode(file_get_contents(__DIR__.'/fixtures/prefetching-manifest.json')); + $buildDir = Str::random(); + $this->makeViteManifest($manifest, $buildDir); + app()->usePublicPath(__DIR__); + + $html = (string) ViteFacade::withEntryPoints(['resources/js/admin.js'])->useBuildDirectory($buildDir)->usePrefetchStrategy('waterfall')->toHtml(); + + $expectedAssets = Js::from([ + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ConfirmPassword-CDwcgU8E.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/GuestLayout-BY3LC-73.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/TextInput-C8CCB_U_.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/PrimaryButton-DuXwr-9M.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ApplicationLogo-BhIZH06z.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/_plugin-vue_export-helper-DlAUqK2U.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ForgotPassword-B0WWE0BO.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Login-DAFSdGSW.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Register-CfYQbTlA.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/ResetPassword-BNl7a4X1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/VerifyEmail-CyukB_SZ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Dashboard-DM_LxQy2.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/AuthenticatedLayout-DfWF52N1.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Edit-CYV2sXpe.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/DeleteUserForm-B1oHFaVP.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdatePasswordForm-CaeWqGla.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/UpdateProfileInformationForm-CJwkYwQQ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/Welcome-D_7l79PQ.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/admin-runtime-import-CRvLQy6v.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'href' => "https://example.com/{$buildDir}/assets/admin-runtime-import-import-DKMIaPXC.js", 'fetchpriority' => 'low'], + ['rel' => 'prefetch', 'as' => 'style', 'href' => "https://example.com/{$buildDir}/assets/admin-runtime-import-BlmN0T4U.css", 'fetchpriority' => 'low'], + ]); + $this->assertSame(<< + + HTML, $html); + + $this->cleanViteManifest($buildDir); + } + protected function cleanViteManifest($path = 'build') { if (file_exists(public_path("{$path}/manifest.json"))) { diff --git a/tests/Foundation/fixtures/prefetching-manifest.json b/tests/Foundation/fixtures/prefetching-manifest.json new file mode 100644 index 000000000000..564ee66a4885 --- /dev/null +++ b/tests/Foundation/fixtures/prefetching-manifest.json @@ -0,0 +1,284 @@ +{ + "_ApplicationLogo-BhIZH06z.js": { + "file": "assets/ApplicationLogo-BhIZH06z.js", + "name": "ApplicationLogo", + "imports": [ + "__plugin-vue_export-helper-DlAUqK2U.js", + "_index-BSdK3M0e.js" + ] + }, + "_AuthenticatedLayout-DfWF52N1.js": { + "file": "assets/AuthenticatedLayout-DfWF52N1.js", + "name": "AuthenticatedLayout", + "imports": [ + "_ApplicationLogo-BhIZH06z.js", + "_index-BSdK3M0e.js" + ] + }, + "_GuestLayout-BY3LC-73.js": { + "file": "assets/GuestLayout-BY3LC-73.js", + "name": "GuestLayout", + "imports": [ + "_ApplicationLogo-BhIZH06z.js", + "_index-BSdK3M0e.js" + ] + }, + "_PrimaryButton-DuXwr-9M.js": { + "file": "assets/PrimaryButton-DuXwr-9M.js", + "name": "PrimaryButton", + "imports": [ + "__plugin-vue_export-helper-DlAUqK2U.js", + "_index-BSdK3M0e.js" + ] + }, + "_TextInput-C8CCB_U_.js": { + "file": "assets/TextInput-C8CCB_U_.js", + "name": "TextInput", + "imports": [ + "_index-BSdK3M0e.js" + ] + }, + "__plugin-vue_export-helper-DlAUqK2U.js": { + "file": "assets/_plugin-vue_export-helper-DlAUqK2U.js", + "name": "_plugin-vue_export-helper" + }, + "_index-!~{00f}~.js": { + "file": "assets/index-B3s1tYeC.css", + "src": "_index-!~{00f}~.js" + }, + "_index-BSdK3M0e.js": { + "file": "assets/index-BSdK3M0e.js", + "name": "index", + "css": [ + "assets/index-B3s1tYeC.css" + ] + }, + "resources/js/Pages/Auth/ConfirmPassword.vue": { + "file": "assets/ConfirmPassword-CDwcgU8E.js", + "name": "ConfirmPassword", + "src": "resources/js/Pages/Auth/ConfirmPassword.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js", + "_GuestLayout-BY3LC-73.js", + "_TextInput-C8CCB_U_.js", + "_PrimaryButton-DuXwr-9M.js", + "_ApplicationLogo-BhIZH06z.js", + "__plugin-vue_export-helper-DlAUqK2U.js" + ] + }, + "resources/js/Pages/Auth/ForgotPassword.vue": { + "file": "assets/ForgotPassword-B0WWE0BO.js", + "name": "ForgotPassword", + "src": "resources/js/Pages/Auth/ForgotPassword.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js", + "_GuestLayout-BY3LC-73.js", + "_TextInput-C8CCB_U_.js", + "_PrimaryButton-DuXwr-9M.js", + "_ApplicationLogo-BhIZH06z.js", + "__plugin-vue_export-helper-DlAUqK2U.js" + ] + }, + "resources/js/Pages/Auth/Login.vue": { + "file": "assets/Login-DAFSdGSW.js", + "name": "Login", + "src": "resources/js/Pages/Auth/Login.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js", + "_GuestLayout-BY3LC-73.js", + "_TextInput-C8CCB_U_.js", + "_PrimaryButton-DuXwr-9M.js", + "_ApplicationLogo-BhIZH06z.js", + "__plugin-vue_export-helper-DlAUqK2U.js" + ] + }, + "resources/js/Pages/Auth/Register.vue": { + "file": "assets/Register-CfYQbTlA.js", + "name": "Register", + "src": "resources/js/Pages/Auth/Register.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js", + "_GuestLayout-BY3LC-73.js", + "_TextInput-C8CCB_U_.js", + "_PrimaryButton-DuXwr-9M.js", + "_ApplicationLogo-BhIZH06z.js", + "__plugin-vue_export-helper-DlAUqK2U.js" + ] + }, + "resources/js/Pages/Auth/ResetPassword.vue": { + "file": "assets/ResetPassword-BNl7a4X1.js", + "name": "ResetPassword", + "src": "resources/js/Pages/Auth/ResetPassword.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js", + "_GuestLayout-BY3LC-73.js", + "_TextInput-C8CCB_U_.js", + "_PrimaryButton-DuXwr-9M.js", + "_ApplicationLogo-BhIZH06z.js", + "__plugin-vue_export-helper-DlAUqK2U.js" + ] + }, + "resources/js/Pages/Auth/VerifyEmail.vue": { + "file": "assets/VerifyEmail-CyukB_SZ.js", + "name": "VerifyEmail", + "src": "resources/js/Pages/Auth/VerifyEmail.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js", + "_GuestLayout-BY3LC-73.js", + "_PrimaryButton-DuXwr-9M.js", + "_ApplicationLogo-BhIZH06z.js", + "__plugin-vue_export-helper-DlAUqK2U.js" + ] + }, + "resources/js/Pages/Dashboard.vue": { + "file": "assets/Dashboard-DM_LxQy2.js", + "name": "Dashboard", + "src": "resources/js/Pages/Dashboard.vue", + "isDynamicEntry": true, + "imports": [ + "_AuthenticatedLayout-DfWF52N1.js", + "_index-BSdK3M0e.js", + "_ApplicationLogo-BhIZH06z.js", + "__plugin-vue_export-helper-DlAUqK2U.js" + ] + }, + "resources/js/Pages/Profile/Edit.vue": { + "file": "assets/Edit-CYV2sXpe.js", + "name": "Edit", + "src": "resources/js/Pages/Profile/Edit.vue", + "isDynamicEntry": true, + "imports": [ + "_AuthenticatedLayout-DfWF52N1.js", + "resources/js/Pages/Profile/Partials/DeleteUserForm.vue", + "resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue", + "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue", + "_index-BSdK3M0e.js", + "_ApplicationLogo-BhIZH06z.js", + "__plugin-vue_export-helper-DlAUqK2U.js", + "_TextInput-C8CCB_U_.js", + "_PrimaryButton-DuXwr-9M.js" + ] + }, + "resources/js/Pages/Profile/Partials/DeleteUserForm.vue": { + "file": "assets/DeleteUserForm-B1oHFaVP.js", + "name": "DeleteUserForm", + "src": "resources/js/Pages/Profile/Partials/DeleteUserForm.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js", + "__plugin-vue_export-helper-DlAUqK2U.js", + "_TextInput-C8CCB_U_.js" + ] + }, + "resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue": { + "file": "assets/UpdatePasswordForm-CaeWqGla.js", + "name": "UpdatePasswordForm", + "src": "resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js", + "_TextInput-C8CCB_U_.js", + "_PrimaryButton-DuXwr-9M.js", + "__plugin-vue_export-helper-DlAUqK2U.js" + ] + }, + "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue": { + "file": "assets/UpdateProfileInformationForm-CJwkYwQQ.js", + "name": "UpdateProfileInformationForm", + "src": "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js", + "_TextInput-C8CCB_U_.js", + "_PrimaryButton-DuXwr-9M.js", + "__plugin-vue_export-helper-DlAUqK2U.js" + ] + }, + "resources/js/Pages/Welcome.vue": { + "file": "assets/Welcome-D_7l79PQ.js", + "name": "Welcome", + "src": "resources/js/Pages/Welcome.vue", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js" + ] + }, + "resources/js/admin-runtime-import-import.js": { + "file": "assets/admin-runtime-import-import-DKMIaPXC.js", + "name": "admin-runtime-import-import", + "src": "resources/js/admin-runtime-import-import.js", + "isDynamicEntry": true + }, + "resources/js/admin-runtime-import.js": { + "file": "assets/admin-runtime-import-CRvLQy6v.js", + "name": "admin-runtime-import", + "src": "resources/js/admin-runtime-import.js", + "isDynamicEntry": true, + "imports": [ + "_index-BSdK3M0e.js" + ], + "dynamicImports": [ + "resources/js/admin-runtime-import-import.js" + ], + "css": [ + "assets/admin-runtime-import-BlmN0T4U.css" + ] + }, + "resources/js/admin.js": { + "file": "assets/admin-Sefg0Q45.js", + "name": "admin", + "src": "resources/js/admin.js", + "isEntry": true, + "imports": [ + "_index-BSdK3M0e.js" + ], + "dynamicImports": [ + "resources/js/Pages/Auth/ConfirmPassword.vue", + "resources/js/Pages/Auth/ForgotPassword.vue", + "resources/js/Pages/Auth/Login.vue", + "resources/js/Pages/Auth/Register.vue", + "resources/js/Pages/Auth/ResetPassword.vue", + "resources/js/Pages/Auth/VerifyEmail.vue", + "resources/js/Pages/Dashboard.vue", + "resources/js/Pages/Profile/Edit.vue", + "resources/js/Pages/Profile/Partials/DeleteUserForm.vue", + "resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue", + "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue", + "resources/js/Pages/Welcome.vue", + "resources/js/admin-runtime-import.js" + ], + "css": [ + "assets/admin-BctAalm_.css" + ] + }, + "resources/js/app.js": { + "file": "assets/app-lliD09ip.js", + "name": "app", + "src": "resources/js/app.js", + "isEntry": true, + "imports": [ + "_index-BSdK3M0e.js" + ], + "dynamicImports": [ + "resources/js/Pages/Auth/ConfirmPassword.vue", + "resources/js/Pages/Auth/ForgotPassword.vue", + "resources/js/Pages/Auth/Login.vue", + "resources/js/Pages/Auth/Register.vue", + "resources/js/Pages/Auth/ResetPassword.vue", + "resources/js/Pages/Auth/VerifyEmail.vue", + "resources/js/Pages/Dashboard.vue", + "resources/js/Pages/Profile/Edit.vue", + "resources/js/Pages/Profile/Partials/DeleteUserForm.vue", + "resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue", + "resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.vue", + "resources/js/Pages/Welcome.vue" + ] + } +} +