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