[ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated : Tue Sep 9 08:20:04 2025 | Cross-referenced by PHPXref |