[ 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[]
  22       */
  23      private $registered = array();
  24  
  25      /**
  26       * Holds the script module identifiers that were enqueued before registered.
  27       *
  28       * @since 6.5.0
  29       * @var array<string, true>
  30       */
  31      private $enqueued_before_registered = array();
  32  
  33      /**
  34       * Tracks whether the @wordpress/a11y script module is available.
  35       *
  36       * Some additional HTML is required on the page for the module to work. Track
  37       * whether it's available to print at the appropriate time.
  38       *
  39       * @since 6.7.0
  40       * @var bool
  41       */
  42      private $a11y_available = false;
  43  
  44      /**
  45       * Registers the script module if no script module with that script module
  46       * identifier has already been registered.
  47       *
  48       * @since 6.5.0
  49       * @since 6.9.0 Added the $args parameter.
  50       *
  51       * @param string            $id       The identifier of the script module. Should be unique. It will be used in the
  52       *                                    final import map.
  53       * @param string            $src      Optional. Full URL of the script module, or path of the script module relative
  54       *                                    to the WordPress root directory. If it is provided and the script module has
  55       *                                    not been registered yet, it will be registered.
  56       * @param array             $deps     {
  57       *                                        Optional. List of dependencies.
  58       *
  59       *                                        @type string|array ...$0 {
  60       *                                            An array of script module identifiers of the dependencies of this script
  61       *                                            module. The dependencies can be strings or arrays. If they are arrays,
  62       *                                            they need an `id` key with the script module identifier, and can contain
  63       *                                            an `import` key with either `static` or `dynamic`. By default,
  64       *                                            dependencies that don't contain an `import` key are considered static.
  65       *
  66       *                                            @type string $id     The script module identifier.
  67       *                                            @type string $import Optional. Import type. May be either `static` or
  68       *                                                                 `dynamic`. Defaults to `static`.
  69       *                                        }
  70       *                                    }
  71       * @param string|false|null $version  Optional. String specifying the script module version number. Defaults to false.
  72       *                                    It is added to the URL as a query string for cache busting purposes. If $version
  73       *                                    is set to false, the version number is the currently installed WordPress version.
  74       *                                    If $version is set to null, no version is added.
  75       * @param array             $args     {
  76       *     Optional. An array of additional args. Default empty array.
  77       *
  78       *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
  79       * }
  80       */
  81  	public function register( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) {
  82          if ( ! isset( $this->registered[ $id ] ) ) {
  83              $dependencies = array();
  84              foreach ( $deps as $dependency ) {
  85                  if ( is_array( $dependency ) ) {
  86                      if ( ! isset( $dependency['id'] ) || ! is_string( $dependency['id'] ) ) {
  87                          _doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' );
  88                          continue;
  89                      }
  90                      $dependencies[] = array(
  91                          'id'     => $dependency['id'],
  92                          'import' => isset( $dependency['import'] ) && 'dynamic' === $dependency['import'] ? 'dynamic' : 'static',
  93                      );
  94                  } elseif ( is_string( $dependency ) ) {
  95                      $dependencies[] = array(
  96                          'id'     => $dependency,
  97                          'import' => 'static',
  98                      );
  99                  } else {
 100                      _doing_it_wrong( __METHOD__, __( 'Entries in dependencies array must be either strings or arrays with an id key.' ), '6.5.0' );
 101                  }
 102              }
 103  
 104              $fetchpriority = 'auto';
 105              if ( isset( $args['fetchpriority'] ) ) {
 106                  if ( $this->is_valid_fetchpriority( $args['fetchpriority'] ) ) {
 107                      $fetchpriority = $args['fetchpriority'];
 108                  } else {
 109                      _doing_it_wrong(
 110                          __METHOD__,
 111                          sprintf(
 112                              /* translators: 1: $fetchpriority, 2: $id */
 113                              __( 'Invalid fetchpriority `%1$s` defined for `%2$s` during script registration.' ),
 114                              is_string( $args['fetchpriority'] ) ? $args['fetchpriority'] : gettype( $args['fetchpriority'] ),
 115                              $id
 116                          ),
 117                          '6.9.0'
 118                      );
 119                  }
 120              }
 121  
 122              $this->registered[ $id ] = array(
 123                  'src'           => $src,
 124                  'version'       => $version,
 125                  'enqueue'       => isset( $this->enqueued_before_registered[ $id ] ),
 126                  'dependencies'  => $dependencies,
 127                  'fetchpriority' => $fetchpriority,
 128              );
 129          }
 130      }
 131  
 132      /**
 133       * Checks if the provided fetchpriority is valid.
 134       *
 135       * @since 6.9.0
 136       *
 137       * @param string|mixed $priority Fetch priority.
 138       * @return bool Whether valid fetchpriority.
 139       */
 140  	private function is_valid_fetchpriority( $priority ): bool {
 141          return in_array( $priority, array( 'auto', 'low', 'high' ), true );
 142      }
 143  
 144      /**
 145       * Sets the fetch priority for a script module.
 146       *
 147       * @since 6.9.0
 148       *
 149       * @param string              $id       Script module identifier.
 150       * @param 'auto'|'low'|'high' $priority Fetch priority for the script module.
 151       * @return bool Whether setting the fetchpriority was successful.
 152       */
 153  	public function set_fetchpriority( string $id, string $priority ): bool {
 154          if ( ! isset( $this->registered[ $id ] ) ) {
 155              return false;
 156          }
 157  
 158          if ( '' === $priority ) {
 159              $priority = 'auto';
 160          }
 161  
 162          if ( ! $this->is_valid_fetchpriority( $priority ) ) {
 163              _doing_it_wrong(
 164                  __METHOD__,
 165                  /* translators: %s: Invalid fetchpriority. */
 166                  sprintf( __( 'Invalid fetchpriority: %s' ), $priority ),
 167                  '6.9.0'
 168              );
 169              return false;
 170          }
 171  
 172          $this->registered[ $id ]['fetchpriority'] = $priority;
 173          return true;
 174      }
 175  
 176      /**
 177       * Marks the script module to be enqueued in the page.
 178       *
 179       * If a src is provided and the script module has not been registered yet, it
 180       * will be registered.
 181       *
 182       * @since 6.5.0
 183       * @since 6.9.0 Added the $args parameter.
 184       *
 185       * @param string            $id       The identifier of the script module. Should be unique. It will be used in the
 186       *                                    final import map.
 187       * @param string            $src      Optional. Full URL of the script module, or path of the script module relative
 188       *                                    to the WordPress root directory. If it is provided and the script module has
 189       *                                    not been registered yet, it will be registered.
 190       * @param array             $deps     {
 191       *                                        Optional. List of dependencies.
 192       *
 193       *                                        @type string|array ...$0 {
 194       *                                            An array of script module identifiers of the dependencies of this script
 195       *                                            module. The dependencies can be strings or arrays. If they are arrays,
 196       *                                            they need an `id` key with the script module identifier, and can contain
 197       *                                            an `import` key with either `static` or `dynamic`. By default,
 198       *                                            dependencies that don't contain an `import` key are considered static.
 199       *
 200       *                                            @type string $id     The script module identifier.
 201       *                                            @type string $import Optional. Import type. May be either `static` or
 202       *                                                                 `dynamic`. Defaults to `static`.
 203       *                                        }
 204       *                                    }
 205       * @param string|false|null $version  Optional. String specifying the script module version number. Defaults to false.
 206       *                                    It is added to the URL as a query string for cache busting purposes. If $version
 207       *                                    is set to false, the version number is the currently installed WordPress version.
 208       *                                    If $version is set to null, no version is added.
 209       * @param array             $args     {
 210       *     Optional. An array of additional args. Default empty array.
 211       *
 212       *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
 213       * }
 214       */
 215  	public function enqueue( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) {
 216          if ( isset( $this->registered[ $id ] ) ) {
 217              $this->registered[ $id ]['enqueue'] = true;
 218          } elseif ( $src ) {
 219              $this->register( $id, $src, $deps, $version, $args );
 220              $this->registered[ $id ]['enqueue'] = true;
 221          } else {
 222              $this->enqueued_before_registered[ $id ] = true;
 223          }
 224      }
 225  
 226      /**
 227       * Unmarks the script module so it will no longer be enqueued in the page.
 228       *
 229       * @since 6.5.0
 230       *
 231       * @param string $id The identifier of the script module.
 232       */
 233  	public function dequeue( string $id ) {
 234          if ( isset( $this->registered[ $id ] ) ) {
 235              $this->registered[ $id ]['enqueue'] = false;
 236          }
 237          unset( $this->enqueued_before_registered[ $id ] );
 238      }
 239  
 240      /**
 241       * Removes a registered script module.
 242       *
 243       * @since 6.5.0
 244       *
 245       * @param string $id The identifier of the script module.
 246       */
 247  	public function deregister( string $id ) {
 248          unset( $this->registered[ $id ] );
 249          unset( $this->enqueued_before_registered[ $id ] );
 250      }
 251  
 252      /**
 253       * Adds the hooks to print the import map, enqueued script modules and script
 254       * module preloads.
 255       *
 256       * In classic themes, the script modules used by the blocks are not yet known
 257       * when the `wp_head` actions is fired, so it needs to print everything in the
 258       * footer.
 259       *
 260       * @since 6.5.0
 261       */
 262  	public function add_hooks() {
 263          $position = wp_is_block_theme() ? 'wp_head' : 'wp_footer';
 264          add_action( $position, array( $this, 'print_import_map' ) );
 265          add_action( $position, array( $this, 'print_enqueued_script_modules' ) );
 266          add_action( $position, array( $this, 'print_script_module_preloads' ) );
 267  
 268          add_action( 'admin_print_footer_scripts', array( $this, 'print_import_map' ) );
 269          add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) );
 270          add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_preloads' ) );
 271  
 272          add_action( 'wp_footer', array( $this, 'print_script_module_data' ) );
 273          add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_data' ) );
 274          add_action( 'wp_footer', array( $this, 'print_a11y_script_module_html' ), 20 );
 275          add_action( 'admin_print_footer_scripts', array( $this, 'print_a11y_script_module_html' ), 20 );
 276      }
 277  
 278      /**
 279       * Prints the enqueued script modules using script tags with type="module"
 280       * attributes.
 281       *
 282       * @since 6.5.0
 283       */
 284  	public function print_enqueued_script_modules() {
 285          foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) {
 286              $args = array(
 287                  'type' => 'module',
 288                  'src'  => $this->get_src( $id ),
 289                  'id'   => $id . '-js-module',
 290              );
 291              if ( 'auto' !== $script_module['fetchpriority'] ) {
 292                  $args['fetchpriority'] = $script_module['fetchpriority'];
 293              }
 294              wp_print_script_tag( $args );
 295          }
 296      }
 297  
 298      /**
 299       * Prints the the static dependencies of the enqueued script modules using
 300       * link tags with rel="modulepreload" attributes.
 301       *
 302       * If a script module is marked for enqueue, it will not be preloaded.
 303       *
 304       * @since 6.5.0
 305       */
 306  	public function print_script_module_preloads() {
 307          foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) {
 308              // Don't preload if it's marked for enqueue.
 309              if ( true !== $script_module['enqueue'] ) {
 310                  echo sprintf(
 311                      '<link rel="modulepreload" href="%s" id="%s"%s>',
 312                      esc_url( $this->get_src( $id ) ),
 313                      esc_attr( $id . '-js-modulepreload' ),
 314                      'auto' !== $script_module['fetchpriority'] ? sprintf( ' fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) ) : ''
 315                  );
 316              }
 317          }
 318      }
 319  
 320      /**
 321       * Prints the import map using a script tag with a type="importmap" attribute.
 322       *
 323       * @since 6.5.0
 324       */
 325  	public function print_import_map() {
 326          $import_map = $this->get_import_map();
 327          if ( ! empty( $import_map['imports'] ) ) {
 328              wp_print_inline_script_tag(
 329                  wp_json_encode( $import_map, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
 330                  array(
 331                      'type' => 'importmap',
 332                      'id'   => 'wp-importmap',
 333                  )
 334              );
 335          }
 336      }
 337  
 338      /**
 339       * Returns the import map array.
 340       *
 341       * @since 6.5.0
 342       *
 343       * @return array Array with an `imports` key mapping to an array of script module identifiers and their respective
 344       *               URLs, including the version query.
 345       */
 346  	private function get_import_map(): array {
 347          $imports = array();
 348          foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ) ) as $id => $script_module ) {
 349              $imports[ $id ] = $this->get_src( $id );
 350          }
 351          return array( 'imports' => $imports );
 352      }
 353  
 354      /**
 355       * Retrieves the list of script modules marked for enqueue.
 356       *
 357       * @since 6.5.0
 358       *
 359       * @return array<string, array> Script modules marked for enqueue, keyed by script module identifier.
 360       */
 361  	private function get_marked_for_enqueue(): array {
 362          $enqueued = array();
 363          foreach ( $this->registered as $id => $script_module ) {
 364              if ( true === $script_module['enqueue'] ) {
 365                  $enqueued[ $id ] = $script_module;
 366              }
 367          }
 368          return $enqueued;
 369      }
 370  
 371      /**
 372       * Retrieves all the dependencies for the given script module identifiers,
 373       * filtered by import types.
 374       *
 375       * It will consolidate an array containing a set of unique dependencies based
 376       * on the requested import types: 'static', 'dynamic', or both. This method is
 377       * recursive and also retrieves dependencies of the dependencies.
 378       *
 379       * @since 6.5.0
 380       *
 381       * @param string[] $ids          The identifiers of the script modules for which to gather dependencies.
 382       * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
 383       *                               Default is both.
 384       * @return array[] List of dependencies, keyed by script module identifier.
 385       */
 386  	private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) {
 387          return array_reduce(
 388              $ids,
 389              function ( $dependency_script_modules, $id ) use ( $import_types ) {
 390                  $dependencies = array();
 391                  foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
 392                      if (
 393                      in_array( $dependency['import'], $import_types, true ) &&
 394                      isset( $this->registered[ $dependency['id'] ] ) &&
 395                      ! isset( $dependency_script_modules[ $dependency['id'] ] )
 396                      ) {
 397                          $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
 398                      }
 399                  }
 400                  return array_merge( $dependency_script_modules, $dependencies, $this->get_dependencies( array_keys( $dependencies ), $import_types ) );
 401              },
 402              array()
 403          );
 404      }
 405  
 406      /**
 407       * Gets the versioned URL for a script module src.
 408       *
 409       * If $version is set to false, the version number is the currently installed
 410       * WordPress version. If $version is set to null, no version is added.
 411       * Otherwise, the string passed in $version is used.
 412       *
 413       * @since 6.5.0
 414       *
 415       * @param string $id The script module identifier.
 416       * @return string The script module src with a version if relevant.
 417       */
 418  	private function get_src( string $id ): string {
 419          if ( ! isset( $this->registered[ $id ] ) ) {
 420              return '';
 421          }
 422  
 423          $script_module = $this->registered[ $id ];
 424          $src           = $script_module['src'];
 425  
 426          if ( false === $script_module['version'] ) {
 427              $src = add_query_arg( 'ver', get_bloginfo( 'version' ), $src );
 428          } elseif ( null !== $script_module['version'] ) {
 429              $src = add_query_arg( 'ver', $script_module['version'], $src );
 430          }
 431  
 432          /**
 433           * Filters the script module source.
 434           *
 435           * @since 6.5.0
 436           *
 437           * @param string $src Module source URL.
 438           * @param string $id  Module identifier.
 439           */
 440          $src = apply_filters( 'script_module_loader_src', $src, $id );
 441  
 442          return $src;
 443      }
 444  
 445      /**
 446       * Print data associated with Script Modules.
 447       *
 448       * The data will be embedded in the page HTML and can be read by Script Modules on page load.
 449       *
 450       * @since 6.7.0
 451       *
 452       * Data can be associated with a Script Module via the
 453       * {@see "script_module_data_{$module_id}"} filter.
 454       *
 455       * The data for a Script Module will be serialized as JSON in a script tag with an ID of the
 456       * form `wp-script-module-data-{$module_id}`.
 457       */
 458  	public function print_script_module_data(): void {
 459          $modules = array();
 460          foreach ( array_keys( $this->get_marked_for_enqueue() ) as $id ) {
 461              if ( '@wordpress/a11y' === $id ) {
 462                  $this->a11y_available = true;
 463              }
 464              $modules[ $id ] = true;
 465          }
 466          foreach ( array_keys( $this->get_import_map()['imports'] ) as $id ) {
 467              if ( '@wordpress/a11y' === $id ) {
 468                  $this->a11y_available = true;
 469              }
 470              $modules[ $id ] = true;
 471          }
 472  
 473          foreach ( array_keys( $modules ) as $module_id ) {
 474              /**
 475               * Filters data associated with a given Script Module.
 476               *
 477               * Script Modules may require data that is required for initialization or is essential
 478               * to have immediately available on page load. These are suitable use cases for
 479               * this data.
 480               *
 481               * The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID
 482               * that the data is associated with.
 483               *
 484               * This is best suited to pass essential data that must be available to the module for
 485               * initialization or immediately on page load. It does not replace the REST API or
 486               * fetching data from the client.
 487               *
 488               * Example:
 489               *
 490               *     add_filter(
 491               *         'script_module_data_MyScriptModuleID',
 492               *         function ( array $data ): array {
 493               *             $data['dataForClient'] = 'ok';
 494               *             return $data;
 495               *         }
 496               *     );
 497               *
 498               * If the filter returns no data (an empty array), nothing will be embedded in the page.
 499               *
 500               * The data for a given Script Module, if provided, will be JSON serialized in a script
 501               * tag with an ID of the form `wp-script-module-data-{$module_id}`.
 502               *
 503               * The data can be read on the client with a pattern like this:
 504               *
 505               * Example:
 506               *
 507               *     const dataContainer = document.getElementById( 'wp-script-module-data-MyScriptModuleID' );
 508               *     let data = {};
 509               *     if ( dataContainer ) {
 510               *         try {
 511               *             data = JSON.parse( dataContainer.textContent );
 512               *         } catch {}
 513               *     }
 514               *     // data.dataForClient === 'ok';
 515               *     initMyScriptModuleWithData( data );
 516               *
 517               * @since 6.7.0
 518               *
 519               * @param array $data The data associated with the Script Module.
 520               */
 521              $data = apply_filters( "script_module_data_{$module_id}", array() );
 522  
 523              if ( is_array( $data ) && array() !== $data ) {
 524                  /*
 525                   * This data will be printed as JSON inside a script tag like this:
 526                   *   <script type="application/json"></script>
 527                   *
 528                   * A script tag must be closed by a sequence beginning with `</`. It's impossible to
 529                   * close a script tag without using `<`. We ensure that `<` is escaped and `/` can
 530                   * remain unescaped, so `</script>` will be printed as `\u003C/script\u00E3`.
 531                   *
 532                   *   - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E.
 533                   *   - JSON_UNESCAPED_SLASHES: Don't escape /.
 534                   *
 535                   * If the page will use UTF-8 encoding, it's safe to print unescaped unicode:
 536                   *
 537                   *   - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`).
 538                   *   - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when
 539                   *     JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was
 540                   *     before PHP 7.1 without this constant. Available as of PHP 7.1.0.
 541                   *
 542                   * The JSON specification requires encoding in UTF-8, so if the generated HTML page
 543                   * is not encoded in UTF-8 then it's not safe to include those literals. They must
 544                   * be escaped to avoid encoding issues.
 545                   *
 546                   * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements.
 547                   * @see https://www.php.net/manual/en/json.constants.php for details on these constants.
 548                   * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing.
 549                   */
 550                  $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS;
 551                  if ( ! is_utf8_charset() ) {
 552                      $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
 553                  }
 554  
 555                  wp_print_inline_script_tag(
 556                      wp_json_encode(
 557                          $data,
 558                          $json_encode_flags
 559                      ),
 560                      array(
 561                          'type' => 'application/json',
 562                          'id'   => "wp-script-module-data-{$module_id}",
 563                      )
 564                  );
 565              }
 566          }
 567      }
 568  
 569      /**
 570       * @access private This is only intended to be called by the registered actions.
 571       *
 572       * @since 6.7.0
 573       */
 574  	public function print_a11y_script_module_html() {
 575          if ( ! $this->a11y_available ) {
 576              return;
 577          }
 578          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;">'
 579              . '<p id="a11y-speak-intro-text" class="a11y-speak-intro-text" hidden>' . esc_html__( 'Notifications' ) . '</p>'
 580              . '<div id="a11y-speak-assertive" class="a11y-speak-region" aria-live="assertive" aria-relevant="additions text" aria-atomic="true"></div>'
 581              . '<div id="a11y-speak-polite" class="a11y-speak-region" aria-live="polite" aria-relevant="additions text" aria-atomic="true"></div>'
 582              . '</div>';
 583      }
 584  }


Generated : Tue Sep 9 08:20:04 2025 Cross-referenced by PHPXref