From d69bfe080c6747567736745b94d7587a5703bbd2 Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Fri, 17 Jan 2025 10:20:05 +1000
Subject: [PATCH 1/4] Rough WIP

---
 core/core.services.yml                        |  2 +-
 .../Core/Asset/CssCollectionRenderer.php      | 98 ++++++++++++++++++-
 core/themes/olivero/olivero.libraries.yml     | 10 +-
 3 files changed, 103 insertions(+), 7 deletions(-)

diff --git a/core/core.services.yml b/core/core.services.yml
index 5ffa0d96f4d9..dedc278b68b0 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1649,7 +1649,7 @@ services:
   Drupal\Core\Session\MetadataBag: '@session_manager.metadata_bag'
   asset.css.collection_renderer:
     class: Drupal\Core\Asset\CssCollectionRenderer
-    arguments: [ '@asset.query_string', '@file_url_generator' ]
+    arguments: [ '@asset.query_string', '@file_url_generator' , '@current_route_match', '@request_stack']
   asset.css.collection_optimizer:
     class: Drupal\Core\Asset\CssCollectionOptimizerLazy
     arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@theme.manager', '@library.dependency_resolver', '@request_stack', '@file_system', '@config.factory', '@file_url_generator', '@datetime.time', '@language_manager']
diff --git a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
index 162a7bccbe8c..362545162b85 100644
--- a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
+++ b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
@@ -3,6 +3,9 @@
 namespace Drupal\Core\Asset;
 
 use Drupal\Core\File\FileUrlGeneratorInterface;
+use Drupal\Core\Render\Markup;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
 
 /**
  * Renders CSS assets.
@@ -16,10 +19,16 @@ class CssCollectionRenderer implements AssetCollectionRendererInterface {
    *   The asset query string.
    * @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator
    *   The file URL generator.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $routeMatch
+   *   Current route match.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
+   *   Request stack.
    */
   public function __construct(
     protected AssetQueryStringInterface $assetQueryString,
     protected FileUrlGeneratorInterface $fileUrlGenerator,
+    protected RouteMatchInterface $routeMatch,
+    protected RequestStack $requestStack,
   ) {
   }
 
@@ -76,7 +85,94 @@ public function render(array $css_assets) {
       $elements[] = $element;
     }
 
-    return $elements;
+    return $this->inlineCriticalCss($elements);
+  }
+
+  /**
+   * Uses an inline style tag for any CSS files that are flagged as critical.
+   *
+   * To make use of this, add {attributes: {critical: true} to CSS files in
+   * your theme or module's libraries.yml file.
+   *
+   * @param array $elements
+   *   Existing CSS elements.
+   *
+   * @return array
+   *   CSS elements with any CSS inlined as required.
+   */
+  protected function inlineCriticalCss(array $assets): array {
+    // Only inline CSS if we are using the frontend theme.
+    // Also skip anything other than HTML requests.
+    $request = $this->requestStack->getCurrentRequest();
+    $wrapperFormat = $request->get('_wrapper_format', 'html');
+    // Views AJAX is special and doesn't set wrapper_format properly, so check
+    // for that too.
+    $currentRoute = $this->routeMatch->getRouteName();
+    if (
+      // Not HTML.
+      $wrapperFormat !== 'html'
+      // OR views AJAX.
+      || $currentRoute === 'views.ajax'
+      // OR Big Pipe placeholders.
+      || $request->headers->get('accept') === 'application/vnd.drupal-ajax') {
+      return $assets;
+    }
+
+    $elements = [];
+    foreach ($assets as $asset) {
+      $attributes = $asset['#attributes'];
+      // Skip files with print media.
+      if ($asset['#attributes']['media'] === 'print') {
+        $elements[] = $asset;
+        continue;
+      }
+      // Any CSS file with a critical flag should be inlined.
+      if (isset($attributes['critical'])) {
+        $inlineCriticalCSS = $this->inlineCssFile(\substr($attributes['href'], 1));
+        if ($inlineCriticalCSS !== NULL) {
+          $elements[] = $inlineCriticalCSS;
+          continue;
+        }
+      }
+      // @todo Defer all non critical CSS -
+      //   https://www.drupal.org/project/drupal/issues/2989324
+      $elements[] = $asset;
+    }
+
+    return \array_filter($elements);
+  }
+
+  /**
+   * Turn a file into a renderable <style> tag.
+   *
+   * @param string $path
+   *   File URL to turn into inline CSS.
+   *
+   * @return array|null
+   *   Return NULL if the file does not exist.
+   */
+  protected function inlineCssFile(string $path): ?array {
+    $path = \strtok($path, "?");
+    if (!\is_string($path)) {
+      return NULL;
+    }
+    $file_name = \sprintf('%s/%s', DRUPAL_ROOT, $path);
+    if (\file_exists($file_name)) {
+      $contents = \file_get_contents($path);
+      if (!\is_string($contents)) {
+        return NULL;
+      }
+      return [
+        '#type' => 'html_tag',
+        '#tag' => 'style',
+        '#value' => Markup::create($contents),
+        '#attributes' => [
+          // Add a data-src attribute to aid in debugging/identification.
+          'data-src' => \basename($path),
+        ],
+      ];
+    }
+    return NULL;
   }
 
 }
