From 2995c16c476a542113460259553054904d44cbb7 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Tue, 3 Oct 2023 13:24:19 -0400 Subject: [PATCH] [AssetMapper] Automatically preload CSS files if WebLink available --- .../Resources/config/asset_mapper.php | 1 + .../Component/AssetMapper/CHANGELOG.md | 1 + .../ImportMap/ImportMapRenderer.php | 32 +++++++++++++++ .../Tests/ImportMap/ImportMapRendererTest.php | 39 +++++++++++++++++++ .../Component/AssetMapper/composer.json | 3 +- 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php index 729effe6b5996..296358cfcf72c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php @@ -174,6 +174,7 @@ param('kernel.charset'), abstract_arg('polyfill URL'), abstract_arg('script HTML attributes'), + service('request_stack'), ]) ->set('asset_mapper.importmap.auditor', ImportMapAuditor::class) diff --git a/src/Symfony/Component/AssetMapper/CHANGELOG.md b/src/Symfony/Component/AssetMapper/CHANGELOG.md index b7402e73e7d34..012dde82a8a26 100644 --- a/src/Symfony/Component/AssetMapper/CHANGELOG.md +++ b/src/Symfony/Component/AssetMapper/CHANGELOG.md @@ -9,6 +9,7 @@ CHANGELOG * Add "entrypoints" concept to the importmap * Always download packages locally instead of using a CDN * Allow relative path strings in the importmap + * Automatically set `_links` attribute for preload CSS files for WebLink integration * Add `PreAssetsCompileEvent` event when running `asset-map:compile` * Add support for importmap paths to use the Asset component (for subdirectories) * Removed the `importmap:export` command diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php index 00d48fe71949f..8e54da6c0ba60 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php @@ -11,7 +11,13 @@ namespace Symfony\Component\AssetMapper\ImportMap; +use Psr\Link\EvolvableLinkProviderInterface; use Symfony\Component\Asset\Packages; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; +use Symfony\Component\WebLink\GenericLinkProvider; +use Symfony\Component\WebLink\Link; /** * @author Kévin Dunglas @@ -27,6 +33,7 @@ public function __construct( private readonly string $charset = 'UTF-8', private readonly string|false $polyfillUrl = ImportMapManager::POLYFILL_URL, private readonly array $scriptAttributes = [], + private readonly ?RequestStack $requestStack = null, ) { } @@ -68,6 +75,10 @@ public function render(string|array $entryPoint, array $attributes = []): string $output .= "\n"; } + if (class_exists(AddLinkHeaderListener::class) && $request = $this->requestStack?->getCurrentRequest()) { + $this->addWebLinkPreloads($request, $cssLinks); + } + $scriptAttributes = $this->createAttributesString($attributes); $importMapJson = json_encode(['imports' => $importMap], \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG); $output .= << new Link('preload', $url), $cssLinks); + + if (null === $linkProvider = $request->attributes->get('_links')) { + $request->attributes->set('_links', new GenericLinkProvider($cssPreloadLinks)); + + return; + } + + if (!$linkProvider instanceof EvolvableLinkProviderInterface) { + return; + } + + foreach ($cssPreloadLinks as $link) { + $linkProvider = $linkProvider->withLink($link); + } + + $request->attributes->set('_links', $linkProvider); + } } diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php index 550d56fc6a1b2..d9f5653b9b38b 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php @@ -15,6 +15,9 @@ use Symfony\Component\Asset\Packages; use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\WebLink\GenericLinkProvider; class ImportMapRendererTest extends TestCase { @@ -130,4 +133,40 @@ private function createBasicImportMapManager(): ImportMapManager return $importMapManager; } + + public function testItAddsPreloadLinks() + { + $importMapManager = $this->createMock(ImportMapManager::class); + $importMapManager->expects($this->once()) + ->method('getImportMapData') + ->willReturn([ + 'app_js_preload' => [ + 'path' => '/assets/app-preload-d1g35t.js', + 'type' => 'js', + 'preload' => true, + ], + 'app_css_preload' => [ + 'path' => '/assets/styles/app-preload-d1g35t.css', + 'type' => 'css', + 'preload' => true, + ], + 'app_css_no_preload' => [ + 'path' => '/assets/styles/app-nopreload-d1g35t.css', + 'type' => 'css', + ], + ]); + + $request = Request::create('/foo'); + $requestStack = new RequestStack(); + $requestStack->push($request); + + $renderer = new ImportMapRenderer($importMapManager, requestStack: $requestStack); + $renderer->render(['app']); + + $linkProvider = $request->attributes->get('_links'); + $this->assertInstanceOf(GenericLinkProvider::class, $linkProvider); + $this->assertCount(1, $linkProvider->getLinks()); + $this->assertSame(['preload'], $linkProvider->getLinks()[0]->getRels()); + $this->assertSame('/assets/styles/app-preload-d1g35t.css', $linkProvider->getLinks()[0]->getHref()); + } } diff --git a/src/Symfony/Component/AssetMapper/composer.json b/src/Symfony/Component/AssetMapper/composer.json index 33b0a2e89367d..3ff5377fbdac8 100644 --- a/src/Symfony/Component/AssetMapper/composer.json +++ b/src/Symfony/Component/AssetMapper/composer.json @@ -29,7 +29,8 @@ "symfony/finder": "^5.4|^6.0|^7.0", "symfony/framework-bundle": "^6.4|^7.0", "symfony/http-foundation": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0" + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/web-link": "^5.4|^6.0|^7.0" }, "conflict": { "symfony/framework-bundle": "<6.4"