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


Generated : Thu Oct 23 08:20:05 2025 Cross-referenced by PHPXref