core/core.services.yml | 8 +++- core/includes/common.inc | 9 +++- .../Drupal/Core/Asset/JsCollectionOptimizer.php | 46 ++++++------------- .../Core/Asset/JsLicenseWebLabelsAnnotator.php | 53 ++++++++++++++++++++++ .../src/Controller/AssetLicenseInfoController.php | 36 ++++++++++++--- .../system/src/Tests/Common/JavaScriptTest.php | 6 +-- 6 files changed, 116 insertions(+), 42 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index fa5ee9f..d43ae9e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -886,13 +886,19 @@ services: arguments: [ '@state' ] asset.js.collection_optimizer: class: Drupal\Core\Asset\JsCollectionOptimizer - arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@asset.js.dumper', '@state', '@url_generator' ] + arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@asset.js.dumper', '@state' ] asset.js.optimizer: class: Drupal\Core\Asset\JsOptimizer asset.js.collection_grouper: class: Drupal\Core\Asset\JsCollectionGrouper asset.js.dumper: class: Drupal\Core\Asset\AssetDumper + asset.js.optimizer_license_web_labels_annotator: + class: Drupal\Core\Asset\JsLicenseWebLabelsAnnotator + arguments: [ '@url_generator' ] + asset.js.collection_optimizer_license_web_labels_annotator: + class: Drupal\Core\Asset\JsCollectionOptimizer + arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer_license_web_labels_annotator', '@asset.js.dumper', '@state', 'system.js_license_web_labels_files' ] library.discovery: class: Drupal\Core\Asset\LibraryDiscovery arguments: ['@library.discovery.collector'] diff --git a/core/includes/common.inc b/core/includes/common.inc index 6c6c4a3..00699f0 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1994,7 +1994,14 @@ function drupal_pre_render_scripts($elements) { // Aggregate the JavaScript if necessary, but only during normal site // operation. if (!defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('js.preprocess')) { - $js_assets = \Drupal::service('asset.js.collection_optimizer')->optimize($js_assets); + $optimized_js_assets = \Drupal::service('asset.js.collection_optimizer')->optimize($js_assets); + // Also create the alternative version of the aggregate that is unoptimized, + // but annotated with deep links to the license information on the + // JavaScript License Web Labels page. + // @see \Drupal\system\Controller\AssetLicenseInfoController + \Drupal::service('asset.js.collection_optimizer_license_web_labels_annotator')->optimize($js_assets); + // Now the optimized JavaScript assets are the ones to be rendered. + $js_assets = $optimized_js_assets; } return \Drupal::service('asset.js.collection_renderer')->render($js_assets); } diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php index b354325..99b27b5 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php @@ -6,7 +6,6 @@ namespace Drupal\Core\Asset; -use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\State\StateInterface; /** @@ -43,11 +42,11 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface { protected $state; /** - * The URL generator. + * The name for the JavaScript asset collections optimized by this instance. * - * @var \Drupal\Core\Routing\UrlGeneratorInterface + * @var string */ - protected $urlGenerator; + protected $name; /** * Constructs a JsCollectionOptimizer. @@ -60,15 +59,16 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface { * The dumper for optimized JS assets. * @param \Drupal\Core\State\StateInterface * The state key/value store. - * @param \Drupal\Core\Routing\UrlGeneratorInterface - * The URL generator. + * @param string + * (optional) The name for the JavaScript asset collections optimized by + * this instance. */ - public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state, UrlGeneratorInterface $url_generator) { + public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state, $name = 'system.js_cache_files') { $this->grouper = $grouper; $this->optimizer = $optimizer; $this->dumper = $dumper; $this->state = $state; - $this->urlGenerator = $url_generator; + $this->name = $name; } /** @@ -89,14 +89,13 @@ public function optimize(array $js_assets) { // Group the assets. $js_groups = $this->grouper->group($js_assets); - // Now optimize each asset in the asset group, concatenate them to generate - // the aggregate and finally dump the result. Also create an unoptimized - // equivalent, for JavaScript Web License Labels compliance. We track the - // aggregates we've already generated, to avoid all this work if possible. + // Now optimize (concatenate, not minify) and dump each asset group, unless + // that was already done, in which case it should appear in + // system.js_cache_files. // Drupal contrib can override this default JS aggregator to keep the same // grouping, optimizing and dumping, but change the strategy that is used to // determine when the aggregate should be rebuilt (e.g. mtime, HTTPS …). - $map = $this->state->get('system.js_optimized_aggregate_files') ?: array(); + $map = $this->state->get($this->name) ?: array(); $js_assets = array(); foreach ($js_groups as $order => $js_group) { // We have to return a single asset, not a group of assets. It is now up @@ -120,7 +119,6 @@ public function optimize(array $js_assets) { $uri = $map[$key]; } if (empty($uri) || !file_exists($uri)) { - $unoptimized_map = $this->state->get('system.js_unoptimized_aggregate_files') ?: array(); // Concatenate and optimize each asset within the group. $data = ''; foreach ($js_group['items'] as $js_asset) { @@ -135,21 +133,7 @@ public function optimize(array $js_assets) { $js_assets[$order]['data'] = $uri; // Persist the URI for this aggregate file. $map[$key] = $uri; - $this->state->set('system.js_optimized_aggregate_files', $map); - - // For JavaScript License Web Labels compliance, we must also - // create an unoptimized (unminified) variant. These are tracked - // in another map that is kept in sync. - $data = ''; - foreach ($js_group['items'] as $js_asset) { - $url = $this->urlGenerator->generateFromRoute('system.javascript_license_web_labels', array(), array('fragment' => drupal_clean_css_identifier($js_asset['data']))); - $data .= "/** JavaScript asset: " . $js_asset['data'] . '; for license information, see ' . $url . " **/\n\n"; - $data .= file_get_contents($js_asset['data']); - $data .= ";\n"; - } - $unoptimized_uri = $this->dumper->dump($data, 'js'); - $unoptimized_map[$key] = $unoptimized_uri; - $this->state->set('system.js_unoptimized_aggregate_files', $unoptimized_map); + $this->state->set($this->name, $map); } else { // Use the persisted URI for the optimized JS file. @@ -194,14 +178,14 @@ protected function generateHash(array $js_group) { * {@inheritdoc} */ public function getAll() { - return $this->state->get('system.js_cache_files'); + return $this->state->get($this->name); } /** * {@inheritdoc} */ public function deleteAll() { - $this->state->delete('system.js_cache_files'); + $this->state->delete($this->name); $delete_stale = function($uri) { // Default stale file threshold is 30 days. if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) { diff --git a/core/lib/Drupal/Core/Asset/JsLicenseWebLabelsAnnotator.php b/core/lib/Drupal/Core/Asset/JsLicenseWebLabelsAnnotator.php index e69de29..a487b27 100644 --- a/core/lib/Drupal/Core/Asset/JsLicenseWebLabelsAnnotator.php +++ b/core/lib/Drupal/Core/Asset/JsLicenseWebLabelsAnnotator.php @@ -0,0 +1,53 @@ +urlGenerator = $url_generator; + } + + /** + * {@inheritdoc} + */ + public function optimize(array $js_asset) { + if ($js_asset['type'] !== 'file') { + throw new \Exception('Only file JavaScript assets can be optimized.'); + } + if ($js_asset['type'] === 'file' && !$js_asset['preprocess']) { + throw new \Exception('Only file JavaScript assets with preprocessing enabled can be optimized.'); + } + + // Generate a prefix to be prepended to the "optimized" asset (no actual + // optimizations are made; this is a no-op optimizer) that deep-links to the + // license information on the JavaScript License Web Labels page. + $url = $this->urlGenerator->generateFromRoute('system.javascript_license_web_labels', array(), array('fragment' => drupal_clean_css_identifier($js_asset['data']))); + $prefix = "/** JavaScript asset: " . $js_asset['data'] . '; for license information, see ' . $url . " **/\n\n"; + return $prefix . file_get_contents($js_asset['data']); + } + +} diff --git a/core/modules/system/src/Controller/AssetLicenseInfoController.php b/core/modules/system/src/Controller/AssetLicenseInfoController.php index acac4c0..0abeba9 100644 --- a/core/modules/system/src/Controller/AssetLicenseInfoController.php +++ b/core/modules/system/src/Controller/AssetLicenseInfoController.php @@ -8,6 +8,7 @@ namespace Drupal\system\Controller; use Drupal\Component\Utility\String; +use Drupal\Core\Asset\AssetCollectionOptimizerInterface; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Asset\LibraryDiscoveryInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -25,11 +26,27 @@ class AssetLicenseInfoController extends ControllerBase { protected $libraryDiscovery; /** + * The JavaScript asset collection optimizer service. + * + * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface + */ + protected $jsCollectionOptimizer; + + /** + * The JavaScript asset collection optimizer license annotator service. + * + * @var \Drupal\Core\Asset\AssetCollectionOptimizerInterface + */ + protected $jsCollectionOptimizerLicenseAnnotator; + + /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( - $container->get('library.discovery') + $container->get('library.discovery'), + $container->get('asset.js.collection_optimizer'), + $container->get('asset.js.collection_optimizer_license_web_labels_annotator') ); } @@ -38,9 +55,15 @@ public static function create(ContainerInterface $container) { * * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery * The asset library discovery service. + * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $js_collection_optimizer + * The JavaScript asset collection optimizer service. + * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $js_collection_optimizer_license_annotator + * The JavaScript asset collection optimizer license annotator service. */ - public function __construct(LibraryDiscoveryInterface $library_discovery) { + public function __construct(LibraryDiscoveryInterface $library_discovery, AssetCollectionOptimizerInterface $js_collection_optimizer, AssetCollectionOptimizerInterface $js_collection_optimizer_license_annotator) { $this->libraryDiscovery = $library_discovery; + $this->jsCollectionOptimizer = $js_collection_optimizer; + $this->jsCollectionOptimizerLicenseAnnotator = $js_collection_optimizer_license_annotator; } /** @@ -114,10 +137,11 @@ public function jslicense() { 'colspan' => 3, ), ); - $aggregated_js_assets = \Drupal::state()->get('system.js_optimized_aggregate_files'); - if (isset($aggregated_js_assets)) { - $unoptimized_aggregated_js_assets = \Drupal::state()->get('system.js_unoptimized_aggregate_files'); - $license_column = l('GNU-GPL-2.0-or-later', 'https://drupal.org/licensing/faq'); + + $aggregated_js_assets = $this->jsCollectionOptimizer->getAll(); + if (count($aggregated_js_assets)) { + $unoptimized_aggregated_js_assets = $this->jsCollectionOptimizerLicenseAnnotator->getAll(); + $license_column = $this->t('Combination'); foreach (array_keys($aggregated_js_assets) as $hash) { $optimized_js = $aggregated_js_assets[$hash]; $unoptimized_js = $unoptimized_aggregated_js_assets[$hash]; diff --git a/core/modules/system/src/Tests/Common/JavaScriptTest.php b/core/modules/system/src/Tests/Common/JavaScriptTest.php index 794a84e..eff099e 100644 --- a/core/modules/system/src/Tests/Common/JavaScriptTest.php +++ b/core/modules/system/src/Tests/Common/JavaScriptTest.php @@ -459,11 +459,11 @@ function testAggregationOrder() { drupal_render($scripts_html); // Store the expected key for the first item in the cache. - $cache = array_keys(\Drupal::state()->get('system.js_optimized_aggregate_files') ?: array()); + $cache = array_keys(\Drupal::state()->get('system.js_cache_files') ?: array()); $expected_key = $cache[0]; // Reset variables and add a file in a different scope first. - \Drupal::state()->delete('system.js_optimized_aggregate_file'); + \Drupal::state()->delete('system.js_cache_files'); drupal_static_reset('_drupal_add_js'); $attached = array(); $attached['#attached']['library'][] = 'core/drupal'; @@ -484,7 +484,7 @@ function testAggregationOrder() { drupal_render($scripts_html); // Compare the expected key for the first file to the current one. - $cache = array_keys(\Drupal::state()->get('system.js_optimized_aggregate_files') ?: array()); + $cache = array_keys(\Drupal::state()->get('system.js_cache_files') ?: array()); $key = $cache[0]; $this->assertEqual($key, $expected_key, 'JavaScript aggregation is not affected by ordering in different scopes.'); }