|
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 } |