diff --git a/core/themes/olivero/olivero.libraries.yml b/core/themes/olivero/olivero.libraries.yml
index d759787b762a..92b1ed552813 100644
--- a/core/themes/olivero/olivero.libraries.yml
+++ b/core/themes/olivero/olivero.libraries.yml
@@ -2,12 +2,12 @@ global-styling:
   version: VERSION
   css:
     base:
-      css/base/fonts.css: {}
-      css/base/variables.css: {}
-      css/base/base.css: {}
+      css/base/fonts.css: { attributes: { critical: true } }
+      css/base/variables.css: { attributes: { critical: true } }
+      css/base/base.css: { attributes: { critical: true } }
     layout:
-      css/layout/layout.css: {}
-      css/layout/grid.css: {}
+      css/layout/layout.css: { attributes: { critical: true } }
+      css/layout/grid.css: { attributes: { critical: true } }
       css/layout/layout-content-narrow.css: {}
       css/layout/layout-content-medium.css: {}
       css/layout/layout-footer.css: {}
-- 
GitLab


From 2bea4be2c4810796888bd23b23c12b3d2650e87d Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Fri, 17 Jan 2025 10:25:32 +1000
Subject: [PATCH 2/4] Opt out of aggregation for criticals

---
 core/themes/olivero/olivero.libraries.yml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/core/themes/olivero/olivero.libraries.yml b/core/themes/olivero/olivero.libraries.yml
index 92b1ed552813..0478b8efd268 100644
--- a/core/themes/olivero/olivero.libraries.yml
+++ b/core/themes/olivero/olivero.libraries.yml
@@ -2,12 +2,12 @@ global-styling:
   version: VERSION
   css:
     base:
-      css/base/fonts.css: { attributes: { critical: true } }
-      css/base/variables.css: { attributes: { critical: true } }
-      css/base/base.css: { attributes: { critical: true } }
+      css/base/fonts.css: { attributes: { critical: true }, preprocess: false }
+      css/base/variables.css: { attributes: { critical: true }, preprocess: false }
+      css/base/base.css: { attributes: { critical: true }, preprocess: false }
     layout:
-      css/layout/layout.css: { attributes: { critical: true } }
-      css/layout/grid.css: { attributes: { critical: true } }
+      css/layout/layout.css: { attributes: { critical: true }, preprocess: false }
+      css/layout/grid.css: { attributes: { critical: true }, preprocess: false }
       css/layout/layout-content-narrow.css: {}
       css/layout/layout-content-medium.css: {}
       css/layout/layout-footer.css: {}
-- 
GitLab


From 53986bab2825230cf6ddb8b899fc967bdae9b272 Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Fri, 17 Jan 2025 10:33:37 +1000
Subject: [PATCH 3/4] Add test coverage

---
 core/lib/Drupal/Core/Asset/CssCollectionRenderer.php    | 3 ++-
 core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
index 362545162b85..747873703277 100644
--- a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
+++ b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
@@ -92,7 +92,8 @@ public function render(array $css_assets) {
    * Uses an inline style tag for any CSS files that are flagged as critical.
    *
    * To make use of this, add {attributes: {critical: true} to CSS files in
-   * your theme or module's libraries.yml file.
+   * your theme or module's libraries.yml file. Be sure to mark the files as
+   * preprocess: false too so they're not aggregated.
    *
    * @param array $elements
    *   Existing CSS elements.
diff --git a/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php b/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php
index c3be4d60279e..d63440da6664 100644
--- a/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php
+++ b/core/tests/Drupal/FunctionalTests/Theme/OliveroTest.php
@@ -41,7 +41,7 @@ class OliveroTest extends BrowserTestBase {
   public function testBaseLibraryAvailable(): void {
     $this->drupalGet('');
     $this->assertSession()->statusCodeEquals(200);
-    $this->assertSession()->responseContains('olivero/css/base/base.css');
+    $this->assertSession()->elementExists('css', 'style[data-src="base.css"]');
     $this->assertSession()->responseContains('olivero/js/navigation-utils.js');
   }
 
-- 
GitLab


From fbf7a78dd041d039e8ba1e50f92dc5a9948c606f Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Fri, 17 Jan 2025 10:35:13 +1000
Subject: [PATCH 4/4] Lint

---
 core/lib/Drupal/Core/Asset/CssCollectionRenderer.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
index 747873703277..4aae4316ccab 100644
--- a/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
+++ b/core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
@@ -95,7 +95,7 @@ public function render(array $css_assets) {
    * your theme or module's libraries.yml file. Be sure to mark the files as
    * preprocess: false too so they're not aggregated.
    *
-   * @param array $elements
+   * @param array $assets
    *   Existing CSS elements.
    *
    * @return array
-- 
GitLab