Verified Commit f79c0a4f authored by godotislate's avatar godotislate
Browse files

task: #1945262 Introduce before and after for conditional ordering in library definitions

By: nod_
By: robloach
By: sun
By: webchick
By: catch
By: penyaskito
By: amol_tatkare
By: taran2l
By: smustgrave
By: luenemann
By: wim leers
By: liam morland
By: neclimdul
By: godotislate
parent 9124f0ad
Loading
Loading
Loading
Loading
Loading
+53 −58
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ internal.backbone:
    url: https://raw.githubusercontent.com/jashkenas/backbone/1.6.1/LICENSE
    gpl-compatible: true
  js:
    assets/vendor/backbone/backbone-min.js: { weight: -19, minified: true, preprocess: false }
    assets/vendor/backbone/backbone-min.js: { minified: true, preprocess: false }
  dependencies:
    - core/internal.underscore

@@ -195,8 +195,8 @@ drupal:
    js: 'drupal'
  version: VERSION
  js:
    misc/drupal.js: { weight: -18 }
    misc/drupal.init.js: { weight: -17 }
    misc/drupal.js: {}
    misc/drupal.init.js: {}
  dependencies:
    - core/drupalSettings
  drupalSettings:
@@ -207,9 +207,7 @@ drupalSettings:
    js: 'drupal'
  version: VERSION
  js:
    # Need to specify a negative weight like drupal.js until
    # https://www.drupal.org/node/1945262 is resolved.
    misc/drupalSettingsLoader.js: { weight: -18 }
    misc/drupalSettingsLoader.js: {}
  drupalSettings:
    # These placeholder values will be set by system_js_settings_alter().
    path:
@@ -279,44 +277,44 @@ loadjs:
internal.jquery_ui:
  version: VERSION
  js:
    # All weights are based on the requirements defined within each file.
    assets/vendor/jquery.ui/ui/labels-min.js: { weight: -11.7, minified: true }
    assets/vendor/jquery.ui/ui/data-min.js: { weight: -11.8, minified: true }
    assets/vendor/jquery.ui/ui/disable-selection-min.js: { weight: -11.8, minified: true }
    assets/vendor/jquery.ui/ui/jquery-patch-min.js: { weight: -11.8, minified: true }
    assets/vendor/jquery.ui/ui/scroll-parent-min.js: { weight: -11.8, minified: true }
    assets/vendor/jquery.ui/ui/unique-id-min.js: { weight: -11.8, minified: true }
    assets/vendor/jquery.ui/ui/focusable-min.js: { weight: -11.8, minified: true }
    assets/vendor/jquery.ui/ui/keycode-min.js: { weight: -11.8, minified: true }
    assets/vendor/jquery.ui/ui/plugin-min.js: { weight: -11.8, minified: true }
    assets/vendor/jquery.ui/ui/widget-min.js: { weight: -11.8, minified: true }
    assets/vendor/jquery.ui/ui/version-min.js: { weight: -11.9, minified: true }
    # These JavaScript assets previously came from core/jquery.ui, a deprecated
    # library.
    # @todo replace with solution in https://drupal.org/node/3076171
    assets/vendor/jquery.ui/ui/version-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/data-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/disable-selection-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/focusable-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/jquery-patch-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/keycode-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/plugin-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/scroll-parent-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/widget-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/unique-id-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/labels-min.js: { minified: true }
  css:
    component:
      assets/vendor/jquery.ui/themes/base/core.css: { weight: -11.8 }
      assets/vendor/jquery.ui/themes/base/core.css: {}
    theme:
      assets/vendor/jquery.ui/themes/base/theme.css: { weight: -11.8 }
      assets/vendor/jquery.ui/themes/base/theme.css: {}
  dependencies:
    - core/jquery

