run:R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:52
R W Run
DIR
2026-03-11 16:18:51
R W Run
DIR
2026-03-11 16:18:51
R W Run
23.8 KB
2026-03-11 16:18:51
R W Run
7.8 KB
2026-03-11 16:18:52
R W Run
36.1 KB
2026-03-11 16:18:51
R W Run
11.9 KB
2026-03-11 16:18:52
R W Run
18.94 KB
2026-03-11 16:18:52
R W Run
7.35 KB
2026-03-11 16:18:52
R W Run
28.6 KB
2026-03-11 16:18:51
R W Run
316 By
2026-03-11 16:18:51
R W Run
12.9 KB
2026-03-11 16:18:51
R W Run
61.02 KB
2026-03-11 16:18:52
R W Run
15 KB
2026-03-11 16:18:51
R W Run
112.05 KB
2026-03-11 16:18:51
R W Run
12.47 KB
2026-03-11 16:18:51
R W Run
15.07 KB
2026-03-11 16:18:52
R W Run
9.84 KB
2026-03-11 16:18:52
R W Run
13.17 KB
2026-03-11 16:18:52
R W Run
33.83 KB
2026-03-11 16:18:51
R W Run
42.63 KB
2026-03-11 16:18:51
R W Run
55.71 KB
2026-03-11 16:18:52
R W Run
12.53 KB
2026-03-11 16:18:51
R W Run
2.55 KB
2026-03-11 16:18:52
R W Run
28.92 KB
2026-03-11 16:18:52
R W Run
539 By
2026-03-11 16:18:51
R W Run
367 By
2026-03-11 16:18:52
R W Run
42.65 KB
2026-03-11 16:18:51
R W Run
401 By
2026-03-11 16:18:51
R W Run
6.61 KB
2026-03-11 16:18:51
R W Run
664 By
2026-03-11 16:18:52
R W Run
20.63 KB
2026-03-11 16:18:51
R W Run
2.18 KB
2026-03-11 16:18:52
R W Run
453 By
2026-03-11 16:18:52
R W Run
457 By
2026-03-11 16:18:51
R W Run
36.83 KB
2026-03-11 16:18:52
R W Run
2.41 KB
2026-03-11 16:18:52
R W Run
8.28 KB
2026-03-11 16:18:51
R W Run
13.89 KB
2026-03-11 16:18:51
R W Run
11.76 KB
2026-03-11 16:18:51
R W Run
2.65 KB
2026-03-11 16:18:51
R W Run
7.43 KB
2026-03-11 16:18:51
R W Run
17.46 KB
2026-03-11 16:18:51
R W Run
5.14 KB
2026-03-11 16:18:52
R W Run
16.7 KB
2026-03-11 16:18:51
R W Run
8.28 KB
2026-03-11 16:18:52
R W Run
2.92 KB
2026-03-11 16:18:52
R W Run
1.32 KB
2026-03-11 16:18:51
R W Run
4.6 KB
2026-03-11 16:18:52
R W Run
11.62 KB
2026-03-11 16:18:52
R W Run
2.5 KB
2026-03-11 16:18:51
R W Run
1.97 KB
2026-03-11 16:18:51
R W Run
11.25 KB
2026-03-11 16:18:52
R W Run
5.32 KB
2026-03-11 16:18:51
R W Run
10.99 KB
2026-03-11 16:18:52
R W Run
68.32 KB
2026-03-11 16:18:51
R W Run
6.34 KB
2026-03-11 16:18:51
R W Run
5.49 KB
2026-03-11 16:18:51
R W Run
1.99 KB
2026-03-11 16:18:52
R W Run
7.02 KB
2026-03-11 16:18:51
R W Run
4.91 KB
2026-03-11 16:18:52
R W Run
16.86 KB
2026-03-11 16:18:51
R W Run
24.23 KB
2026-03-11 16:18:51
R W Run
3.97 KB
2026-03-11 16:18:51
R W Run
47.66 KB
2026-03-11 16:18:51
R W Run
9.22 KB
2026-03-11 16:18:51
R W Run
25.51 KB
2026-03-11 16:18:51
R W Run
198.38 KB
2026-03-11 16:18:52
R W Run
56.65 KB
2026-03-11 16:18:51
R W Run
10.46 KB
2026-03-11 16:18:51
R W Run
10.95 KB
2026-03-11 16:18:52
R W Run
29.26 KB
2026-03-11 16:18:51
R W Run
70.91 KB
2026-03-11 16:18:52
R W Run
35.3 KB
2026-03-11 16:18:52
R W Run
16.61 KB
2026-03-11 16:18:52
R W Run
2.57 KB
2026-03-11 16:18:52
R W Run
39.83 KB
2026-03-11 16:18:51
R W Run
70.64 KB
2026-03-11 16:18:51
R W Run
15.56 KB
2026-03-11 16:18:52
R W Run
7.33 KB
2026-03-11 16:18:52
R W Run
253 By
2026-03-11 16:18:51
R W Run
7.96 KB
2026-03-11 16:18:52
R W Run
3.23 KB
2026-03-11 16:18:52
R W Run
969 By
2026-03-11 16:18:52
R W Run
16.28 KB
2026-03-11 16:18:51
R W Run
7.22 KB
2026-03-11 16:18:51
R W Run
12.95 KB
2026-03-11 16:18:51
R W Run
6.53 KB
2026-03-11 16:18:51
R W Run
3.42 KB
2026-03-11 16:18:52
R W Run
5.84 KB
2026-03-11 16:18:51
R W Run
1.97 KB
2026-03-11 16:18:51
R W Run
4.3 KB
2026-03-11 16:18:52
R W Run
2.91 KB
2026-03-11 16:18:51
R W Run
16.46 KB
2026-03-11 16:18:52
R W Run
40.6 KB
2026-03-11 16:18:51
R W Run
20.22 KB
2026-03-11 16:18:51
R W Run
36.11 KB
2026-03-11 16:18:52
R W Run
17.01 KB
2026-03-11 16:18:51
R W Run
7.27 KB
2026-03-11 16:18:52
R W Run
6.62 KB
2026-03-11 16:18:52
R W Run
16.49 KB
2026-03-11 16:18:52
R W Run
1.79 KB
2026-03-11 16:18:52
R W Run
29.82 KB
2026-03-11 16:18:51
R W Run
6.67 KB
2026-03-11 16:18:52
R W Run
8.98 KB
2026-03-11 16:18:52
R W Run
19.42 KB
2026-03-11 16:18:51
R W Run
12.01 KB
2026-03-11 16:18:51
R W Run
17.11 KB
2026-03-11 16:18:51
R W Run
6.74 KB
2026-03-11 16:18:52
R W Run
30.93 KB
2026-03-11 16:18:51
R W Run
4.99 KB
2026-03-11 16:18:51
R W Run
4.25 KB
2026-03-11 16:18:51
R W Run
24.72 KB
2026-03-11 16:18:51
R W Run
29.96 KB
2026-03-11 16:18:52
R W Run
6.41 KB
2026-03-11 16:18:51
R W Run
160 KB
2026-03-11 16:18:51
R W Run
6.72 KB
2026-03-11 16:18:52
R W Run
10.92 KB
2026-03-11 16:18:51
R W Run
4.77 KB
2026-03-11 16:18:51
R W Run
3.38 KB
2026-03-11 16:18:51
R W Run
11.18 KB
2026-03-11 16:18:51
R W Run
62.19 KB
2026-03-11 16:18:51
R W Run
2.46 KB
2026-03-11 16:18:51
R W Run
9.17 KB
2026-03-11 16:18:51
R W Run
32.15 KB
2026-03-11 16:18:51
R W Run
34.05 KB
2026-03-11 16:18:52
R W Run
7.15 KB
2026-03-11 16:18:51
R W Run
3.47 KB
2026-03-11 16:18:52
R W Run
1.87 KB
2026-03-11 16:18:52
R W Run
30.91 KB
2026-03-11 16:18:51
R W Run
7.29 KB
2026-03-11 16:18:52
R W Run
7.35 KB
2026-03-11 16:18:51
R W Run
12.54 KB
2026-03-11 16:18:51
R W Run
19.12 KB
2026-03-11 16:18:51
R W Run
18.12 KB
2026-03-11 16:18:52
R W Run
39.99 KB
2026-03-11 16:18:52
R W Run
5.17 KB
2026-03-11 16:18:52
R W Run
979 By
2026-03-11 16:18:51
R W Run
18.44 KB
2026-03-11 16:18:52
R W Run
10.24 KB
2026-03-11 16:18:51
R W Run
1.77 KB
2026-03-11 16:18:52
R W Run
34.9 KB
2026-03-11 16:18:51
R W Run
7.19 KB
2026-03-11 16:18:52
R W Run
160.5 KB
2026-03-11 16:18:51
R W Run
64.27 KB
2026-03-11 16:18:51
R W Run
27.95 KB
2026-03-11 16:18:51
R W Run
4.69 KB
2026-03-11 16:18:51
R W Run
2.94 KB
2026-03-11 16:18:51
R W Run
43.13 KB
2026-03-11 16:18:52
R W Run
2.25 KB
2026-03-11 16:18:52
R W Run
22.5 KB
2026-03-11 16:18:51
R W Run
13.01 KB
2026-03-11 16:18:52
R W Run
3.27 KB
2026-03-11 16:18:51
R W Run
18 KB
2026-03-11 16:18:51
R W Run
210.4 KB
2026-03-11 16:18:52
R W Run
25.86 KB
2026-03-11 16:18:52
R W Run
115.85 KB
2026-03-11 16:18:51
R W Run
373 By
2026-03-11 16:18:52
R W Run
343 By
2026-03-11 16:18:52
R W Run
338 By
2026-03-11 16:18:51
R W Run
100.73 KB
2026-03-11 16:18:52
R W Run
130.93 KB
2026-03-11 16:18:51
R W Run
19.1 KB
2026-03-11 16:18:51
R W Run
17.41 KB
2026-03-11 16:18:52
R W Run
41.98 KB
2026-03-11 16:18:52
R W Run
400 By
2026-03-11 16:18:52
R W Run
11.1 KB
2026-03-11 16:18:52
R W Run
37.02 KB
2026-03-11 16:18:51
R W Run
2.24 KB
2026-03-11 16:18:51
R W Run
188.13 KB
2026-03-11 16:18:51
R W Run
338 By
2026-03-11 16:18:51
R W Run
38 KB
2026-03-11 16:18:51
R W Run
4.02 KB
2026-03-11 16:18:52
R W Run
5.38 KB
2026-03-11 16:18:51
R W Run
3.05 KB
2026-03-11 16:18:52
R W Run
2.61 KB
2026-03-11 16:18:51
R W Run
1.16 KB
2026-03-11 16:18:52
R W Run
4.04 KB
2026-03-11 16:18:51
R W Run
3.71 KB
2026-03-11 16:18:51
R W Run
24.6 KB
2026-03-11 16:18:51
R W Run
9.56 KB
2026-03-11 16:18:51
R W Run
346.43 KB
2026-03-11 16:18:52
R W Run
281.84 KB
2026-03-11 16:18:52
R W Run
14.95 KB
2026-03-11 16:18:51
R W Run
8.44 KB
2026-03-11 16:18:52
R W Run
168.95 KB
2026-03-11 16:18:52
R W Run
20.71 KB
2026-03-11 16:18:52
R W Run
25.27 KB
2026-03-11 16:18:51
R W Run
5.72 KB
2026-03-11 16:18:51
R W Run
4.63 KB
2026-03-11 16:18:52
R W Run
81.73 KB
2026-03-11 16:18:51
R W Run
67.18 KB
2026-03-11 16:18:51
R W Run
156.36 KB
2026-03-11 16:18:52
R W Run
55.19 KB
2026-03-11 16:18:51
R W Run
162 By
2026-03-11 16:18:51
R W Run
61.72 KB
2026-03-11 16:18:51
R W Run
216.06 KB
2026-03-11 16:18:52
R W Run
65.09 KB
2026-03-11 16:18:51
R W Run
25.24 KB
2026-03-11 16:18:52
R W Run
4.81 KB
2026-03-11 16:18:51
R W Run
6.48 KB
2026-03-11 16:18:52
R W Run
21.25 KB
2026-03-11 16:18:51
R W Run
2.79 KB
2026-03-11 16:18:52
R W Run
89.69 KB
2026-03-11 16:18:52
R W Run
19.42 KB
2026-03-11 16:18:52
R W Run
3.69 KB
2026-03-11 16:18:52
R W Run
4.11 KB
2026-03-11 16:18:51
R W Run
40.74 KB
2026-03-11 16:18:51
R W Run
25.38 KB
2026-03-11 16:18:51
R W Run
43.31 KB
2026-03-11 16:18:52
R W Run
102.57 KB
2026-03-11 16:18:52
R W Run
6.18 KB
2026-03-11 16:18:51
R W Run
124.47 KB
2026-03-11 16:18:52
R W Run
35.65 KB
2026-03-11 16:18:52
R W Run
6.94 KB
2026-03-11 16:18:52
R W Run
67.04 KB
2026-03-11 16:18:52
R W Run
10.62 KB
2026-03-11 16:18:51
R W Run
289.35 KB
2026-03-11 16:18:52
R W Run
36.23 KB
2026-03-11 16:18:51
R W Run
200 By
2026-03-11 16:18:52
R W Run
200 By
2026-03-11 16:18:52
R W Run
98.29 KB
2026-03-11 16:18:52
R W Run
30.02 KB
2026-03-11 16:18:52
R W Run
19.03 KB
2026-03-11 16:18:52
R W Run
5.06 KB
2026-03-11 16:18:52
R W Run
255 By
2026-03-11 16:18:51
R W Run
22.66 KB
2026-03-11 16:18:52
R W Run
154.63 KB
2026-03-11 16:18:51
R W Run
9.68 KB
2026-03-11 16:18:51
R W Run
258 By
2026-03-11 16:18:51
R W Run
23.49 KB
2026-03-11 16:18:51
R W Run
3.16 KB
2026-03-11 16:18:51
R W Run
8.4 KB
2026-03-11 16:18:52
R W Run
441 By
2026-03-11 16:18:51
R W Run
7.39 KB
2026-03-11 16:18:51
R W Run
173 KB
2026-03-11 16:18:52
R W Run
544 By
2026-03-11 16:18:52
R W Run
4.17 KB
2026-03-11 16:18:51
R W Run
35.97 KB
2026-03-11 16:18:52
R W Run
1.69 KB
2026-03-11 16:18:51
R W Run
2.84 KB
2026-03-11 16:18:52
R W Run
6.09 KB
2026-03-11 16:18:51
R W Run
8.71 KB
2026-03-11 16:18:51
R W Run
131.84 KB
2026-03-11 16:18:51
R W Run
37.45 KB
2026-03-11 16:18:51
R W Run
173.89 KB
2026-03-11 16:18:51
R W Run
7.09 KB
2026-03-11 16:18:51
R W Run
6.41 KB
2026-03-11 16:18:51
R W Run
1.08 KB
2026-03-11 16:18:51
R W Run
69.46 KB
2026-03-11 16:18:52
R W Run
445 By
2026-03-11 16:18:51
R W Run
799 By
2026-03-11 16:18:52
R W Run
error_log
📄class-wp-script-modules.php
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 */
16class WP_Script_Modules {
17 /**
18 * Holds the registered script modules, keyed by script module identifier.
19 *
20 * @since 6.5.0
21 * @var array<string, array<string, mixed>>
22 */
23 private $registered = array();
24
25 /**
26 * An array of IDs for queued script modules.
27 *
28 * @since 6.9.0
29 * @var string[]
30 */
31 private $queue = array();
32
33 /**
34 * Holds the script module identifiers that have been printed.
35 *
36 * @since 6.9.0
37 * @var string[]
38 */
39 private $done = array();
40
41 /**
42 * Tracks whether the @wordpress/a11y script module is available.
43 *
44 * Some additional HTML is required on the page for the module to work. Track
45 * whether it's available to print at the appropriate time.
46 *
47 * @since 6.7.0
48 * @var bool
49 */
50 private $a11y_available = false;
51
52 /**
53 * Holds a mapping of dependents (as IDs) for a given script ID.
54 * Used to optimize recursive dependency tree checks.
55 *
56 * @since 6.9.0
57 * @var array<string, string[]>
58 */
59 private $dependents_map = array();
60
61 /**
62 * Holds the valid values for fetchpriority.
63 *
64 * @since 6.9.0
65 * @var string[]
66 */
67 private $priorities = array(
68 'low',
69 'auto',
70 'high',
71 );
72
73 /**
74 * List of IDs for script modules encountered which have missing dependencies.
75 *
76 * An ID is added to this list when it is discovered to have missing dependencies. At this time, a warning is
77 * emitted with {@see _doing_it_wrong()}. The ID is then added to this list, so that duplicate warnings don't occur.
78 *
79 * @since 6.9.1
80 * @var string[]
81 */
82 private $modules_with_missing_dependencies = array();
83
84 /**
85 * Registers the script module if no script module with that script module
86 * identifier has already been registered.
87 *
88 * @since 6.5.0
89 * @since 6.9.0 Added the $args parameter.
90 *
91 * @param string $id The identifier of the script module. Should be unique. It will be used in the
92 * final import map.
93 * @param string $src Optional. Full URL of the script module, or path of the script module relative
94 * to the WordPress root directory. If it is provided and the script module has
95 * not been registered yet, it will be registered.
96 * @param array $deps {
97 * Optional. List of dependencies.
98 *
99 * @type string|array ...$0 {
100 * An array of script module identifiers of the dependencies of this script
101 * module. The dependencies can be strings or arrays. If they are arrays,
102 * they need an `id` key with the script module identifier, and can contain
103 * an `import` key with either `static` or `dynamic`. By default,
104 * dependencies that don't contain an `import` key are considered static.
105 *
106 * @type string $id The script module identifier.
107 * @type string $import Optional. Import type. May be either `static` or
108 * `dynamic`. Defaults to `static`.
109 * }
110 * }
111 * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
112 * It is added to the URL as a query string for cache busting purposes. If $version
113 * is set to false, the version number is the currently installed WordPress version.
114 * If $version is set to null, no version is added.
115 * @param array $args {
116 * Optional. An array of additional args. Default empty array.
117 *
118 * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
119 * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
120 * }
121 */
122 public function register( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) {
123 if ( '' === $id ) {
124 _doing_it_wrong( __METHOD__, __( 'Non-empty string required for id.' ), '6.9.0' );
125 return;
126 }
127
128 if ( ! isset( $this->registered[ $id ] ) ) {
129 $dependencies = array();
130 foreach ( $deps as $dependency ) {
131 if ( is_array( $dependency ) ) {
132 if ( ! isset( $dependency['id'] ) || ! is_string( $dependency['id'] ) ) {
133 _doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' );
134 continue;
135 }
136 $dependencies[] = array(
137 'id' => $dependency['id'],
138 'import' => isset( $dependency['import'] ) && 'dynamic' === $dependency['import'] ? 'dynamic' : 'static',
139 );
140 } elseif ( is_string( $dependency ) ) {
141 $dependencies[] = array(
142 'id' => $dependency,
143 'import' => 'static',
144 );
145 } else {
146 _doing_it_wrong( __METHOD__, __( 'Entries in dependencies array must be either strings or arrays with an id key.' ), '6.5.0' );
147 }
148 }
149
150 $in_footer = isset( $args['in_footer'] ) && (bool) $args['in_footer'];
151
152 $fetchpriority = 'auto';
153 if ( isset( $args['fetchpriority'] ) ) {
154 if ( $this->is_valid_fetchpriority( $args['fetchpriority'] ) ) {
155 $fetchpriority = $args['fetchpriority'];
156 } else {
157 _doing_it_wrong(
158 __METHOD__,
159 sprintf(
160 /* translators: 1: $fetchpriority, 2: $id */
161 __( 'Invalid fetchpriority `%1$s` defined for `%2$s` during script registration.' ),
162 is_string( $args['fetchpriority'] ) ? $args['fetchpriority'] : gettype( $args['fetchpriority'] ),
163 $id
164 ),
165 '6.9.0'
166 );
167 }
168 }
169
170 $this->registered[ $id ] = array(
171 'src' => $src,
172 'version' => $version,
173 'dependencies' => $dependencies,
174 'in_footer' => $in_footer,
175 'fetchpriority' => $fetchpriority,
176 );
177 }
178 }
179
180 /**
181 * Gets IDs for queued script modules.
182 *
183 * @since 6.9.0
184 *
185 * @return string[] Script module IDs.
186 */
187 public function get_queue(): array {
188 return $this->queue;
189 }
190
191 /**
192 * Checks if the provided fetchpriority is valid.
193 *
194 * @since 6.9.0
195 *
196 * @param string|mixed $priority Fetch priority.
197 * @return bool Whether valid fetchpriority.
198 */
199 private function is_valid_fetchpriority( $priority ): bool {
200 return in_array( $priority, $this->priorities, true );
201 }
202
203 /**
204 * Sets the fetch priority for a script module.
205 *
206 * @since 6.9.0
207 *
208 * @param string $id Script module identifier.
209 * @param 'auto'|'low'|'high' $priority Fetch priority for the script module.
210 * @return bool Whether setting the fetchpriority was successful.
211 */
212 public function set_fetchpriority( string $id, string $priority ): bool {
213 if ( ! isset( $this->registered[ $id ] ) ) {
214 return false;
215 }
216
217 if ( '' === $priority ) {
218 $priority = 'auto';
219 }
220
221 if ( ! $this->is_valid_fetchpriority( $priority ) ) {
222 _doing_it_wrong(
223 __METHOD__,
224 /* translators: %s: Invalid fetchpriority. */
225 sprintf( __( 'Invalid fetchpriority: %s' ), $priority ),
226 '6.9.0'
227 );
228 return false;
229 }
230
231 $this->registered[ $id ]['fetchpriority'] = $priority;
232 return true;
233 }
234
235 /**
236 * Sets whether a script module should be printed in the footer.
237 *
238 * This is only relevant in block themes.
239 *
240 * @since 6.9.0
241 *
242 * @param string $id Script module identifier.
243 * @param bool $in_footer Whether to print in the footer.
244 * @return bool Whether setting the printing location was successful.
245 */
246 public function set_in_footer( string $id, bool $in_footer ): bool {
247 if ( ! isset( $this->registered[ $id ] ) ) {
248 return false;
249 }
250 $this->registered[ $id ]['in_footer'] = $in_footer;
251 return true;
252 }
253
254 /**
255 * Marks the script module to be enqueued in the page.
256 *
257 * If a src is provided and the script module has not been registered yet, it
258 * will be registered.
259 *
260 * @since 6.5.0
261 * @since 6.9.0 Added the $args parameter.
262 *
263 * @param string $id The identifier of the script module. Should be unique. It will be used in the
264 * final import map.
265 * @param string $src Optional. Full URL of the script module, or path of the script module relative
266 * to the WordPress root directory. If it is provided and the script module has
267 * not been registered yet, it will be registered.
268 * @param array $deps {
269 * Optional. List of dependencies.
270 *
271 * @type string|array ...$0 {
272 * An array of script module identifiers of the dependencies of this script
273 * module. The dependencies can be strings or arrays. If they are arrays,
274 * they need an `id` key with the script module identifier, and can contain
275 * an `import` key with either `static` or `dynamic`. By default,
276 * dependencies that don't contain an `import` key are considered static.
277 *
278 * @type string $id The script module identifier.
279 * @type string $import Optional. Import type. May be either `static` or
280 * `dynamic`. Defaults to `static`.
281 * }
282 * }
283 * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
284 * It is added to the URL as a query string for cache busting purposes. If $version
285 * is set to false, the version number is the currently installed WordPress version.
286 * If $version is set to null, no version is added.
287 * @param array $args {
288 * Optional. An array of additional args. Default empty array.
289 *
290 * @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
291 * @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
292 * }
293 */
294 public function enqueue( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) {
295 if ( '' === $id ) {
296 _doing_it_wrong( __METHOD__, __( 'Non-empty string required for id.' ), '6.9.0' );
297 return;
298 }
299
300 if ( ! in_array( $id, $this->queue, true ) ) {
301 $this->queue[] = $id;
302 }
303 if ( ! isset( $this->registered[ $id ] ) && $src ) {
304 $this->register( $id, $src, $deps, $version, $args );
305 }
306 }
307
308 /**
309 * Unmarks the script module so it will no longer be enqueued in the page.
310 *
311 * @since 6.5.0
312 *
313 * @param string $id The identifier of the script module.
314 */
315 public function dequeue( string $id ) {
316 $this->queue = array_values( array_diff( $this->queue, array( $id ) ) );
317 }
318
319 /**
320 * Removes a registered script module.
321 *
322 * @since 6.5.0
323 *
324 * @param string $id The identifier of the script module.
325 */
326 public function deregister( string $id ) {
327 $this->dequeue( $id );
328 unset( $this->registered[ $id ] );
329 }
330
331 /**
332 * Adds the hooks to print the import map, enqueued script modules and script
333 * module preloads.
334 *
335 * In classic themes, the script modules used by the blocks are not yet known
336 * when the `wp_head` actions is fired, so it needs to print everything in the
337 * footer.
338 *
339 * @since 6.5.0
340 */
341 public function add_hooks() {
342 $is_block_theme = wp_is_block_theme();
343 $position = $is_block_theme ? 'wp_head' : 'wp_footer';
344 add_action( $position, array( $this, 'print_import_map' ) );
345 if ( $is_block_theme ) {
346 /*
347 * Modules can only be printed in the head for block themes because only with
348 * block themes will import map be fully populated by modules discovered by
349 * rendering the block template. In classic themes, modules are enqueued during
350 * template rendering, thus the import map must be printed in the footer,
351 * followed by all enqueued modules.
352 */
353 add_action( 'wp_head', array( $this, 'print_head_enqueued_script_modules' ) );
354 }
355 add_action( 'wp_footer', array( $this, 'print_enqueued_script_modules' ) );
356 add_action( $position, array( $this, 'print_script_module_preloads' ) );
357
358 add_action( 'admin_print_footer_scripts', array( $this, 'print_import_map' ) );
359 add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) );
360 add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_preloads' ) );
361
362 add_action( 'wp_footer', array( $this, 'print_script_module_data' ) );
363 add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_data' ) );
364 add_action( 'wp_footer', array( $this, 'print_a11y_script_module_html' ), 20 );
365 add_action( 'admin_print_footer_scripts', array( $this, 'print_a11y_script_module_html' ), 20 );
366 }
367
368 /**
369 * Gets the highest fetch priority for the provided script IDs.
370 *
371 * @since 6.9.0
372 *
373 * @param string[] $ids Script module IDs.
374 * @return 'auto'|'low'|'high' Highest fetch priority for the provided script module IDs.
375 */
376 private function get_highest_fetchpriority( array $ids ): string {
377 static $high_priority_index = null;
378 if ( null === $high_priority_index ) {
379 $high_priority_index = count( $this->priorities ) - 1;
380 }
381
382 $highest_priority_index = 0;
383 foreach ( $ids as $id ) {
384 if ( isset( $this->registered[ $id ] ) ) {
385 $highest_priority_index = (int) max(
386 $highest_priority_index,
387 (int) array_search( $this->registered[ $id ]['fetchpriority'], $this->priorities, true )
388 );
389 if ( $high_priority_index === $highest_priority_index ) {
390 break;
391 }
392 }
393 }
394
395 return $this->priorities[ $highest_priority_index ];
396 }
397
398 /**
399 * Prints the enqueued script modules in head.
400 *
401 * This is only used in block themes.
402 *
403 * @since 6.9.0
404 */
405 public function print_head_enqueued_script_modules() {
406 foreach ( $this->get_sorted_dependencies( $this->queue ) as $id ) {
407 if (
408 isset( $this->registered[ $id ] ) &&
409 ! $this->registered[ $id ]['in_footer']
410 ) {
411 // If any dependency is set to be printed in footer, skip printing this module in head.
412 $dependencies = array_keys( $this->get_dependencies( array( $id ) ) );
413 foreach ( $dependencies as $dependency_id ) {
414 if (
415 in_array( $dependency_id, $this->queue, true ) &&
416 isset( $this->registered[ $dependency_id ] ) &&
417 $this->registered[ $dependency_id ]['in_footer']
418 ) {
419 continue 2;
420 }
421 }
422 $this->print_script_module( $id );
423 }
424 }
425 }
426
427 /**
428 * Prints the enqueued script modules in footer.
429 *
430 * @since 6.5.0
431 */
432 public function print_enqueued_script_modules() {
433 foreach ( $this->get_sorted_dependencies( $this->queue ) as $id ) {
434 $this->print_script_module( $id );
435 }
436 }
437
438 /**
439 * Prints the enqueued script module using script tags with type="module"
440 * attributes.
441 *
442 * @since 6.9.0
443 *
444 * @param string $id The script module identifier.
445 */
446 private function print_script_module( string $id ) {
447 if ( in_array( $id, $this->done, true ) || ! in_array( $id, $this->queue, true ) ) {
448 return;
449 }
450
451 $this->done[] = $id;
452
453 $src = $this->get_src( $id );
454 if ( '' === $src ) {
455 return;
456 }
457
458 $attributes = array(
459 'type' => 'module',
460 'src' => $src,
461 'id' => $id . '-js-module',
462 );
463
464 $script_module = $this->registered[ $id ];
465 $queued_dependents = array_intersect( $this->queue, $this->get_recursive_dependents( $id ) );
466 $fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $queued_dependents ) );
467 if ( 'auto' !== $fetchpriority ) {
468 $attributes['fetchpriority'] = $fetchpriority;
469 }
470 if ( $fetchpriority !== $script_module['fetchpriority'] ) {
471 $attributes['data-wp-fetchpriority'] = $script_module['fetchpriority'];
472 }
473 wp_print_script_tag( $attributes );
474 }
475
476 /**
477 * Prints the static dependencies of the enqueued script modules using
478 * link tags with rel="modulepreload" attributes.
479 *
480 * If a script module is marked for enqueue, it will not be preloaded.
481 *
482 * @since 6.5.0
483 */
484 public function print_script_module_preloads() {
485 $dependency_ids = $this->get_sorted_dependencies( $this->queue, array( 'static' ) );
486 foreach ( $dependency_ids as $id ) {
487 // Don't preload if it's marked for enqueue.
488 if ( in_array( $id, $this->queue, true ) ) {
489 continue;
490 }
491
492 $src = $this->get_src( $id );
493 if ( '' === $src ) {
494 continue;
495 }
496
497 $enqueued_dependents = array_intersect( $this->get_recursive_dependents( $id ), $this->queue );
498 $highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents );
499 printf(
500 '<link rel="modulepreload" href="%s" id="%s"',
501 esc_url( $src ),
502 esc_attr( $id . '-js-modulepreload' )
503 );
504 if ( 'auto' !== $highest_fetchpriority ) {
505 printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) );
506 }
507 if ( $highest_fetchpriority !== $this->registered[ $id ]['fetchpriority'] && 'auto' !== $this->registered[ $id ]['fetchpriority'] ) {
508 printf( ' data-wp-fetchpriority="%s"', esc_attr( $this->registered[ $id ]['fetchpriority'] ) );
509 }
510 echo ">\n";
511 }
512 }
513
514 /**
515 * Prints the import map using a script tag with a type="importmap" attribute.
516 *
517 * @since 6.5.0
518 */
519 public function print_import_map() {
520 $import_map = $this->get_import_map();
521 if ( ! empty( $import_map['imports'] ) ) {
522 wp_print_inline_script_tag(
523 (string) wp_json_encode( $import_map, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
524 array(
525 'type' => 'importmap',
526 'id' => 'wp-importmap',
527 )
528 );
529 }
530 }
531
532 /**
533 * Returns the import map array.
534 *
535 * @since 6.5.0
536 *
537 * @return array Array with an `imports` key mapping to an array of script module identifiers and their respective
538 * URLs, including the version query.
539 */
540 private function get_import_map(): array {
541 $imports = array();
542 foreach ( array_keys( $this->get_dependencies( $this->queue ) ) as $id ) {
543 $src = $this->get_src( $id );
544 if ( '' !== $src ) {
545 $imports[ $id ] = $src;
546 }
547 }
548 return array( 'imports' => $imports );
549 }
550
551 /**
552 * Retrieves the list of script modules marked for enqueue.
553 *
554 * Even though this is a private method and is unused in core, there are ecosystem plugins accessing it via the
555 * Reflection API. The ecosystem should rather use {@see self::get_queue()}.
556 *
557 * @since 6.5.0
558 *
559 * @return array<string, array> Script modules marked for enqueue, keyed by script module identifier.
560 */
561 private function get_marked_for_enqueue(): array {
562 return wp_array_slice_assoc(
563 $this->registered,
564 $this->queue
565 );
566 }
567
568 /**
569 * Retrieves all the dependencies for the given script module identifiers, filtered by import types.
570 *
571 * It will consolidate an array containing a set of unique dependencies based
572 * on the requested import types: 'static', 'dynamic', or both. This method is
573 * recursive and also retrieves dependencies of the dependencies.
574 *
575 * @since 6.5.0
576 *
577 * @param string[] $ids The identifiers of the script modules for which to gather dependencies.
578 * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
579 * Default is both.
580 * @return array<string, array> List of dependencies, keyed by script module identifier.
581 */
582 private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
583 $all_dependencies = array();
584 $id_queue = $ids;
585
586 while ( ! empty( $id_queue ) ) {
587 $id = array_shift( $id_queue );
588 if ( ! isset( $this->registered[ $id ] ) ) {
589 continue;
590 }
591
592 foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
593 if (
594 ! isset( $all_dependencies[ $dependency['id'] ] ) &&
595 in_array( $dependency['import'], $import_types, true ) &&
596 isset( $this->registered[ $dependency['id'] ] )
597 ) {
598 $all_dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
599
600 // Add this dependency to the list to get dependencies for.
601 $id_queue[] = $dependency['id'];
602 }
603 }
604 }
605
606 return $all_dependencies;
607 }
608
609 /**
610 * Gets all dependents of a script module.
611 *
612 * This is not recursive.
613 *
614 * @since 6.9.0
615 *
616 * @see WP_Scripts::get_dependents()
617 *
618 * @param string $id The script ID.
619 * @return string[] Script module IDs.
620 */
621 private function get_dependents( string $id ): array {
622 // Check if dependents map for the handle in question is present. If so, use it.
623 if ( isset( $this->dependents_map[ $id ] ) ) {
624 return $this->dependents_map[ $id ];
625 }
626
627 $dependents = array();
628
629 // Iterate over all registered scripts, finding dependents of the script passed to this method.
630 foreach ( $this->registered as $registered_id => $args ) {
631 if ( in_array( $id, wp_list_pluck( $args['dependencies'], 'id' ), true ) ) {
632 $dependents[] = $registered_id;
633 }
634 }
635
636 // Add the module's dependents to the map to ease future lookups.
637 $this->dependents_map[ $id ] = $dependents;
638
639 return $dependents;
640 }
641
642 /**
643 * Gets all recursive dependents of a script module.
644 *
645 * @since 6.9.0
646 *
647 * @see WP_Scripts::get_dependents()
648 *
649 * @param string $id The script ID.
650 * @return string[] Script module IDs.
651 */
652 private function get_recursive_dependents( string $id ): array {
653 $dependents = array();
654 $id_queue = array( $id );
655 $processed = array();
656
657 while ( ! empty( $id_queue ) ) {
658 $current_id = array_shift( $id_queue );
659
660 // Skip unregistered or already-processed script modules.
661 if ( ! isset( $this->registered[ $current_id ] ) || isset( $processed[ $current_id ] ) ) {
662 continue;
663 }
664
665 // Mark as processed to guard against infinite loops from circular dependencies.
666 $processed[ $current_id ] = true;
667
668 // Find the direct dependents of the current script.
669 foreach ( $this->get_dependents( $current_id ) as $dependent_id ) {
670 // Only add the dependent if we haven't found it before.
671 if ( ! isset( $dependents[ $dependent_id ] ) ) {
672 $dependents[ $dependent_id ] = true;
673
674 // Add dependency to the queue.
675 $id_queue[] = $dependent_id;
676 }
677 }
678 }
679
680 return array_keys( $dependents );
681 }
682
683 /**
684 * Sorts the given script module identifiers based on their dependencies.
685 *
686 * It will return a list of script module identifiers sorted in the order
687 * they should be printed, so that dependencies are printed before the script
688 * modules that depend on them.
689 *
690 * @since 6.9.0
691 *
692 * @param string[] $ids The identifiers of the script modules to sort.
693 * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
694 * Default is both.
695 * @return string[] Sorted list of script module identifiers.
696 */
697 private function get_sorted_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
698 $sorted = array();
699
700 foreach ( $ids as $id ) {
701 $this->sort_item_dependencies( $id, $import_types, $sorted );
702 }
703
704 return array_unique( $sorted );
705 }
706
707 /**
708 * Recursively sorts the dependencies for a single script module identifier.
709 *
710 * @since 6.9.0
711 *
712 * @param string $id The identifier of the script module to sort.
713 * @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
714 * @param string[] &$sorted The array of sorted identifiers, passed by reference.
715 * @return bool True on success, false on failure (e.g., missing dependency).
716 */
717 private function sort_item_dependencies( string $id, array $import_types, array &$sorted ): bool {
718 // If already processed, don't do it again.
719 if ( in_array( $id, $sorted, true ) ) {
720 return true;
721 }
722
723 // If the item doesn't exist, fail.
724 if ( ! isset( $this->registered[ $id ] ) ) {
725 return false;
726 }
727
728 $dependency_ids = array();
729 foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
730 if ( in_array( $dependency['import'], $import_types, true ) ) {
731 $dependency_ids[] = $dependency['id'];
732 }
733 }
734
735 // If the item requires dependencies that do not exist, fail.
736 $missing_dependencies = array_diff( $dependency_ids, array_keys( $this->registered ) );
737 if ( count( $missing_dependencies ) > 0 ) {
738 if ( ! in_array( $id, $this->modules_with_missing_dependencies, true ) ) {
739 _doing_it_wrong(
740 get_class( $this ) . '::register',
741 sprintf(
742 /* translators: 1: Script module ID, 2: List of missing dependency IDs. */
743 __( 'The script module with the ID "%1$s" was enqueued with dependencies that are not registered: %2$s.' ),
744 $id,
745 implode( wp_get_list_item_separator(), $missing_dependencies )
746 ),
747 '6.9.1'
748 );
749 $this->modules_with_missing_dependencies[] = $id;
750 }
751
752 return false;
753 }
754
755 // Recursively process dependencies.
756 foreach ( $dependency_ids as $dependency_id ) {
757 if ( ! $this->sort_item_dependencies( $dependency_id, $import_types, $sorted ) ) {
758 // A dependency failed to resolve, so this branch fails.
759 return false;
760 }
761 }
762
763 // All dependencies are sorted, so we can now add the current item.
764 $sorted[] = $id;
765
766 return true;
767 }
768
769 /**
770 * Gets the versioned URL for a script module src.
771 *
772 * If $version is set to false, the version number is the currently installed
773 * WordPress version. If $version is set to null, no version is added.
774 * Otherwise, the string passed in $version is used.
775 *
776 * @since 6.5.0
777 *
778 * @param string $id The script module identifier.
779 * @return string The script module src with a version if relevant.
780 */
781 private function get_src( string $id ): string {
782 if ( ! isset( $this->registered[ $id ] ) ) {
783 return '';
784 }
785
786 $script_module = $this->registered[ $id ];
787 $src = $script_module['src'];
788
789 if ( '' !== $src ) {
790 if ( false === $script_module['version'] ) {
791 $src = add_query_arg( 'ver', get_bloginfo( 'version' ), $src );
792 } elseif ( null !== $script_module['version'] ) {
793 $src = add_query_arg( 'ver', $script_module['version'], $src );
794 }
795 }
796
797 /**
798 * Filters the script module source.
799 *
800 * @since 6.5.0
801 *
802 * @param string $src Module source URL.
803 * @param string $id Module identifier.
804 */
805 $src = apply_filters( 'script_module_loader_src', $src, $id );
806 if ( ! is_string( $src ) ) {
807 $src = '';
808 }
809
810 return $src;
811 }
812
813 /**
814 * Print data associated with Script Modules.
815 *
816 * The data will be embedded in the page HTML and can be read by Script Modules on page load.
817 *
818 * @since 6.7.0
819 *
820 * Data can be associated with a Script Module via the
821 * {@see "script_module_data_{$module_id}"} filter.
822 *
823 * The data for a Script Module will be serialized as JSON in a script tag with an ID of the
824 * form `wp-script-module-data-{$module_id}`.
825 */
826 public function print_script_module_data(): void {
827 $modules = array();
828 foreach ( array_unique( $this->queue ) as $id ) {
829 if ( '@wordpress/a11y' === $id ) {
830 $this->a11y_available = true;
831 }
832 $modules[ $id ] = true;
833 }
834 foreach ( array_keys( $this->get_import_map()['imports'] ) as $id ) {
835 if ( '@wordpress/a11y' === $id ) {
836 $this->a11y_available = true;
837 }
838 $modules[ $id ] = true;
839 }
840
841 foreach ( array_keys( $modules ) as $module_id ) {
842 /**
843 * Filters data associated with a given Script Module.
844 *
845 * Script Modules may require data that is required for initialization or is essential
846 * to have immediately available on page load. These are suitable use cases for
847 * this data.
848 *
849 * The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID
850 * that the data is associated with.
851 *
852 * This is best suited to pass essential data that must be available to the module for
853 * initialization or immediately on page load. It does not replace the REST API or
854 * fetching data from the client.
855 *
856 * Example:
857 *
858 * add_filter(
859 * 'script_module_data_MyScriptModuleID',
860 * function ( array $data ): array {
861 * $data['dataForClient'] = 'ok';
862 * return $data;
863 * }
864 * );
865 *
866 * If the filter returns no data (an empty array), nothing will be embedded in the page.
867 *
868 * The data for a given Script Module, if provided, will be JSON serialized in a script
869 * tag with an ID of the form `wp-script-module-data-{$module_id}`.
870 *
871 * The data can be read on the client with a pattern like this:
872 *
873 * Example:
874 *
875 * const dataContainer = document.getElementById( 'wp-script-module-data-MyScriptModuleID' );
876 * let data = {};
877 * if ( dataContainer ) {
878 * try {
879 * data = JSON.parse( dataContainer.textContent );
880 * } catch {}
881 * }
882 * // data.dataForClient === 'ok';
883 * initMyScriptModuleWithData( data );
884 *
885 * @since 6.7.0
886 *
887 * @param array $data The data associated with the Script Module.
888 */
889 $data = apply_filters( "script_module_data_{$module_id}", array() );
890
891 if ( is_array( $data ) && array() !== $data ) {
892 /*
893 * This data will be printed as JSON inside a script tag like this:
894 * <script type="application/json"></script>
895 *
896 * A script tag must be closed by a sequence beginning with `</`. It's impossible to
897 * close a script tag without using `<`. We ensure that `<` is escaped and `/` can
898 * remain unescaped, so `</script>` will be printed as `\u003C/script\u00E3`.
899 *
900 * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E.
901 * - JSON_UNESCAPED_SLASHES: Don't escape /.
902 *
903 * If the page will use UTF-8 encoding, it's safe to print unescaped unicode:
904 *
905 * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`).
906 * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when
907 * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was
908 * before PHP 7.1 without this constant. Available as of PHP 7.1.0.
909 *
910 * The JSON specification requires encoding in UTF-8, so if the generated HTML page
911 * is not encoded in UTF-8 then it's not safe to include those literals. They must
912 * be escaped to avoid encoding issues.
913 *
914 * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements.
915 * @see https://www.php.net/manual/en/json.constants.php for details on these constants.
916 * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing.
917 */
918 $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS;
919 if ( ! is_utf8_charset() ) {
920 $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
921 }
922
923 wp_print_inline_script_tag(
924 (string) wp_json_encode(
925 $data,
926 $json_encode_flags
927 ),
928 array(
929 'type' => 'application/json',
930 'id' => "wp-script-module-data-{$module_id}",
931 )
932 );
933 }
934 }
935 }
936
937 /**
938 * @access private This is only intended to be called by the registered actions.
939 *
940 * @since 6.7.0
941 */
942 public function print_a11y_script_module_html() {
943 if ( ! $this->a11y_available ) {
944 return;
945 }
946 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;">'
947 . '<p id="a11y-speak-intro-text" class="a11y-speak-intro-text" hidden>' . esc_html__( 'Notifications' ) . '</p>'
948 . '<div id="a11y-speak-assertive" class="a11y-speak-region" aria-live="assertive" aria-relevant="additions text" aria-atomic="true"></div>'
949 . '<div id="a11y-speak-polite" class="a11y-speak-region" aria-live="polite" aria-relevant="additions text" aria-atomic="true"></div>'
950 . '</div>';
951 }
952}
953