wp/wp-includes/class-wp-script-modules.php
changeset 21 48c4eec2b7e6
child 22 8c2e4d02f4ef
equal deleted inserted replaced
20:7b1b88e27a20 21:48c4eec2b7e6
       
     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 
       
   187 	/**
       
   188 	 * Prints the enqueued script modules using script tags with type="module"
       
   189 	 * attributes.
       
   190 	 *
       
   191 	 * @since 6.5.0
       
   192 	 */
       
   193 	public function print_enqueued_script_modules() {
       
   194 		foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) {
       
   195 			wp_print_script_tag(
       
   196 				array(
       
   197 					'type' => 'module',
       
   198 					'src'  => $this->get_src( $id ),
       
   199 					'id'   => $id . '-js-module',
       
   200 				)
       
   201 			);
       
   202 		}
       
   203 	}
       
   204 
       
   205 	/**
       
   206 	 * Prints the the static dependencies of the enqueued script modules using
       
   207 	 * link tags with rel="modulepreload" attributes.
       
   208 	 *
       
   209 	 * If a script module is marked for enqueue, it will not be preloaded.
       
   210 	 *
       
   211 	 * @since 6.5.0
       
   212 	 */
       
   213 	public function print_script_module_preloads() {
       
   214 		foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) {
       
   215 			// Don't preload if it's marked for enqueue.
       
   216 			if ( true !== $script_module['enqueue'] ) {
       
   217 				echo sprintf(
       
   218 					'<link rel="modulepreload" href="%s" id="%s">',
       
   219 					esc_url( $this->get_src( $id ) ),
       
   220 					esc_attr( $id . '-js-modulepreload' )
       
   221 				);
       
   222 			}
       
   223 		}
       
   224 	}
       
   225 
       
   226 	/**
       
   227 	 * Prints the import map using a script tag with a type="importmap" attribute.
       
   228 	 *
       
   229 	 * @since 6.5.0
       
   230 	 *
       
   231 	 * @global WP_Scripts $wp_scripts The WP_Scripts object for printing the polyfill.
       
   232 	 */
       
   233 	public function print_import_map() {
       
   234 		$import_map = $this->get_import_map();
       
   235 		if ( ! empty( $import_map['imports'] ) ) {
       
   236 			global $wp_scripts;
       
   237 			if ( isset( $wp_scripts ) ) {
       
   238 				wp_print_inline_script_tag(
       
   239 					wp_get_script_polyfill(
       
   240 						$wp_scripts,
       
   241 						array(
       
   242 							'HTMLScriptElement.supports && HTMLScriptElement.supports("importmap")' => 'wp-polyfill-importmap',
       
   243 						)
       
   244 					),
       
   245 					array(
       
   246 						'id' => 'wp-load-polyfill-importmap',
       
   247 					)
       
   248 				);
       
   249 			}
       
   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 }