diff --git a/core/core.services.yml b/core/core.services.yml index 6eea938..4693c46 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1499,7 +1499,7 @@ services: class: Drupal\Core\Asset\CssCollectionRenderer arguments: [ '@state' ] asset.css.collection_optimizer: - class: Drupal\Core\Asset\CssCollectionOptimizer + class: Drupal\Core\Asset\CssCollectionOptimizerLazy arguments: [ '@asset.css.collection_grouper', '@asset.css.optimizer', '@theme.manager' ] asset.css.optimizer: class: Drupal\Core\Asset\CssOptimizer @@ -1511,7 +1511,7 @@ services: class: Drupal\Core\Asset\JsCollectionRenderer arguments: [ '@state' ] asset.js.collection_optimizer: - class: Drupal\Core\Asset\JsCollectionOptimizer + class: Drupal\Core\Asset\JsCollectionOptimizerLazy arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@theme.manager' ] asset.js.optimizer: class: Drupal\Core\Asset\JsOptimizer diff --git a/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerInterface.php b/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerInterface.php index 526acf5..249db77 100644 --- a/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerInterface.php +++ b/core/lib/Drupal/Core/Asset/AssetCollectionOptimizerInterface.php @@ -23,6 +23,8 @@ public function optimize(array $assets); * * @return string[] * URIs for all optimized asset collection assets. + * + * @deprecated as off Drupal 8.3.x, will be removed before 9.0.0 */ public function getAll(); diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php index 71d902c..a27b009 100644 --- a/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php @@ -2,15 +2,13 @@ namespace Drupal\Core\Asset; -use \Drupal\Component\Utility\UrlHelper; -use \Drupal\Core\Asset\AssetOptimizerInterface; -use \Drupal\Core\Theme\ThemeManagerInterface; +use Drupal\Core\State\StateInterface; /** * Optimizes CSS assets. */ -class CssCollectionOptimizer implements AssetCollectionGroupOptimizerInterface { - use AssetGroupSetHashTrait; +class CssCollectionOptimizer implements AssetCollectionOptimizerInterface { + use CssCollectionOptimizerTrait; /** * A CSS asset grouper. @@ -20,18 +18,25 @@ class CssCollectionOptimizer implements AssetCollectionGroupOptimizerInterface { protected $grouper; /** - * The CSS asset optimizer. + * A CSS asset optimizer. * * @var \Drupal\Core\Asset\CssOptimizer */ protected $optimizer; /** - * The theme manager. + * An asset dumper. * - * @var \Drupal\Core\Theme\ThemeManagerInterface + * @var \Drupal\Core\Asset\AssetDumper */ - protected $themeManager; + protected $dumper; + + /** + * The state key/value store. + * + * @var \Drupal\Core\State\StateInterface + */ + protected $state; /** * Constructs a CssCollectionOptimizer. @@ -40,13 +45,16 @@ class CssCollectionOptimizer implements AssetCollectionGroupOptimizerInterface { * The grouper for CSS assets. * @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer * The optimizer for a single CSS asset. - * @param \Drupal\Core\Theme\ThemeManagerInterface - * The Theme manager. + * @param \Drupal\Core\Asset\AssetDumperInterface $dumper + * The dumper for optimized CSS assets. + * @param \Drupal\Core\State\StateInterface $state + * The state key/value store. */ - public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, ThemeManagerInterface $theme_manager) { + public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) { $this->grouper = $grouper; $this->optimizer = $optimizer; - $this->themeManager = $theme_manager; + $this->dumper = $dumper; + $this->state = $state; } /** @@ -66,10 +74,15 @@ public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptim public function optimize(array $css_assets) { // Group the assets. $css_groups = $this->grouper->group($css_assets); - $key = $this->generateHash($css_groups); + // Now optimize (concatenate + minify) and dump each asset group, unless + // that was already done, in which case it should appear in + // drupal_css_cache_files. + // Drupal contrib can override this default CSS 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('drupal_css_cache_files') ?: array(); $css_assets = array(); - $libraries = []; foreach ($css_groups as $order => $css_group) { // We have to return a single asset, not a group of assets. It is now up // to one of the pieces of code in the switch statement below to set the @@ -84,13 +97,26 @@ public function optimize(array $css_assets) { $uri = $css_group['items'][0]['data']; $css_assets[$order]['data'] = $uri; } + // Preprocess (aggregate), unless the aggregate file already exists. 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. - foreach ($css_group['items'] as $css_asset) { - $libraries[$css_asset['library']] = $css_asset['library']; + $key = $this->generateHash($css_group); + $uri = ''; + if (isset($map[$key])) { + $uri = $map[$key]; + } + if (empty($uri) || !file_exists($uri)) { + $data = $this->optimizeGroup($css_group); + // 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; } @@ -104,47 +130,27 @@ public function optimize(array $css_assets) { break; } } - // Generate a URL for the group, but do not process it inline, this is - // done by \Drupal\system\controller\CssAssetController - $minimal_set = \Drupal::service('library.dependency_resolver')->getMinimalRepresentativeSubset($libraries); - $theme_name = $this->themeManager->getActiveTheme()->getName(); - $ajax_page_state = \Drupal::request()->get('ajax_page_state'); - $already_loaded = isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []; - $minimal_already_loaded_set = \Drupal::service('library.dependency_resolver')->getMinimalRepresentativeSubset($already_loaded); - $query = UrlHelper::buildQuery(['libraries' => $minimal_set, 'theme' => $theme_name, 'already_loaded' => $minimal_already_loaded_set]); - $path = 'public://' . 'css'; - foreach ($css_assets as $order => $css_asset) { - if (!empty($css_asset['preprocessed'])) { - $filename = 'css' . '_' . $order . '_' . $key . '.' . 'css'; - // Create the css/ or js/ path within the files folder. - $uri = $path . '/' . $filename; - $css_assets[$order]['data'] = file_create_url($uri) . '?' . $query; - } - } return $css_assets; } /** - * [@inheritdoc} + * Generate a hash for a given group of CSS assets. + * + * @param array $css_group + * A group of CSS assets. + * + * @return string + * A hash to uniquely identify the given group of CSS assets. */ - public function optimizeGroup(array $group) { - // Optimize each asset within the group. - $data = ''; - foreach ($group['items'] as $asset) { - $data .= $this->optimizer->optimize($asset); + protected function generateHash(array $css_group) { + $css_data = array(); + foreach ($css_group['items'] as $css_file) { + $css_data[] = $css_file['data']; } - // 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. - $regexp = '/@import[^;]+;/i'; - preg_match_all($regexp, $data, $matches); - $data = preg_replace($regexp, '', $data); - return implode('', $matches[0]) . $data; + return hash('sha256', serialize($css_data)); } - /** * {@inheritdoc} */ @@ -152,17 +158,4 @@ public function getAll() { return $this->state->get('drupal_css_cache_files'); } - /** - * {@inheritdoc} - */ - public function deleteAll() { - $delete_stale = function($uri) { - // Default stale file threshold is 30 days. - if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) { - file_unmanaged_delete($uri); - } - }; - file_scan_directory('public://css', '/.*/', array('callback' => $delete_stale)); - } - } diff --git a/core/lib/Drupal/Core/Asset/CssCollectionOptimizerTrait.php b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerTrait.php new file mode 100644 index 0000000..d4788b4 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/CssCollectionOptimizerTrait.php @@ -0,0 +1,55 @@ +optimizer->optimize($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. + $regexp = '/@import[^;]+;/i'; + preg_match_all($regexp, $data, $matches); + $data = preg_replace($regexp, '', $data); + return implode('', $matches[0]) . $data; + } + + /** + * Deletes all optimized asset collections assets. + */ + public function deleteAll() { + $delete_stale = function($uri) { + // Default stale file threshold is 30 days. + if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) { + file_unmanaged_delete($uri); + } + }; + file_scan_directory('public://css', '/.*/', array('callback' => $delete_stale)); + } + +} diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php index c5efe10..4d996a3 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php @@ -2,69 +2,88 @@ namespace Drupal\Core\Asset; -use \Drupal\Component\Utility\UrlHelper; -use \Drupal\Core\Theme\ThemeManagerInterface; +use Drupal\Core\State\StateInterface; + /** * Optimizes JavaScript assets. */ -class JsCollectionOptimizer implements AssetCollectionGroupOptimizerInterface { - use AssetGroupSetHashTrait; +class JsCollectionOptimizer implements AssetCollectionOptimizerInterface { + use JsCollectionOptimizerTrait; /** * A JS asset grouper. * - * @var \Drupal\Core\Asset\JsCollectionGrouperInterface + * @var \Drupal\Core\Asset\JsCollectionGrouper */ protected $grouper; /** * A JS asset optimizer. * - * @var \Drupal\Core\Asset\AssetOptimizerInterface + * @var \Drupal\Core\Asset\JsOptimizer */ protected $optimizer; /** - * The theme manager. + * An asset dumper. + * + * @var \Drupal\Core\Asset\AssetDumper + */ + protected $dumper; + + /** + * The state key/value store. * - * @var \Drupal\Core\Theme\ThemeManagerInterface + * @var \Drupal\Core\State\StateInterface */ - protected $themeManager; + protected $state; /** * Constructs a JsCollectionOptimizer. * * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface $grouper * The grouper for JS assets. - * @param \Drupal\Core\Asset\AssetOptimizerInterface - * The asset optimizer. - * @param \Drupal\Core\Theme\ThemeManagerInterface; - * The theme manger. + * @param \Drupal\Core\Asset\AssetOptimizerInterface $optimizer + * The optimizer for a single JS asset. + * @param \Drupal\Core\Asset\AssetDumperInterface $dumper + * The dumper for optimized JS assets. + * @param \Drupal\Core\State\StateInterface $state + * The state key/value store. */ - public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, ThemeManagerInterface $theme_manager) { + public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) { $this->grouper = $grouper; $this->optimizer = $optimizer; - $this->themeManager = $theme_manager; + $this->dumper = $dumper; + $this->state = $state; } /** * {@inheritdoc} * - * File names are generated based on library/asset definitions. This includes - * a hash of the assets and the group index. Additionally the full set of - * libraries, already loaded libraries and theme are sent as query parameters - * to allow a PHP controller to generate a valid file with sufficient - * information. Files are not generated by this method since they're assumed - * to be successfully returned from the URL created whether on disk or not. + * The cache file name is retrieved on a page load via a lookup variable that + * contains an associative array. The array key is the hash of the names in + * $files while the value is the cache file name. The cache file is generated + * in two cases. First, if there is no file name value for the key, which will + * happen if a new file name has been added to $files or after the lookup + * variable is emptied to force a rebuild of the cache. Second, the cache file + * is generated if it is missing on disk. Old cache files are not deleted + * immediately when the lookup variable is emptied, but are deleted after a + * configurable period (@code system.performance.stale_file_threshold @endcode) + * to ensure that files referenced by a cached page will still be available. */ public function optimize(array $js_assets) { // Group the assets. $js_groups = $this->grouper->group($js_assets); - $key = $this->generateHash($js_groups); + // 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_cache_files') ?: array(); $js_assets = array(); - $libraries = []; foreach ($js_groups as $order => $js_group) { // We have to return a single asset, not a group of assets. It is now up // to one of the pieces of code in the switch statement below to set the @@ -79,13 +98,26 @@ public function optimize(array $js_assets) { $uri = $js_group['items'][0]['data']; $js_assets[$order]['data'] = $uri; } + // Preprocess (aggregate), unless the aggregate file already exists. 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. - foreach ($js_group['items'] as $js_asset) { - $libraries[$js_asset['library']] = $js_asset['library']; + $key = $this->generateHash($js_group); + $uri = ''; + if (isset($map[$key])) { + $uri = $map[$key]; + } + if (empty($uri) || !file_exists($uri)) { + $data = $this->optimizeGroup($js_group); + // 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; } @@ -97,29 +129,6 @@ public function optimize(array $js_assets) { $uri = $js_group['items'][0]['data']; $js_assets[$order]['data'] = $uri; break; - case 'setting': - $js_assets[$order]['data'] = $js_group['data']; - break; - } - } - if ($libraries) { - // Generate a URL for the group, but do not process it inline, this is - // done by \Drupal\system\controller\JsAssetController - $minimal_set = \Drupal::service('library.dependency_resolver')->getMinimalRepresentativeSubset($libraries); - $theme_name = $this->themeManager->getActiveTheme()->getName(); - $ajax_page_state = \Drupal::request()->get('ajax_page_state'); - $already_loaded = isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []; - $minimal_already_loaded_set = \Drupal::service('library.dependency_resolver')->getMinimalRepresentativeSubset($already_loaded); - $header_query = UrlHelper::buildQuery(['libraries' => $minimal_set, 'theme' => $theme_name, 'already_loaded' => $minimal_already_loaded_set, 'scope' => 'header']); - $footer_query = UrlHelper::buildQuery(['libraries' => $minimal_set, 'theme' => $theme_name, 'already_loaded' => $minimal_already_loaded_set, 'scope' => 'footer']); - $path = 'public://' . 'js'; - foreach ($js_assets as $order => $js_asset) { - if (!empty($js_asset['preprocessed'])) { - $query = $js_asset['scope'] == 'header' ? $header_query : $footer_query; - $filename = 'js' . '_' . $order . '_' . $key . '.' . 'js'; - $uri = $path . '/' . $filename; - $js_assets[$order]['data'] = file_create_url($uri) . '?' . $query; - } } } @@ -127,46 +136,27 @@ public function optimize(array $js_assets) { } /** - * {@inheritdoc} + * Generate a hash for a given group of JavaScript assets. + * + * @param array $js_group + * A group of JavaScript assets. + * + * @return string + * A hash to uniquely identify the given group of JavaScript assets. */ - public function optimizegroup(array $group) { - $data = ''; - foreach ($group['items'] as $js_asset) { - // 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"; + protected function generateHash(array $js_group) { + $js_data = array(); + foreach ($js_group['items'] as $js_file) { + $js_data[] = $js_file['data']; } - // Remove unwanted JS code that cause issues. - return $this->optimizer->clean($data); + return hash('sha256', serialize($js_data)); } - /** * {@inheritdoc} */ public function getAll() { - // @todo: whut? - return []; - } - - /** - * {@inheritdoc} - */ - public function deleteAll() { - $delete_stale = function($uri) { - // Default stale file threshold is 30 days. - if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) { - file_unmanaged_delete($uri); - } - }; - file_scan_directory('public://js', '/.*/', array('callback' => $delete_stale)); + return $this->state->get('system.js_cache_files'); } } diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php similarity index 78% copy from core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php copy to core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php index c5efe10..cff1978 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerLazy.php @@ -8,8 +8,9 @@ /** * Optimizes JavaScript assets. */ -class JsCollectionOptimizer implements AssetCollectionGroupOptimizerInterface { +class JsCollectionOptimizerLazy implements AssetCollectionGroupOptimizerInterface { use AssetGroupSetHashTrait; + use JsCollectionOptimizerTrait; /** * A JS asset grouper. @@ -19,13 +20,6 @@ class JsCollectionOptimizer implements AssetCollectionGroupOptimizerInterface { protected $grouper; /** - * A JS asset optimizer. - * - * @var \Drupal\Core\Asset\AssetOptimizerInterface - */ - protected $optimizer; - - /** * The theme manager. * * @var \Drupal\Core\Theme\ThemeManagerInterface @@ -129,44 +123,8 @@ public function optimize(array $js_assets) { /** * {@inheritdoc} */ - public function optimizegroup(array $group) { - $data = ''; - foreach ($group['items'] as $js_asset) { - // 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. - return $this->optimizer->clean($data); - } - - - /** - * {@inheritdoc} - */ public function getAll() { - // @todo: whut? return []; } - /** - * {@inheritdoc} - */ - public function deleteAll() { - $delete_stale = function($uri) { - // Default stale file threshold is 30 days. - if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) { - file_unmanaged_delete($uri); - } - }; - file_scan_directory('public://js', '/.*/', array('callback' => $delete_stale)); - } - } diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizerTrait.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerTrait.php new file mode 100644 index 0000000..cdd66ec --- /dev/null +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizerTrait.php @@ -0,0 +1,57 @@ +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. + return $this->optimizer->clean($data); + } + + /** + * Deletes all optimized asset collections assets. + */ + public function deleteAll() { + $delete_stale = function($uri) { + // Default stale file threshold is 30 days. + if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) { + file_unmanaged_delete($uri); + } + }; + file_scan_directory('public://js', '/.*/', array('callback' => $delete_stale)); + } + +}