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


Generated : Sat Nov 23 08:20:01 2024 Cross-referenced by PHPXref