drupal.autocomplete:
  version: VERSION
  js:
    misc/autocomplete.js: { weight: -1 }
    # The remaining JavaScript assets previously came from core/jquery.ui, a
    # deprecated library.
    # All weights are based on the requirements defined within each file.
    # @todo replace with solution in https://drupal.org/node/3076171
    assets/vendor/jquery.ui/ui/widgets/autocomplete-min.js: { weight: -11.7, minified: true }
    assets/vendor/jquery.ui/ui/widgets/menu-min.js: { weight: -11.7, minified: true }
    assets/vendor/jquery.ui/ui/widgets/menu-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/widgets/autocomplete-min.js: { minified: true }
    misc/autocomplete.js: {}
  # All CSS assets previously came from core/jquery.ui, a deprecated library.
  # @todo replace with solution found in https://drupal.org/node/3076171
  css:
    component:
      assets/vendor/jquery.ui/themes/base/autocomplete.css: { weight: -11.7 }
      assets/vendor/jquery.ui/themes/base/menu.css: { weight: -11.7 }
      misc/components/autocomplete-loading.module.css: { weight: -10 }

      assets/vendor/jquery.ui/themes/base/menu.css: {}
      assets/vendor/jquery.ui/themes/base/autocomplete.css: {}
      misc/components/autocomplete-loading.module.css: {}
  dependencies:
    - core/jquery
    - core/internal.jquery_ui
@@ -368,31 +366,29 @@ drupal.debounce:
drupal.dialog:
  version: VERSION
  js:
    # The remaining JavaScript assets previously came from core/jquery.ui, a
    # deprecated library.
    assets/vendor/tua-body-scroll-lock/tua-bsl.umd.min.js: { minified: true }
    assets/vendor/jquery.ui/ui/widgets/controlgroup-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/form-reset-mixin-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/widgets/mouse-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/widgets/draggable-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/widgets/resizable-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/widgets/checkboxradio-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/widgets/button-min.js: { minified: true }
    assets/vendor/jquery.ui/ui/widgets/dialog-min.js: { minified: true }
    misc/dialog/dialog.js: {}
    misc/dialog/dialog.position.js: {}
    misc/dialog/dialog.jquery-ui.js: {}
    # The remaining JavaScript assets previously came from core/jquery.ui, a
    # deprecated library.
    # All weights are based on the requirements defined within each file.
    # @todo replace with solution in https://drupal.org/node/2158943
    assets/vendor/jquery.ui/ui/widgets/dialog-min.js: { weight: -11.4, minified: true }
    assets/vendor/jquery.ui/ui/widgets/button-min.js: { weight: -11.5, minified: true }
    assets/vendor/jquery.ui/ui/widgets/checkboxradio-min.js: { weight: -11.6, minified: true }
    assets/vendor/jquery.ui/ui/widgets/draggable-min.js: { weight: -11.6, minified: true }
    assets/vendor/jquery.ui/ui/widgets/resizable-min.js: { weight: -11.6, minified: true }
    assets/vendor/jquery.ui/ui/widgets/controlgroup-min.js: { weight: -11.7, minified: true }
    assets/vendor/jquery.ui/ui/form-reset-mixin-min.js: { weight: -11.7, minified: true }
    assets/vendor/jquery.ui/ui/widgets/mouse-min.js: { weight: -11.7, minified: true }
    assets/vendor/tua-body-scroll-lock/tua-bsl.umd.min.js: { weight: -1,  minified: true }
  # All CSS assets previously came from core/jquery.ui, a deprecated library.
  # @todo replace with solution found in https://drupal.org/node/2158943
  css:
    component:
      assets/vendor/jquery.ui/themes/base/dialog.css: { weight: -11.4 }
      assets/vendor/jquery.ui/themes/base/button.css: { weight: -11.5 }
      assets/vendor/jquery.ui/themes/base/checkboxradio.css: { weight: -11.6 }
      assets/vendor/jquery.ui/themes/base/resizable.css: { weight: -11.6 }
      assets/vendor/jquery.ui/themes/base/controlgroup.css: { weight: -11.7 }
      assets/vendor/jquery.ui/themes/base/dialog.css: {}
      assets/vendor/jquery.ui/themes/base/button.css: {}
      assets/vendor/jquery.ui/themes/base/controlgroup.css: {}
      assets/vendor/jquery.ui/themes/base/checkboxradio.css: {}
      assets/vendor/jquery.ui/themes/base/resizable.css: {}
  dependencies:
    - core/jquery
    - core/internal.jquery_ui
