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


Generated : Tue Jun 16 08:20:09 2026 Cross-referenced by PHPXref