Skip to content

Commit ae9f78a

Browse files
[9.x] Support preloading assets with Vite (#44096)
* support (module)preloading with Vite * sort preloading tags while generating * formatting Co-authored-by: Taylor Otwell <[email protected]>
1 parent ef96440 commit ae9f78a

File tree

5 files changed

+853
-18
lines changed

5 files changed

+853
-18
lines changed

src/Illuminate/Foundation/Vite.php

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,37 @@ class Vite implements Htmlable
6262
*/
6363
protected $styleTagAttributesResolvers = [];
6464

65+
/**
66+
* The preload tag attributes resolvers.
67+
*
68+
* @var array
69+
*/
70+
protected $preloadTagAttributesResolvers = [];
71+
72+
/**
73+
* The preloaded assets.
74+
*
75+
* @var array
76+
*/
77+
protected $preloadedAssets = [];
78+
6579
/**
6680
* The cached manifest files.
6781
*
6882
* @var array
6983
*/
7084
protected static $manifests = [];
7185

86+
/**
87+
* Get the preloaded assets.
88+
*
89+
* @var array
90+
*/
91+
public function preloadedAssets()
92+
{
93+
return $this->preloadedAssets;
94+
}
95+
7296
/**
7397
* Get the Content Security Policy nonce applied to all generated tags.
7498
*
@@ -186,6 +210,23 @@ public function useStyleTagAttributes($attributes)
186210
return $this;
187211
}
188212

213+
/**
214+
* Use the given callback to resolve attributes for preload tags.
215+
*
216+
* @param (callable(string, string, ?array, ?array): array)|array $attributes
217+
* @return $this
218+
*/
219+
public function usePreloadTagAttributes($attributes)
220+
{
221+
if (! is_callable($attributes)) {
222+
$attributes = fn () => $attributes;
223+
}
224+
225+
$this->preloadTagAttributesResolvers[] = $attributes;
226+
227+
return $this;
228+
}
229+
189230
/**
190231
* Generate Vite tags for an entrypoint.
191232
*
@@ -212,14 +253,36 @@ public function __invoke($entrypoints, $buildDirectory = null)
212253
$manifest = $this->manifest($buildDirectory);
213254

214255
$tags = collect();
256+
$preloads = collect();
215257

216258
foreach ($entrypoints as $entrypoint) {
217259
$chunk = $this->chunk($manifest, $entrypoint);
218260

261+
$preloads->push([
262+
$chunk['src'],
263+
$this->assetPath("{$buildDirectory}/{$chunk['file']}"),
264+
$chunk,
265+
$manifest
266+
]);
267+
219268
foreach ($chunk['imports'] ?? [] as $import) {
269+
$preloads->push([
270+
$import,
271+
$this->assetPath("{$buildDirectory}/{$manifest[$import]['file']}"),
272+
$manifest[$import],
273+
$manifest
274+
]);
275+
220276
foreach ($manifest[$import]['css'] ?? [] as $css) {
221277
$partialManifest = Collection::make($manifest)->where('file', $css);
222278

279+
$preloads->push([
280+
$partialManifest->keys()->first(),
281+
$this->assetPath("{$buildDirectory}/{$css}"),
282+
$partialManifest->first(),
283+
$manifest
284+
]);
285+
223286
$tags->push($this->makeTagForChunk(
224287
$partialManifest->keys()->first(),
225288
$this->assetPath("{$buildDirectory}/{$css}"),
@@ -239,6 +302,13 @@ public function __invoke($entrypoints, $buildDirectory = null)
239302
foreach ($chunk['css'] ?? [] as $css) {
240303
$partialManifest = Collection::make($manifest)->where('file', $css);
241304

305+
$preloads->push([
306+
$partialManifest->keys()->first(),
307+
$this->assetPath("{$buildDirectory}/{$css}"),
308+
$partialManifest->first(),
309+
$manifest
310+
]);
311+
242312
$tags->push($this->makeTagForChunk(
243313
$partialManifest->keys()->first(),
244314
$this->assetPath("{$buildDirectory}/{$css}"),
@@ -250,7 +320,10 @@ public function __invoke($entrypoints, $buildDirectory = null)
250320

251321
[$stylesheets, $scripts] = $tags->partition(fn ($tag) => str_starts_with($tag, '<link'));
252322

253-
return new HtmlString($stylesheets->join('').$scripts->join(''));
323+
$preloads = $preloads->sortByDesc(fn ($args) => $this->isCssPath($args[1]))
324+
->map(fn ($args) => $this->makePreloadTagForChunk(...$args));
325+
326+
return new HtmlString($preloads->join('').$stylesheets->join('').$scripts->join(''));
254327
}
255328

256329
/**
@@ -286,6 +359,26 @@ protected function makeTagForChunk($src, $url, $chunk, $manifest)
286359
);
287360
}
288361

362+
/**
363+
* Make a preload tag for the given chunk.
364+
*
365+
* @param string $src
366+
* @param string $url
367+
* @param array $chunk
368+
* @param array $manifest
369+
* @return string|null
370+
*/
371+
protected function makePreloadTagForChunk($src, $url, $chunk, $manifest)
372+
{
373+
$attributes = $this->resolvePreloadTagAttributes($src, $url, $chunk, $manifest);
374+
375+
$this->preloadedAssets[$url] = $this->parseAttributes(
376+
Collection::make($attributes)->forget('href')->all()
377+
);
378+
379+
return '<link '.implode(' ', $this->parseAttributes($attributes)).' />';
380+
}
381+
289382
/**
290383
* Resolve the attributes for the chunks generated script tag.
291384
*
@@ -330,6 +423,37 @@ protected function resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)
330423
return $attributes;
331424
}
332425

426+
/**
427+
* Resolve the attributes for the chunks generated preload tag.
428+
*
429+
* @param string $src
430+
* @param string $url
431+
* @param array $chunk
432+
* @param array $manifest
433+
* @return array
434+
*/
435+
protected function resolvePreloadTagAttributes($src, $url, $chunk, $manifest)
436+
{
437+
$attributes = $this->isCssPath($url) ? [
438+
'rel' => 'preload',
439+
'as' => 'style',
440+
'href' => $url,
441+
] : [
442+
'rel' => 'modulepreload',
443+
'href' => $url,
444+
];
445+
446+
$attributes = $this->integrityKey !== false
447+
? array_merge($attributes, ['integrity' => $chunk[$this->integrityKey] ?? false])
448+
: $attributes;
449+
450+
foreach ($this->preloadTagAttributesResolvers as $resolver) {
451+
$attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));
452+
}
453+
454+
return $attributes;
455+
}
456+
333457
/**
334458
* Generate an appropriate tag for the given URL in HMR mode.
335459
*
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Illuminate\Http\Middleware;
4+
5+
use Illuminate\Support\Collection;
6+
use Illuminate\Support\Facades\Vite;
7+
8+
class AddLinkHeadersForPreloadedAssets
9+
{
10+
/**
11+
* Handle the incoming request.
12+
*
13+
* @param \Illuminate\Http\Request $request
14+
* @param \Closure $next
15+
* @return \Illuminate\Http\Response
16+
*/
17+
public function handle($request, $next)
18+
{
19+
return tap($next($request), function ($response) {
20+
if (Vite::preloadedAssets() !== []) {
21+
$response->header('Link', Collection::make(Vite::preloadedAssets())
22+
->map(fn ($attributes, $url) => "<{$url}>; ".implode('; ', $attributes))
23+
->join(', '));
24+
}
25+
});
26+
}
27+
}

0 commit comments

Comments
 (0)