diff --git a/core/core.services.yml b/core/core.services.yml index be54cc82ecf35a8f48e703c6644d2f4c83ebfba8..c08088c36a68ff060fbf38acc69a3f3d42ccc17e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1676,7 +1676,7 @@ services: arguments: [ '@asset.query_string', '@file_url_generator' ] 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'] + 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', '@asset.css.dumper', '@state'] asset.css.optimizer: class: Drupal\Core\Asset\CssOptimizer arguments: ['@file_url_generator'] @@ -1690,7 +1690,7 @@ services: arguments: [ '@asset.query_string','@file_url_generator', '@datetime.time' ] asset.js.collection_optimizer: class: Drupal\Core\Asset\JsCollectionOptimizerLazy - arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@theme.manager', '@library.dependency_resolver', '@request_stack', '@file_system', '@config.factory', '@file_url_generator', '@datetime.time', '@language_manager'] + arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@theme.manager', '@library.dependency_resolver', '@request_stack', '@file_system', '@config.factory', '@file_url_generator', '@datetime.time', '@language_manager', '@asset.css.dumper', '@state'] asset.js.optimizer: class: Drupal\Core\Asset\JsOptimizer arguments: ['@logger.channel.default'] diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php index 8eed354b552dab4aace0099ed68146be74d7c71d..243e604b13d94c6454b2f1aabce92aa6a32a6567 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerLazy.php @@ -8,6 +8,7 @@ use Drupal\Core\File\FileSystemInterface; use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\State\StateInterface; use Drupal\Core\Theme\ThemeManagerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -18,6 +19,20 @@ class CssCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterfa use AssetGroupSetHashTrait; + /** + * An asset dumper. + * + * @var \Drupal\Core\Asset\AssetDumper + */ + protected $dumper; + + /** + * The state key/value store. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + /** * Constructs a CssCollectionOptimizerLazy. * @@ -53,7 +68,11 @@ public function __construct( protected readonly FileUrlGeneratorInterface $fileUrlGenerator, protected readonly TimeInterface $time, protected readonly LanguageManagerInterface $languageManager, - ) {} + AssetDumperInterface $dumper, + StateInterface $state + ) { + $this->dumper = $dumper; + $this->state = $state;} /** * {@inheritdoc} @@ -88,7 +107,49 @@ public function optimize(array $css_assets, array $libraries) { // we must know the entire set of libraries used to generate all CSS // groups, whether or not files in a group are from a particular // library or not. - $css_assets[$order]['preprocessed'] = TRUE; + $key = $this->generateHash($css_group); + $uri = ''; + if (isset($map[$key])) { + $uri = $map[$key]; + } + if (empty($uri) || !file_exists($uri)) { + // Optimize each asset within the group. + $data = ''; + $current_license = FALSE; + foreach ($css_group['items'] as $css_asset) { + // Ensure license information is available as a comment after + // optimization. + if ($css_asset['license'] !== $current_license) { + $data .= "/* @license " . $css_asset['license']['name'] . " " . $css_asset['license']['url'] . " */\n"; + } + $current_license = $css_asset['license']; + $data .= $this->optimizer->optimize($css_asset); + } + // Per the W3C specification at + // http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import + // rules must precede any other style, so we move those to the + // top. The regular expression is expressed in NOWDOC since it is + // detecting backslashes as well as single and double quotes. It + // is difficult to read when represented as a quoted string. + $regexp = <<<'REGEXP' +/@import\s*(?:'(?:\\'|.)*'|"(?:\\"|.)*"|url\(\s*(?:\\[\)\'\"]|[^'")])*\s*\)|url\(\s*'(?:\'|.)*'\s*\)|url\(\s*"(?:\"|.)*"\s*\)).*;/iU +REGEXP; + preg_match_all($regexp, $data, $matches); + $data = preg_replace($regexp, '', $data); + $data = implode('', $matches[0]) . (!empty($matches[0]) ? "\n" : '') . $data; + // Dump the optimized CSS for this group into an aggregate file. + $uri = $this->dumper->dump($data, 'css'); + // Set the URI for this group's aggregate file. + $css_assets[$order]['data'] = $uri; + // Persist the URI for this aggregate file. + $map[$key] = $uri; + $this->state->set('drupal_css_cache_files', $map); + } + else { + // Use the persisted URI for the optimized CSS file. + $css_assets[$order]['data'] = $uri; + } + $css_assets[$order]['preprocessed'] = TRUE; } } if ($css_group['type'] === 'external') { @@ -112,18 +173,6 @@ public function optimize(array $css_assets, array $libraries) { $query_args['exclude'] = UrlHelper::compressQueryParameter(implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($already_loaded))); } - // Generate a URL for each group of assets, but do not process them inline, - // this is done using optimizeGroup() when the asset path is requested. - foreach ($css_assets as $order => $css_asset) { - if (!empty($css_asset['preprocessed'])) { - $query = ['delta' => "$order"] + $query_args; - $filename = 'css_' . $this->generateHash($css_asset) . '.css'; - $uri = 'assets://css/' . $filename; - $css_assets[$order]['data'] = $this->fileUrlGenerator->generateString($uri) . '?' . UrlHelper::buildQuery($query); - } - unset($css_assets[$order]['items']); - } - return $css_assets; } diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php index 8178c09c3e9faba5d4e1afb5e11cfe96f34b89f1..e63eff79c3070f1045852db4304b0aa49d17cd12 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php @@ -8,6 +8,7 @@ use Drupal\Core\File\FileSystemInterface; use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\State\StateInterface; use Drupal\Core\Theme\ThemeManagerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -18,6 +19,20 @@ class JsCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterfac use AssetGroupSetHashTrait; + /** + * An asset dumper. + * + * @var \Drupal\Core\Asset\AssetDumper + */ + protected $dumper; + + /** + * The state key/value store. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; + /** * Constructs a JsCollectionOptimizerLazy. * @@ -53,7 +68,12 @@ public function __construct( protected readonly FileUrlGeneratorInterface $fileUrlGenerator, protected readonly TimeInterface $time, protected readonly LanguageManagerInterface $languageManager, - ) {} + AssetDumperInterface $dumper, + StateInterface $state + ) { + $this->dumper = $dumper; + $this->state = $state; + } /** * {@inheritdoc} @@ -85,10 +105,47 @@ public function optimize(array $js_assets, array $libraries) { $js_assets[$order]['data'] = $uri; } else { - // To reproduce the full context of assets outside of the request, - // we must know the entire set of libraries used to generate all CSS - // groups, whether or not files in a group are from a particular - // library or not. + $key = $this->generateHash($js_group); + $uri = ''; + if (isset($map[$key])) { + $uri = $map[$key]; + } + if (empty($uri) || !file_exists($uri)) { + // Concatenate each asset within the group. + $data = ''; + $current_license = FALSE; + foreach ($js_group['items'] as $js_asset) { + // Ensure license information is available as a comment after + // optimization. + if ($js_asset['license'] !== $current_license) { + $data .= "/* @license " . $js_asset['license']['name'] . " " . $js_asset['license']['url'] . " */\n"; + } + $current_license = $js_asset['license']; + // Optimize this JS file, but only if it's not yet minified. + if (isset($js_asset['minified']) && $js_asset['minified']) { + $data .= file_get_contents($js_asset['data']); + } + else { + $data .= $this->optimizer->optimize($js_asset); + } + // Append a ';' and a newline after each JS file to prevent them + // from running together. + $data .= ";\n"; + } + // Remove unwanted JS code that cause issues. + $data = $this->optimizer->clean($data); + // Dump the optimized JS for this group into an aggregate file. + $uri = $this->dumper->dump($data, 'js'); + // Set the URI for this group's aggregate file. + $js_assets[$order]['data'] = $uri; + // Persist the URI for this aggregate file. + $map[$key] = $uri; + $this->state->set('system.js_cache_files', $map); + } + else { + // Use the persisted URI for the optimized JS file. + $js_assets[$order]['data'] = $uri; + } $js_assets[$order]['preprocessed'] = TRUE; } break; @@ -120,23 +177,6 @@ public function optimize(array $js_assets, array $libraries) { if ($already_loaded) { $query_args['exclude'] = UrlHelper::compressQueryParameter(implode(',', $this->dependencyResolver->getMinimalRepresentativeSubset($already_loaded))); } - - // Generate a URL for the group, but do not process it inline, this is - // done by \Drupal\system\controller\JsAssetController. - foreach ($js_assets as $order => $js_asset) { - if (!empty($js_asset['preprocessed'])) { - $query = [ - 'scope' => $js_asset['scope'] === 'header' ? 'header' : 'footer', - 'delta' => "$order", - ] + $query_args; - // Add a filename prefix to mitigate ad blockers which can block - // any script beginning with 'ad'. - $filename = 'js_' . $this->generateHash($js_asset) . '.js'; - $uri = 'assets://js/' . $filename; - $js_assets[$order]['data'] = $this->fileUrlGenerator->generateString($uri) . '?' . UrlHelper::buildQuery($query); - } - unset($js_assets[$order]['items']); - } } return $js_assets;