| [ 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 * @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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Tue Jun 16 08:20:09 2026 | Cross-referenced by PHPXref |