@@ -555,12 +551,11 @@ drupal.tabbingmanager:
drupal.tabledrag:
  version: VERSION
  js:
    misc/tabledrag.js: { weight: -1 }
    misc/tabledrag.js: {}
  css:
    component:
      misc/components/tabledrag.module.css: {}
      misc/components/tree-child.module.css: {}

  dependencies:
    - core/jquery
    - core/drupal
@@ -628,7 +623,7 @@ drupal.touchevents-test:
  header: true
  version: VERSION
  js:
    misc/touchevents-test.js: { weight: -21 }
    misc/touchevents-test.js: {}

drupal.vertical-tabs:
  version: VERSION
@@ -643,9 +638,10 @@ drupal.vertical-tabs:
    - core/drupal
    - core/drupalSettings
    - core/drupal.form
  before:
    - core/drupal.collapse

htmx:
  aggregate_target:
    js: 'htmx'
  remote: https://github.com/bigskysoftware/htmx
  version: "2.0.4"
  license:
@@ -653,7 +649,7 @@ htmx:
    url:  https://raw.githubusercontent.com/bigskysoftware/htmx/refs/tags/v2.0.4/LICENSE
    gpl-compatible: true
  js:
    assets/vendor/htmx/htmx.min.js: { minified: true }
    assets/vendor/htmx/htmx.min.js: { minified: true, preprocess: false }

internal.floating-ui:
  aggregate_target:
@@ -681,7 +677,7 @@ jquery:
    # across different aggregates which can include different combinations of
    # libraries that depend on jQuery, so that it is only downloaded once per
    # browser instead of potentially multiple times from different pages.
    assets/vendor/jquery/jquery.min.js: { preprocess: false, minified: true, weight: -20 }
    assets/vendor/jquery/jquery.min.js: { preprocess: false, minified: true }

internal.jquery.form:
  # Internal library. Do not depend on it outside core nor add new core usage.
@@ -714,7 +710,7 @@ normalize:
    gpl-compatible: true
  css:
    base:
      assets/vendor/normalize-css/normalize.css: { weight: -20 }
      assets/vendor/normalize-css/normalize.css: {}

once:
  remote: https://git.drupalcode.org/project/once
@@ -724,7 +720,7 @@ once:
    url: https://git.drupalcode.org/project/once/-/raw/v1.0.1/LICENSE.md
    gpl-compatible: true
  js:
    assets/vendor/once/once.min.js: { weight: -19, minified: true, preprocess: false }
    assets/vendor/once/once.min.js: { minified: true, preprocess: false }

sortable:
  remote: https://github.com/SortableJS/Sortable
@@ -744,7 +740,7 @@ tabbable:
    url: https://raw.githubusercontent.com/focus-trap/tabbable/v6.3.0/LICENSE
    gpl-compatible: true
  js:
    assets/vendor/tabbable/index.umd.min.js: { weight: -1, minified: true, preprocess: false }
    assets/vendor/tabbable/index.umd.min.js: { minified: true, preprocess: false }

transliteration:
  remote: https://github.com/dzcpy/transliteration
@@ -770,8 +766,7 @@ internal.underscore:
    url: https://raw.githubusercontent.com/jashkenas/underscore/1.13.8/LICENSE
    gpl-compatible: true
  js:
    assets/vendor/underscore/underscore-min.js: { weight: -20, minified: true, preprocess: false }

    assets/vendor/underscore/underscore-min.js: { minified: true, preprocess: false }

drupal.dialog.off_canvas:
  version: VERSION
