[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-script-modules.php (source)

   1  <?php
   2  /**
   3   * Script Modules API: WP_Script_Modules class.
   4   *
   5   * Native support for ES Modules and Import Maps.
   6   *
   7   * @package WordPress
   8   * @subpackage Script Modules
   9   */
  10  
  11  /**
  12   * Core class used to register script modules.
  13   *
  14   * @since 6.5.0
  15   */
  16  class WP_Script_Modules {
  17      /**
  18       * Holds the registered script modules, keyed by script module identifier.
  19       *
  20       * @since 6.5.0
  21       * @var array<string, array<string, mixed>>
  22       */
  23      private $registered = array();
  24  
  25      /**
  26       * An array of IDs for queued script modules.
  27       *
  28       * @since 6.9.0
  29       * @var string[]
  30       */
  31      private $queue = array();
  32  
  33      /**
  34       * Holds the script module identifiers that have been printed.
  35       *
  36       * @since 6.9.0
  37       * @var string[]
  38       */
  39      private $done = array();
  40  
  41      /**
  42       * Tracks whether the @wordpress/a11y script module is available.
  43       *
  44       * Some additional HTML is required on the page for the module to work. Track
  45       * whether it's available to print at the appropriate time.
  46       *
  47       * @since 6.7.0
  48       * @var bool
  49       */
  50      private $a11y_available = false;
  51  
  52      /**
  53       * Holds a mapping of dependents (as IDs) for a given script ID.
  54       * Used to optimize recursive dependency tree checks.
  55       *
  56       * @since 6.9.0
  57       * @var array<string, string[]>
  58       */
  59      private $dependents_map = array();
  60  
  61      /**
  62       * Holds the valid values for fetchpriority.
  63       *
  64       * @since 6.9.0
  65       * @var string[]
  66       */
  67      private $priorities = array(
  68          'low',
  69          'auto',
  70          'high',
  71      );
  72  
  73      /**
  74       * List of IDs for script modules encountered which have missing dependencies.
  75       *
  76       * An ID is added to this list when it is discovered to have missing dependencies. At this time, a warning is
  77       * emitted with {@see _doing_it_wrong()}. The ID is then added to this list, so that duplicate warnings don't occur.
  78       *
  79       * @since 6.9.1
  80       * @var string[]
  81       */
  82      private $modules_with_missing_dependencies = array();
  83  
  84      /**
  85       * Registers the script module if no script module with that script module
  86       * identifier has already been registered.
  87       *
  88       * @since 6.5.0
  89       * @since 6.9.0 Added the $args parameter.
  90       *
  91       * @param string                     $id       The identifier of the script module. Should be unique. It will be used in the
  92       *                                             final import map.
  93       * @param string                     $src      Optional. Full URL of the script module, or path of the script module relative
  94       *                                             to the WordPress root directory. If it is provided and the script module has
  95       *                                             not been registered yet, it will be registered.
  96       * @param array<string|array>        $deps     {
  97       *                                                 Optional. List of dependencies.
  98       *
  99       *                                                 @type string|array ...$0 {
 100       *                                                     An array of script module identifiers of the dependencies of this script
 101       *                                                     module. The dependencies can be strings or arrays. If they are arrays,
 102       *                                                     they need an `id` key with the script module identifier, and can contain
 103       *                                                     an `import` key with either `static` or `dynamic`. By default,
 104       *                                                     dependencies that don't contain an `import` key are considered static.
 105       *
 106       *                                                     @type string $id     The script module identifier.
 107       *                                                     @type string $import Optional. Import type. May be either `static` or
 108       *                                                                          `dynamic`. Defaults to `static`.
 109       *                                                 }
 110       *                                             }
 111       * @param string|false|null          $version  Optional. String specifying the script module version number. Defaults to false.
 112       *                                             It is added to the URL as a query string for cache busting purposes. If $version
 113       *                                             is set to false, the version number is the currently installed WordPress version.
 114       *                                             If $version is set to null, no version is added.
 115       * @param array<string, string|bool> $args     {
 116       *     Optional. An array of additional args. Default empty array.
 117       *
 118       *     @type bool                $in_footer     Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
 119       *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
 120       * }
 121       */
 122  	public function register( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) {
 123          if ( '' === $id ) {
 124              _doing_it_wrong( __METHOD__, __( 'Non-empty string required for id.' ), '6.9.0' );
 125              return;
 126          }
 127  
 128          if ( ! isset( $this->registered[ $id ] ) ) {
 129              $dependencies = array();
 130              foreach ( $deps as $dependency ) {
 131                  if ( is_array( $dependency ) ) {
 132                      if ( ! isset( $dependency['id'] ) || ! is_string( $dependency['id'] ) ) {
 133                          _doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' );
 134                          continue;
 135                      }
 136                      $dependencies[] = array(
 137                          'id'     => $dependency['id'],
 138                          'import' => isset( $dependency['import'] ) && 'dynamic' === $dependency['import'] ? 'dynamic' : 'static',
 139                      );
 140                  } elseif ( is_string( $dependency ) ) {
 141                      $dependencies[] = array(
 142                          'id'     => $dependency,
 143                          'import' => 'static',
 144                      );
 145                  } else {
 146                      _doing_it_wrong( __METHOD__, __( 'Entries in dependencies array must be either strings or arrays with an id key.' ), '6.5.0' );
 147                  }
 148              }
 149  
 150              $in_footer = isset( $args['in_footer'] ) && (bool) $args['in_footer'];
 151  
 152              $fetchpriority = 'auto';
 153              if ( isset( $args['fetchpriority'] ) ) {
 154                  if ( $this->is_valid_fetchpriority( $args['fetchpriority'] ) ) {
 155                      $fetchpriority = $args['fetchpriority'];
 156                  } else {
 157                      _doing_it_wrong(
 158                          __METHOD__,
 159                          sprintf(
 160                              /* translators: 1: $fetchpriority, 2: $id */
 161                              __( 'Invalid fetchpriority `%1$s` defined for `%2$s` during script registration.' ),
 162                              is_string( $args['fetchpriority'] ) ? $args['fetchpriority'] : gettype( $args['fetchpriority'] ),
 163                              $id
 164                          ),
 165                          '6.9.0'
 166                      );
 167                  }
 168              }
 169  
 170              $this->registered[ $id ] = array(
 171                  'src'           => $src,
 172                  'version'       => $version,
 173                  'dependencies'  => $dependencies,
 174                  'in_footer'     => $in_footer,
 175                  'fetchpriority' => $fetchpriority,
 176              );
 177          }
 178      }
 179  
 180      /**
 181       * Gets IDs for queued script modules.
 182       *
 183       * @since 6.9.0
 184       *
 185       * @return string[] Script module IDs.
 186       */
 187  	public function get_queue(): array {
 188          return $this->queue;
 189      }
 190  
 191      /**
 192       * Checks if the provided fetchpriority is valid.
 193       *
 194       * @since 6.9.0
 195       *
 196       * @param string|mixed $priority Fetch priority.
 197       * @return bool Whether valid fetchpriority.
 198       */
 199  	private function is_valid_fetchpriority( $priority ): bool {
 200          return in_array( $priority, $this->priorities, true );
 201      }
 202  
 203      /**
 204       * Sets the fetch priority for a script module.
 205       *
 206       * @since 6.9.0
 207       *
 208       * @param string              $id       Script module identifier.
 209       * @param 'auto'|'low'|'high' $priority Fetch priority for the script module.
 210       * @return bool Whether setting the fetchpriority was successful.
 211       */
 212  	public function set_fetchpriority( string $id, string $priority ): bool {
 213          if ( ! isset( $this->registered[ $id ] ) ) {
 214              return false;
 215          }
 216  
 217          if ( '' === $priority ) {
 218              $priority = 'auto';
 219          }
 220  
 221          if ( ! $this->is_valid_fetchpriority( $priority ) ) {
 222              _doing_it_wrong(
 223                  __METHOD__,
 224                  /* translators: %s: Invalid fetchpriority. */
 225                  sprintf( __( 'Invalid fetchpriority: %s' ), $priority ),
 226                  '6.9.0'
 227              );
 228              return false;
 229          }
 230  
 231          $this->registered[ $id ]['fetchpriority'] = $priority;
 232          return true;
 233      }
 234  
 235      /**
 236       * Sets whether a script module should be printed in the footer.
 237       *
 238       * This is only relevant in block themes.
 239       *
 240       * @since 6.9.0
 241       *
 242       * @param string           $id        Script module identifier.
 243       * @param bool             $in_footer Whether to print in the footer.
 244       * @return bool Whether setting the printing location was successful.
 245       */
 246  	public function set_in_footer( string $id, bool $in_footer ): bool {
 247          if ( ! isset( $this->registered[ $id ] ) ) {
 248              return false;
 249          }
 250          $this->registered[ $id ]['in_footer'] = $in_footer;
 251          return true;
 252      }
 253  
 254      /**
 255       * Marks the script module to be enqueued in the page.
 256       *
 257       * If a src is provided and the script module has not been registered yet, it
 258       * will be registered.
 259       *
 260       * @since 6.5.0
 261       * @since 6.9.0 Added the $args parameter.
 262       *
 263       * @param string                     $id       The identifier of the script module. Should be unique. It will be used in the
 264       *                                             final import map.
 265       * @param string                     $src      Optional. Full URL of the script module, or path of the script module relative
 266       *                                             to the WordPress root directory. If it is provided and the script module has
 267       *                                             not been registered yet, it will be registered.
 268       * @param array<string|array>        $deps     {
 269       *                                                 Optional. List of dependencies.
 270       *
 271       *                                                 @type string|array ...$0 {
 272       *                                                     An array of script module identifiers of the dependencies of this script
 273       *                                                     module. The dependencies can be strings or arrays. If they are arrays,
 274       *                                                     they need an `id` key with the script module identifier, and can contain
 275       *                                                     an `import` key with either `static` or `dynamic`. By default,
 276       *                                                     dependencies that don't contain an `import` key are considered static.
 277       *
 278       *                                                     @type string $id     The script module identifier.
 279       *                                                     @type string $import Optional. Import type. May be either `static` or
 280       *                                                                          `dynamic`. Defaults to `static`.
 281       *                                                 }
 282       *                                             }
 283       * @param string|false|null          $version  Optional. String specifying the script module version number. Defaults to false.
 284       *                                             It is added to the URL as a query string for cache busting purposes. If $version
 285       *                                             is set to false, the version number is the currently installed WordPress version.
 286       *                                             If $version is set to null, no version is added.
 287       * @param array<string, string|bool> $args     {
 288       *     Optional. An array of additional args. Default empty array.
 289       *
 290       *     @type bool                $in_footer     Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
 291       *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
 292       * }
 293       */
 294  	public function enqueue( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) {
 295          if ( '' === $id ) {
 296              _doing_it_wrong( __METHOD__, __( 'Non-empty string required for id.' ), '6.9.0' );
 297              return;
 298          }
 299  
 300          if ( ! in_array( $id, $this->queue, true ) ) {
 301              $this->queue[] = $id;
 302          }
 303          if ( ! isset( $this->registered[ $id ] ) && $src ) {
 304              $this->register( $id, $src, $deps, $version, $args );
 305          }
 306      }
 307  
 308      /**
 309       * Unmarks the script module so it will no longer be enqueued in the page.
 310       *
 311       * @since 6.5.0
 312       *
 313       * @param string $id The identifier of the script module.
 314       */
 315  	public function dequeue( string $id ) {
 316          $this->queue = array_values( array_diff( $this->queue, array( $id ) ) );
 317      }
 318  
 319      /**
 320       * Removes a registered script module.
 321       *
 322       * @since 6.5.0
 323       *
 324       * @param string $id The identifier of the script module.
 325       */
 326  	public function deregister( string $id ) {
 327          $this->dequeue( $id );
 328          unset( $this->registered[ $id ] );
 329      }
 330  
 331      /**
 332       * Adds the hooks to print the import map, enqueued script modules and script
 333       * module preloads.
 334       *
 335       * In classic themes, the script modules used by the blocks are not yet known
 336       * when the `wp_head` actions is fired, so it needs to print everything in the
 337       * footer.
 338       *
 339       * @since 6.5.0
 340       */
 341  	public function add_hooks() {
 342          $is_block_theme = wp_is_block_theme();
 343          $position       = $is_block_theme ? 'wp_head' : 'wp_footer';
 344          add_action( $position, array( $this, 'print_import_map' ) );
 345          if ( $is_block_theme ) {
 346              /*
 347               * Modules can only be printed in the head for block themes because only with
 348               * block themes will import map be fully populated by modules discovered by
 349               * rendering the block template. In classic themes, modules are enqueued during
 350               * template rendering, thus the import map must be printed in the footer,
 351               * followed by all enqueued modules.
 352               */
 353              add_action( 'wp_head', array( $this, 'print_head_enqueued_script_modules' ) );
 354          }
 355          add_action( 'wp_footer', array( $this, 'print_enqueued_script_modules' ) );
 356          add_action( $position, array( $this, 'print_script_module_preloads' ) );
 357  
 358          add_action( 'admin_print_footer_scripts', array( $this, 'print_import_map' ) );
 359          add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) );
 360          add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_preloads' ) );
 361  
 362          add_action( 'wp_footer', array( $this, 'print_script_module_data' ) );
 363          add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_data' ) );
 364          add_action( 'wp_footer', array( $this, 'print_a11y_script_module_html' ), 20 );
 365          add_action( 'admin_print_footer_scripts', array( $this, 'print_a11y_script_module_html' ), 20 );
 366      }
 367  
 368      /**
 369       * Gets the highest fetch priority for the provided script IDs.
 370       *
 371       * @since 6.9.0
 372       *
 373       * @param string[] $ids Script module IDs.
 374       * @return 'auto'|'low'|'high' Highest fetch priority for the provided script module IDs.
 375       */
 376  	private function get_highest_fetchpriority( array $ids ): string {
 377          static $high_priority_index = null;
 378          if ( null === $high_priority_index ) {
 379              $high_priority_index = count( $this->priorities ) - 1;
 380          }
 381  
 382          $highest_priority_index = 0;
 383          foreach ( $ids as $id ) {
 384              if ( isset( $this->registered[ $id ] ) ) {
 385                  $highest_priority_index = (int) max(
 386                      $highest_priority_index,
 387                      (int) array_search( $this->registered[ $id ]['fetchpriority'], $this->priorities, true )
 388                  );
 389                  if ( $high_priority_index === $highest_priority_index ) {
 390                      break;
 391                  }
 392              }
 393          }
 394  
 395          return $this->priorities[ $highest_priority_index ];
 396      }
 397  
 398      /**
 399       * Prints the enqueued script modules in head.
 400       *
 401       * This is only used in block themes.
 402       *
 403       * @since 6.9.0
 404       */
 405  	public function print_head_enqueued_script_modules() {
 406          foreach ( $this->get_sorted_dependencies( $this->queue ) as $id ) {
 407              if (
 408                  isset( $this->registered[ $id ] ) &&
 409                  ! $this->registered[ $id ]['in_footer']
 410              ) {
 411                  // If any dependency is set to be printed in footer, skip printing this module in head.
 412                  $dependencies = array_keys( $this->get_dependencies( array( $id ) ) );
 413                  foreach ( $dependencies as $dependency_id ) {
 414                      if (
 415                          in_array( $dependency_id, $this->queue, true ) &&
 416                          isset( $this->registered[ $dependency_id ] ) &&
 417                          $this->registered[ $dependency_id ]['in_footer']
 418                      ) {
 419                          continue 2;
 420                      }
 421                  }
 422                  $this->print_script_module( $id );
 423              }
 424          }
 425      }
 426  
 427      /**
 428       * Prints the enqueued script modules in footer.
 429       *
 430       * @since 6.5.0
 431       */
 432  	public function print_enqueued_script_modules() {
 433          foreach ( $this->get_sorted_dependencies( $this->queue ) as $id ) {
 434              $this->print_script_module( $id );
 435          }
 436      }
 437  
 438      /**
 439       * Prints the enqueued script module using script tags with type="module"
 440       * attributes.
 441       *
 442       * @since 6.9.0
 443       *
 444       * @param string $id The script module identifier.
 445       */
 446  	private function print_script_module( string $id ) {
 447          if ( in_array( $id, $this->done, true ) || ! in_array( $id, $this->queue, true ) ) {
 448              return;
 449          }
 450  
 451          $this->done[] = $id;
 452  
 453          $src = $this->get_src( $id );
 454          if ( '' === $src ) {
 455              return;
 456          }
 457  
 458          $attributes = array(
 459              'type' => 'module',
 460              'src'  => $src,
 461              'id'   => $id . '-js-module',
 462          );
 463  
 464          $script_module     = $this->registered[ $id ];
 465          $queued_dependents = array_intersect( $this->queue, $this->get_recursive_dependents( $id ) );
 466          $fetchpriority     = $this->get_highest_fetchpriority( array_merge( array( $id ), $queued_dependents ) );
 467          if ( 'auto' !== $fetchpriority ) {
 468              $attributes['fetchpriority'] = $fetchpriority;
 469          }
 470          if ( $fetchpriority !== $script_module['fetchpriority'] ) {
 471              $attributes['data-wp-fetchpriority'] = $script_module['fetchpriority'];
 472          }
 473          wp_print_script_tag( $attributes );
 474      }
 475  
 476      /**
 477       * Prints the static dependencies of the enqueued script modules using
 478       * link tags with rel="modulepreload" attributes.
 479       *
 480       * If a script module is marked for enqueue, it will not be preloaded.
 481       *
 482       * @since 6.5.0
 483       */
 484  	public function print_script_module_preloads() {
 485          $dependency_ids = $this->get_sorted_dependencies( $this->queue, array( 'static' ) );
 486          foreach ( $dependency_ids as $id ) {
 487              // Don't preload if it's marked for enqueue.
 488              if ( in_array( $id, $this->queue, true ) ) {
 489                  continue;
 490              }
 491  
 492              $src = $this->get_src( $id );
 493              if ( '' === $src ) {
 494                  continue;
 495              }
 496  
 497              $enqueued_dependents   = array_intersect( $this->get_recursive_dependents( $id ), $this->queue );
 498              $highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents );
 499              printf(
 500                  '<link rel="modulepreload" href="%s" id="%s"',
 501                  esc_url( $src ),
 502                  esc_attr( $id . '-js-modulepreload' )
 503              );
 504              if ( 'auto' !== $highest_fetchpriority ) {
 505                  printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) );
 506              }
 507              if ( $highest_fetchpriority !== $this->registered[ $id ]['fetchpriority'] && 'auto' !== $this->registered[ $id ]['fetchpriority'] ) {
 508                  printf( ' data-wp-fetchpriority="%s"', esc_attr( $this->registered[ $id ]['fetchpriority'] ) );
 509              }
 510              echo ">\n";
 511          }
 512      }
 513  
 514      /**
 515       * Prints the import map using a script tag with a type="importmap" attribute.
 516       *
 517       * @since 6.5.0
 518       */
 519  	public function print_import_map() {
 520          $import_map = $this->get_import_map();
 521          if ( ! empty( $import_map['imports'] ) ) {
 522              wp_print_inline_script_tag(
 523                  (string) wp_json_encode( $import_map, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
 524                  array(
 525                      'type' => 'importmap',
 526                      'id'   => 'wp-importmap',
 527                  )
 528              );
 529          }
 530      }
 531  
 532      /**
 533       * Returns the import map array.
 534       *
 535       * @since 6.5.0
 536       *
 537       * @return array<string, array<string, string>> Array with an `imports` key mapping to an array of script module
 538       *                                              identifiers and their respective URLs, including the version query.
 539       */
 540  	private function get_import_map(): array {
 541          $imports = array();
 542          foreach ( array_keys( $this->get_dependencies( $this->queue ) ) as $id ) {
 543              $src = $this->get_src( $id );
 544              if ( '' !== $src ) {
 545                  $imports[ $id ] = $src;
 546              }
 547          }
 548          return array( 'imports' => $imports );
 549      }
 550  
 551      /**
 552       * Retrieves the list of script modules marked for enqueue.
 553       *
 554       * Even though this is a private method and is unused in core, there are ecosystem plugins accessing it via the
 555       * Reflection API. The ecosystem should rather use {@see self::get_queue()}.
 556       *
 557       * @since 6.5.0
 558       *
 559       * @return array<string, array<string, mixed>> Script modules marked for enqueue, keyed by script module identifier.
 560       */
 561  	private function get_marked_for_enqueue(): array {
 562          return wp_array_slice_assoc(
 563              $this->registered,
 564              $this->queue
 565          );
 566      }
 567  
 568      /**
 569       * Retrieves all the dependencies for the given script module identifiers, filtered by import types.
 570       *
 571       * It will consolidate an array containing a set of unique dependencies based
 572       * on the requested import types: 'static', 'dynamic', or both. This method is
 573       * recursive and also retrieves dependencies of the dependencies.
 574       *
 575       * @since 6.5.0
 576       *
 577       * @param string[] $ids          The identifiers of the script modules for which to gather dependencies.
 578       * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
 579       *                                         Default is both.
 580       * @return array<string, array<string, mixed>> List of dependencies, keyed by script module identifier.
 581       */
 582  	private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
 583          $all_dependencies = array();
 584          $id_queue         = $ids;
 585  
 586          while ( ! empty( $id_queue ) ) {
 587              $id = array_shift( $id_queue );
 588              if ( ! isset( $this->registered[ $id ] ) ) {
 589                  continue;
 590              }
 591  
 592              foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
 593                  if (
 594                      ! isset( $all_dependencies[ $dependency['id'] ] ) &&
 595                      in_array( $dependency['import'], $import_types, true ) &&
 596                      isset( $this->registered[ $dependency['id'] ] )
 597                  ) {
 598                      $all_dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
 599  
 600                      // Add this dependency to the list to get dependencies for.
 601                      $id_queue[] = $dependency['id'];
 602                  }
 603              }
 604          }
 605  
 606          return $all_dependencies;
 607      }
 608  
 609      /**
 610       * Gets all dependents of a script module.
 611       *
 612       * This is not recursive.
 613       *
 614       * @since 6.9.0
 615       *
 616       * @see WP_Scripts::get_dependents()
 617       *
 618       * @param string $id The script ID.
 619       * @return string[] Script module IDs.
 620       */
 621  	private function get_dependents( string $id ): array {
 622          // Check if dependents map for the handle in question is present. If so, use it.
 623          if ( isset( $this->dependents_map[ $id ] ) ) {
 624              return $this->dependents_map[ $id ];
 625          }
 626  
 627          $dependents = array();
 628  
 629          // Iterate over all registered scripts, finding dependents of the script passed to this method.
 630          foreach ( $this->registered as $registered_id => $args ) {
 631              if ( in_array( $id, wp_list_pluck( $args['dependencies'], 'id' ), true ) ) {
 632                  $dependents[] = $registered_id;
 633              }
 634          }
 635  
 636          // Add the module's dependents to the map to ease future lookups.
 637          $this->dependents_map[ $id ] = $dependents;
 638  
 639          return $dependents;
 640      }
 641  
 642      /**
 643       * Gets all recursive dependents of a script module.
 644       *
 645       * @since 6.9.0
 646       *
 647       * @see WP_Scripts::get_dependents()
 648       *
 649       * @param string $id The script ID.
 650       * @return string[] Script module IDs.
 651       */
 652  	private function get_recursive_dependents( string $id ): array {
 653          $dependents = array();
 654          $id_queue   = array( $id );
 655          $processed  = array();
 656  
 657          while ( ! empty( $id_queue ) ) {
 658              $current_id = array_shift( $id_queue );
 659  
 660              // Skip unregistered or already-processed script modules.
 661              if ( ! isset( $this->registered[ $current_id ] ) || isset( $processed[ $current_id ] ) ) {
 662                  continue;
 663              }
 664  
 665              // Mark as processed to guard against infinite loops from circular dependencies.
 666              $processed[ $current_id ] = true;
 667  
 668              // Find the direct dependents of the current script.
 669              foreach ( $this->get_dependents( $current_id ) as $dependent_id ) {
 670                  // Only add the dependent if we haven't found it before.
 671                  if ( ! isset( $dependents[ $dependent_id ] ) ) {
 672                      $dependents[ $dependent_id ] = true;
 673  
 674                      // Add dependency to the queue.
 675                      $id_queue[] = $dependent_id;
 676                  }
 677              }
 678          }
 679  
 680          return array_keys( $dependents );
 681      }
 682  
 683      /**
 684       * Sorts the given script module identifiers based on their dependencies.
 685       *
 686       * It will return a list of script module identifiers sorted in the order
 687       * they should be printed, so that dependencies are printed before the script
 688       * modules that depend on them.
 689       *
 690       * @since 6.9.0
 691       *
 692       * @param string[] $ids          The identifiers of the script modules to sort.
 693       * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
 694       *                                         Default is both.
 695       * @return string[] Sorted list of script module identifiers.
 696       */
 697  	private function get_sorted_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
 698          $sorted = array();
 699  
 700          foreach ( $ids as $id ) {
 701              $this->sort_item_dependencies( $id, $import_types, $sorted );
 702          }
 703  
 704          return array_unique( $sorted );
 705      }
 706  
 707      /**
 708       * Recursively sorts the dependencies for a single script module identifier.
 709       *
 710       * @since 6.9.0
 711       *
 712       * @param string   $id           The identifier of the script module to sort.
 713       * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
 714       * @param string[] &$sorted      The array of sorted identifiers, passed by reference.
 715       * @return bool True on success, false on failure (e.g., missing dependency).
 716       */
 717  	private function sort_item_dependencies( string $id, array $import_types, array &$sorted ): bool {
 718          // If already processed, don't do it again.
 719          if ( in_array( $id, $sorted, true ) ) {
 720              return true;
 721          }
 722  
 723          // If the item doesn't exist, fail.
 724          if ( ! isset( $this->registered[ $id ] ) ) {
 725              return false;
 726          }
 727  
 728          $dependency_ids = array();
 729          foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
 730              if ( in_array( $dependency['import'], $import_types, true ) ) {
 731                  $dependency_ids[] = $dependency['id'];
 732              }
 733          }
 734  
 735          // If the item requires dependencies that do not exist, fail.
 736          $missing_dependencies = array_diff( $dependency_ids, array_keys( $this->registered ) );
 737          if ( count( $missing_dependencies ) > 0 ) {
 738              if ( ! in_array( $id, $this->modules_with_missing_dependencies, true ) ) {
 739                  _doing_it_wrong(
 740                      get_class( $this ) . '::register',
 741                      sprintf(
 742                          /* translators: 1: Script module ID, 2: Comma-separated list of missing dependency IDs. */
 743                          __( 'The script module with the ID "%1$s" was enqueued with dependencies that are not registered: %2$s.' ),
 744                          $id,
 745                          implode( ', ', $missing_dependencies )
 746                      ),
 747                      '6.9.1'
 748                  );
 749                  $this->modules_with_missing_dependencies[] = $id;
 750              }
 751  
 752              return false;
 753          }
 754  
 755          // Recursively process dependencies.
 756          foreach ( $dependency_ids as $dependency_id ) {
 757              if ( ! $this->sort_item_dependencies( $dependency_id, $import_types, $sorted ) ) {
 758                  // A dependency failed to resolve, so this branch fails.
 759                  return false;
 760              }
 761          }
 762  
 763          // All dependencies are sorted, so we can now add the current item.
 764          $sorted[] = $id;
 765  
 766          return true;
 767      }
 768  
 769      /**
 770       * Gets the versioned URL for a script module src.
 771       *
 772       * If $version is set to false, the version number is the currently installed
 773       * WordPress version. If $version is set to null, no version is added.
 774       * Otherwise, the string passed in $version is used.
 775       *
 776       * @since 6.5.0
 777       *
 778       * @param string $id The script module identifier.
 779       * @return string The script module src with a version if relevant.
 780       */
 781  	private function get_src( string $id ): string {
 782          if ( ! isset( $this->registered[ $id ] ) ) {
 783              return '';
 784          }
 785  
 786          $script_module = $this->registered[ $id ];
 787          $src           = $script_module['src'];
 788  
 789          if ( '' !== $src ) {
 790              if ( false === $script_module['version'] ) {
 791                  $src = add_query_arg( 'ver', get_bloginfo( 'version' ), $src );
 792              } elseif ( null !== $script_module['version'] ) {
 793                  $src = add_query_arg( 'ver', $script_module['version'], $src );
 794              }
 795          }
 796  
 797          /**
 798           * Filters the script module source.
 799           *
 800           * @since 6.5.0
 801           *
 802           * @param string $src Module source URL.
 803           * @param string $id  Module identifier.
 804           */
 805          $src = apply_filters( 'script_module_loader_src', $src, $id );
 806          if ( ! is_string( $src ) ) {
 807              $src = '';
 808          }
 809  
 810          return $src;
 811      }
 812  
 813      /**
 814       * Print data associated with Script Modules.
 815       *
 816       * The data will be embedded in the page HTML and can be read by Script Modules on page load.
 817       *
 818       * @since 6.7.0
 819       *
 820       * Data can be associated with a Script Module via the
 821       * {@see "script_module_data_{$module_id}"} filter.
 822       *
 823       * The data for a Script Module will be serialized as JSON in a script tag with an ID of the
 824       * form `wp-script-module-data-{$module_id}`.
 825       */
 826  	public function print_script_module_data(): void {
 827          $modules = array();
 828          foreach ( array_unique( $this->queue ) as $id ) {
 829              if ( '@wordpress/a11y' === $id ) {
 830                  $this->a11y_available = true;
 831              }
 832              $modules[ $id ] = true;
 833          }
 834          foreach ( array_keys( $this->get_import_map()['imports'] ) as $id ) {
 835              if ( '@wordpress/a11y' === $id ) {
 836                  $this->a11y_available = true;
 837              }
 838              $modules[ $id ] = true;
 839          }
 840  
 841          foreach ( array_keys( $modules ) as $module_id ) {
 842              /**
 843               * Filters data associated with a given Script Module.
 844               *
 845               * Script Modules may require data that is required for initialization or is essential
 846               * to have immediately available on page load. These are suitable use cases for
 847               * this data.
 848               *
 849               * The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID
 850               * that the data is associated with.
 851               *
 852               * This is best suited to pass essential data that must be available to the module for
 853               * initialization or immediately on page load. It does not replace the REST API or
 854               * fetching data from the client.
 855               *
 856               * Example:
 857               *
 858               *     add_filter(
 859               *         'script_module_data_MyScriptModuleID',
 860               *         function ( array $data ): array {
 861               *             $data['dataForClient'] = 'ok';
 862               *             return $data;
 863               *         }
 864               *     );
 865               *
 866               * If the filter returns no data (an empty array), nothing will be embedded in the page.
 867               *
 868               * The data for a given Script Module, if provided, will be JSON serialized in a script
 869               * tag with an ID of the form `wp-script-module-data-{$module_id}`.
 870               *
 871               * The data can be read on the client with a pattern like this:
 872               *
 873               * Example:
 874               *
 875               *     const dataContainer = document.getElementById( 'wp-script-module-data-MyScriptModuleID' );
 876               *     let data = {};
 877               *     if ( dataContainer ) {
 878               *         try {
 879               *             data = JSON.parse( dataContainer.textContent );
 880               *         } catch {}
 881               *     }
 882               *     // data.dataForClient === 'ok';
 883               *     initMyScriptModuleWithData( data );
 884               *
 885               * @since 6.7.0
 886               *
 887               * @param array $data The data associated with the Script Module.
 888               */
 889              $data = apply_filters( "script_module_data_{$module_id}", array() );
 890  
 891              if ( is_array( $data ) && array() !== $data ) {
 892                  /*
 893                   * This data will be printed as JSON inside a script tag like this:
 894                   *   <script type="application/json"></script>
 895                   *
 896                   * A script tag must be closed by a sequence beginning with `</`. It's impossible to
 897                   * close a script tag without using `<`. We ensure that `<` is escaped and `/` can
 898                   * remain unescaped, so `</script>` will be printed as `\u003C/script\u00E3`.
 899                   *
 900                   *   - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E.
 901                   *   - JSON_UNESCAPED_SLASHES: Don't escape /.
 902                   *
 903                   * If the page will use UTF-8 encoding, it's safe to print unescaped unicode:
 904                   *
 905                   *   - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`).
 906                   *   - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when
 907                   *     JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was
 908                   *     before PHP 7.1 without this constant. Available as of PHP 7.1.0.
 909                   *
 910                   * The JSON specification requires encoding in UTF-8, so if the generated HTML page
 911                   * is not encoded in UTF-8 then it's not safe to include those literals. They must
 912                   * be escaped to avoid encoding issues.
 913                   *
 914                   * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements.
 915                   * @see https://www.php.net/manual/en/json.constants.php for details on these constants.
 916                   * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing.
 917                   */
 918                  $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS;
 919                  if ( ! is_utf8_charset() ) {
 920                      $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
 921                  }
 922  
 923                  wp_print_inline_script_tag(
 924                      (string) wp_json_encode(
 925                          $data,
 926                          $json_encode_flags
 927                      ),
 928                      array(
 929                          'type' => 'application/json',
 930                          'id'   => "wp-script-module-data-{$module_id}",
 931                      )
 932                  );
 933              }
 934          }
 935      }
 936  
 937      /**
 938       * @access private This is only intended to be called by the registered actions.
 939       *
 940       * @since 6.7.0
 941       */
 942  	public function print_a11y_script_module_html() {
 943          if ( ! $this->a11y_available ) {
 944              return;
 945          }
 946          echo '<div style="position:absolute;margin:-1px;padding:0;height:1px;width:1px;overflow:hidden;clip-path:inset(50%);border:0;word-wrap:normal !important;">'
 947              . '<p id="a11y-speak-intro-text" class="a11y-speak-intro-text" hidden>' . esc_html__( 'Notifications' ) . '</p>'
 948              . '<div id="a11y-speak-assertive" class="a11y-speak-region" aria-live="assertive" aria-relevant="additions text" aria-atomic="true"></div>'
 949              . '<div id="a11y-speak-polite" class="a11y-speak-region" aria-live="polite" aria-relevant="additions text" aria-atomic="true"></div>'
 950              . '</div>';
 951      }
 952  }


Generated : Tue Apr 21 08:20:12 2026 Cross-referenced by PHPXref