Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Support preloading assets with Vite #44096

Merged
merged 3 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 125 additions & 1 deletion src/Illuminate/Foundation/Vite.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,37 @@ class Vite implements Htmlable
*/
protected $styleTagAttributesResolvers = [];

/**
* The preload tag attributes resolvers.
*
* @var array
*/
protected $preloadTagAttributesResolvers = [];

/**
* The preloaded assets.
*
* @var array
*/
protected $preloadedAssets = [];

/**
* The cached manifest files.
*
* @var array
*/
protected static $manifests = [];

/**
* Get the preloaded assets.
*
* @var array
*/
public function preloadedAssets()
{
return $this->preloadedAssets;
}

/**
* Get the Content Security Policy nonce applied to all generated tags.
*
Expand Down Expand Up @@ -186,6 +210,23 @@ public function useStyleTagAttributes($attributes)
return $this;
}

/**
* Use the given callback to resolve attributes for preload tags.
*
* @param (callable(string, string, ?array, ?array): array)|array $attributes
* @return $this
*/
public function usePreloadTagAttributes($attributes)
{
if (! is_callable($attributes)) {
$attributes = fn () => $attributes;
}

$this->preloadTagAttributesResolvers[] = $attributes;

return $this;
}

/**
* Generate Vite tags for an entrypoint.
*
Expand All @@ -212,14 +253,36 @@ public function __invoke($entrypoints, $buildDirectory = null)
$manifest = $this->manifest($buildDirectory);

$tags = collect();
$preloads = collect();

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

$preloads->push([
$chunk['src'],
$this->assetPath("{$buildDirectory}/{$chunk['file']}"),
$chunk,
$manifest
]);

foreach ($chunk['imports'] ?? [] as $import) {
$preloads->push([
$import,
$this->assetPath("{$buildDirectory}/{$manifest[$import]['file']}"),
$manifest[$import],
$manifest
]);

foreach ($manifest[$import]['css'] ?? [] as $css) {
$partialManifest = Collection::make($manifest)->where('file', $css);

$preloads->push([
$partialManifest->keys()->first(),
$this->assetPath("{$buildDirectory}/{$css}"),
$partialManifest->first(),
$manifest
]);

$tags->push($this->makeTagForChunk(
$partialManifest->keys()->first(),
$this->assetPath("{$buildDirectory}/{$css}"),
Expand All @@ -239,6 +302,13 @@ public function __invoke($entrypoints, $buildDirectory = null)
foreach ($chunk['css'] ?? [] as $css) {
$partialManifest = Collection::make($manifest)->where('file', $css);

$preloads->push([
$partialManifest->keys()->first(),
$this->assetPath("{$buildDirectory}/{$css}"),
$partialManifest->first(),
$manifest
]);

$tags->push($this->makeTagForChunk(
$partialManifest->keys()->first(),
$this->assetPath("{$buildDirectory}/{$css}"),
Expand All @@ -250,7 +320,10 @@ public function __invoke($entrypoints, $buildDirectory = null)

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

return new HtmlString($stylesheets->join('').$scripts->join(''));
$preloads = $preloads->sortByDesc(fn ($args) => $this->isCssPath($args[1]))
->map(fn ($args) => $this->makePreloadTagForChunk(...$args));

return new HtmlString($preloads->join('').$stylesheets->join('').$scripts->join(''));
}

/**
Expand Down Expand Up @@ -286,6 +359,26 @@ protected function makeTagForChunk($src, $url, $chunk, $manifest)
);
}

/**
* Make a preload tag for the given chunk.
*
* @param string $src
* @param string $url
* @param array $chunk
* @param array $manifest
* @return string|null
*/
protected function makePreloadTagForChunk($src, $url, $chunk, $manifest)
{
$attributes = $this->resolvePreloadTagAttributes($src, $url, $chunk, $manifest);

$this->preloadedAssets[$url] = $this->parseAttributes(
Collection::make($attributes)->forget('href')->all()
);

return '<link '.implode(' ', $this->parseAttributes($attributes)).' />';
}

/**
* Resolve the attributes for the chunks generated script tag.
*
Expand Down Expand Up @@ -330,6 +423,37 @@ protected function resolveStylesheetTagAttributes($src, $url, $chunk, $manifest)
return $attributes;
}

/**
* Resolve the attributes for the chunks generated preload tag.
*
* @param string $src
* @param string $url
* @param array $chunk
* @param array $manifest
* @return array
*/
protected function resolvePreloadTagAttributes($src, $url, $chunk, $manifest)
{
$attributes = $this->isCssPath($url) ? [
'rel' => 'preload',
'as' => 'style',
'href' => $url,
] : [
'rel' => 'modulepreload',
'href' => $url,
];

$attributes = $this->integrityKey !== false
? array_merge($attributes, ['integrity' => $chunk[$this->integrityKey] ?? false])
: $attributes;

foreach ($this->preloadTagAttributesResolvers as $resolver) {
$attributes = array_merge($attributes, $resolver($src, $url, $chunk, $manifest));
}

return $attributes;
}

/**
* Generate an appropriate tag for the given URL in HMR mode.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Illuminate\Http\Middleware;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Vite;

class AddLinkHeadersForPreloadedAssets
{
/**
* Handle the incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Illuminate\Http\Response
*/
public function handle($request, $next)
{
return tap($next($request), function ($response) {
if (Vite::preloadedAssets() !== []) {
$response->header('Link', Collection::make(Vite::preloadedAssets())
->map(fn ($attributes, $url) => "<{$url}>; ".implode('; ', $attributes))
->join(', '));
}
});
}
}
Loading