+22 −11
Original line number Diff line number Diff line
@@ -257,6 +257,17 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, ?Langua
      [$extension, $name] = explode('/', $library, 2);
      $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
      foreach ($definition['css'] as $options) {
        // Libraries are loaded based on dependencies, then their attaching
        // order. Individual asset files are ordered how they are defined within
        // the library itself.
        // Re-adding an asset file will rewrite the resulting array leading to
        // an incorrect order of asset files, i.e. the last occurrence of the
        // particular asset will win, but the desired behavior is the opposite -
        // the first occurrence must win.
        // So, let's skip processing of already added asset files.
        if (array_key_exists($options['data'], $css)) {
          continue;
        }
        $options += $default_options;
        // Copy the asset library license information to each file.
        $options['license'] = $definition['license'];
@@ -265,11 +276,6 @@ public function getCssAssets(AttachedAssetsInterface $assets, $optimize, ?Langua
        if ($options['type'] === 'file' && $options['preprocess'] && str_contains($options['data'], '?')) {
          $options['preprocess'] = FALSE;
        }

        // Always add a tiny value to the weight, to conserve the insertion
        // order.
        $options['weight'] += count($css) / 30000;

        $options['library'] = $library;
        $options['aggregate_target'] = $definition['aggregate_target'] ?? ['js' => FALSE, 'css' => FALSE];

@@ -387,6 +393,17 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize, ?Languag
        [$extension, $name] = explode('/', $library, 2);
        $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
        foreach ($definition['js'] as $options) {
          // Libraries are loaded based on dependencies, then their attaching
          // order. Individual asset files are ordered how they are defined
          // within the library itself.
          // Re-adding an asset file will rewrite the resulting array leading to
          // an incorrect order of asset files, i.e. the last occurrence of the
          // particular asset will win, but the desired behavior is the opposite
          // - the first occurrence must win.
          // So, let's skip processing of already added asset files.
          if (array_key_exists($options['data'], $javascript)) {
            continue;
          }
          $options += $default_options;
          // Copy the asset library license information to each file.
          $options['license'] = $definition['license'];
@@ -399,13 +416,8 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize, ?Languag
          // attributes are set.
          $options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;

          // Always add a tiny value to the weight, to conserve the insertion
          // order.
          $options['weight'] += count($javascript) / 30000;

          $options['library'] = $library;
          $options['aggregate_target'] = $definition['aggregate_target'] ?? ['js' => FALSE, 'css' => FALSE];

          // Local and external files must keep their name as the associative
          // key so the same JavaScript file is not added twice.
          $javascript[$options['data']] = $options;
@@ -475,7 +487,6 @@ public function getJsAssets(AttachedAssetsInterface $assets, $optimize, ?Languag
      $settings_as_inline_javascript = [
        'type' => 'setting',
        'group' => JS_SETTING,
        'weight' => 0,
        'data' => $settings,
      ];
      $settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
+71 −33
Original line number Diff line number Diff line
@@ -2,6 +2,8 @@

namespace Drupal\Core\Asset;

use Drupal\Component\Graph\Graph;

/**
 * Resolves the dependencies of asset (CSS/JavaScript) libraries.
 */
@@ -14,13 +16,6 @@ class LibraryDependencyResolver implements LibraryDependencyResolverInterface {
   */
  protected $libraryDiscovery;

  /**
   * The libraries dependencies.
   *
   * @var array
   */
  protected $librariesDependencies = [];

  /**
   * Constructs a new LibraryDependencyResolver instance.
   *
@@ -35,14 +30,20 @@ public function __construct(LibraryDiscoveryInterface $library_discovery) {
   * {@inheritdoc}
   */
  public function getLibrariesWithDependencies(array $libraries) {
    $return = [];
    foreach ($libraries as $library) {
      if (!isset($this->librariesDependencies[$library])) {
        $this->librariesDependencies[$library] = $this->doGetDependencies([$library]);
      }
      $return += $this->librariesDependencies[$library];
    $libraries_graph = $this->doGetDependencies($libraries);
    $libraries_graph = $this->doProcessBeforeAfter($libraries_graph);

    $graph_object = new Graph($libraries_graph);
    $graph = $graph_object->searchAndSort();

    uasort($graph, function ($a, $b) {
      if ($a['weight'] == $b['weight']) {
        return 0;
      }
    return array_values($return);
      return ($a['weight'] < $b['weight']) ? 1 : -1;
    });

    return array_keys($graph);
  }

  /**
@@ -50,29 +51,67 @@ public function getLibrariesWithDependencies(array $libraries) {
   *
   * Helper method for ::getLibrariesWithDependencies().
   *
   * @param string[] $libraries_with_unresolved_dependencies
   *   A list of libraries, with unresolved dependencies, in the order they
   *   should be loaded.
   * @param string[] $final_libraries
   *   The final list of libraries (the return value) that is being built
   *   recursively.
   * @param string[] $libraries
   *   A list of libraries in the order they should be loaded.
   * @param array $graph
   *   The graph of libraries that is being built recursively.
   *
   * @return string[]
   *   A list of libraries, in the order they should be loaded, including their
   *   dependencies.
   */
  protected function doGetDependencies(array $libraries_with_unresolved_dependencies, array $final_libraries = []) {
    foreach ($libraries_with_unresolved_dependencies as $library) {
      if (!isset($final_libraries[$library])) {
  protected function doGetDependencies(array $libraries, array $graph = []) {
    foreach ($libraries as $library) {
      if (!isset($graph[$library])) {
        [$extension, $name] = explode('/', $library, 2);
        $definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
        if ($definition) {
          $graph[$library]['edges'] = [];
        }

        if (!empty($definition['dependencies'])) {
          $final_libraries = $this->doGetDependencies($definition['dependencies'], $final_libraries);
          foreach ($definition['dependencies'] as $dependency) {
            $graph[$library]['edges'][$dependency] = $dependency;
          }

          $graph = $this->doGetDependencies($definition['dependencies'], $graph);
        }
      }
        $final_libraries[$library] = $library;
    }
    return $graph;
  }

  /**
   * Processes before and after settings for the libraries graph.
   *
   * @param array $graph
   *   The libraries graph array.
   *
   * @return array
   *   The libraries graph with before and after processed.
   */
  protected function doProcessBeforeAfter($graph): array {
    foreach ($graph as $library => $data) {
      [$extension, $name] = explode('/', $library, 2);
      $definition = $this->libraryDiscovery->getLibraryByName($extension, $name) + [
        'after' => [],
        'before' => [],
      ];

      foreach ($definition['after'] as $after) {
        if (isset($graph[$after])) {
          $graph[$library]['edges'][$after] = $after;
        }
    return $final_libraries;
      }

      foreach ($definition['before'] as $before) {
        if (isset($graph[$before])) {
          $graph[$before]['edges'][$library] = $library;
        }
      }
    }

    return $graph;
  }

  /**
@@ -81,15 +120,14 @@ protected function doGetDependencies(array $libraries_with_unresolved_dependenci
  public function getMinimalRepresentativeSubset(array $libraries) {
    assert(count($libraries) === count(array_unique($libraries)), '$libraries can\'t contain duplicate items.');

    // Determine each library's dependencies.
    $all_dependencies = [];
    foreach ($libraries as $library) {
      $with_deps = $this->getLibrariesWithDependencies([$library]);
      // We don't need library itself listed in the dependencies.
      $all_dependencies = array_unique(array_merge($all_dependencies, array_diff($with_deps, [$library])));
    $graph = $this->doGetDependencies($libraries);

    $libraries_to_exclude = [];
    foreach ($graph as $vertex) {
      $libraries_to_exclude += $vertex['edges'];
    }

    return array_values(array_diff($libraries, array_intersect($all_dependencies, $libraries)));
    return array_values(array_diff($libraries, $libraries_to_exclude));
  }

}
+6 −33
Original line number Diff line number Diff line
@@ -352,41 +352,14 @@ public function jsAlter(&$javascript, AttachedAssetsInterface $assets, LanguageI
    $translations_library = 'core/ckeditor5.translations';
    if (in_array($translations_library, $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()), TRUE)
      || in_array($translations_library, $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()), TRUE)) {

      // This file is used to get a weight that will make it possible to
      // aggregate all translation files in a single aggregate.
      $ckeditor_umd_file = 'core/assets/vendor/ckeditor5/ckeditor5.umd.js';
      // Use the placeholder file weight to set all the translations files
      // weights so they can be aggregated together as expected. Account for
      // requests where the library is not loaded such as when during an AJAX
      // request when it was already loaded via the main request. In these cases
      // it is unlikely that multiple JavaScript aggregates will be created
      // anyway since AJAX requests generally result in very few libraries being
      // loaded.
      $default_weight = $javascript[$placeholder_file]['weight'] ?? 0;
      if (isset($javascript[$ckeditor_umd_file])) {
        $default_weight = $javascript[$ckeditor_umd_file]['weight'];
      }

      $ckeditor5_language = $this->languageMapper->getMapping($language->getId());
      // Remove all CKEditor 5 translations files that are not in the current
      // language.
      foreach ($javascript as $index => &$item) {
        // This is not a CKEditor 5 translation file, skip it.
        if (empty($item['ckeditor5_langcode'])) {
          continue;
        }
        // This file is the correct translation for this page.
        if ($item['ckeditor5_langcode'] === $ckeditor5_language) {
          // Set the weight for the translation file to be able to have the
          // translation files aggregated.
          $item['weight'] = $default_weight;
        }
        else {
          // Remove files that don't match the language requested.
          unset($javascript[$index]);
        }
      }
      $ckeditor5_language = $this->languageMapper->getMapping($language->getId());
      $javascript = array_filter($javascript, function ($item) use ($ckeditor5_language) {
        // Skip files that are not CKEditor5 translation files. Remove files
        // that don't match the language requested.
        return empty($item['ckeditor5_langcode']) || $item['ckeditor5_langcode'] === $ckeditor5_language;
      });
    }
    // The placeholder file is not a real file, remove it from the list.
    unset($javascript[$placeholder_file]);
+1 −9
Original line number Diff line number Diff line
@@ -125,7 +125,6 @@ public static function providerPathsToFormNames(): array {
   * @legacy-covers \Drupal\ckeditor5\Hook\Ckeditor5Hooks::jsAlter
   */
  public function testJsAlterHook(): void {
    $placeholder_file = 'core/assets/vendor/ckeditor5/translation.js';
    $language_mapper = $this->createMock(LanguageMapper::class);
    $language_mapper->expects($this->atLeastOnce())
      ->method('getMapping')
@@ -167,7 +166,6 @@ public function testJsAlterHook(): void {
    $expected_javascript = [
      'keep_this' => [
        'ckeditor5_langcode' => 'en',
        'weight' => 5,
      ],
      'keep_this_too' => [],
    ];
@@ -179,11 +177,7 @@ public function testJsAlterHook(): void {
    \Drupal::setContainer($container);

    // First check that it filters when the placeholder script is present.
    $javascript = $original_javascript + [
      $placeholder_file => [
        'weight' => 5,
      ],
    ];
    $javascript = $original_javascript;
    $hooks->jsAlter($javascript, $assets, $language);
    $this->assertEquals($expected_javascript, $javascript);

@@ -199,8 +193,6 @@ public function testJsAlterHook(): void {
    ]);
    $javascript = $original_javascript;
    $hooks->jsAlter($javascript, $assets, $language);
    // There was no placeholder to get the weight from.
    $expected_javascript['keep_this']['weight'] = 0;
    $this->assertEquals($expected_javascript, $javascript);
  }

Loading