* Script Modules API: WP_Script_Modules class.
* Native support for ES Modules and Import Maps.
* @subpackage Script Modules
* Core class used to register script modules.
class WP_Script_Modules {
* Holds the registered script modules, keyed by script module identifier.
* @var array<string, array<string, mixed>>
private $registered = array();
* An array of IDs for queued script modules.
private $queue = array();
* Holds the script module identifiers that have been printed.
* Tracks whether the @wordpress/a11y script module is available.
* Some additional HTML is required on the page for the module to work. Track
* whether it's available to print at the appropriate time.
private $a11y_available = false;
* Holds a mapping of dependents (as IDs) for a given script ID.
* Used to optimize recursive dependency tree checks.
* @var array<string, string[]>
private $dependents_map = array();
* Holds the valid values for fetchpriority.
private $priorities = array(
* Registers the script module if no script module with that script module
* identifier has already been registered.
* @since 6.9.0 Added the $args parameter.
* @param string $id The identifier of the script module. Should be unique. It will be used in the
* @param string $src Optional. Full URL of the script module, or path of the script module relative
* to the WordPress root directory. If it is provided and the script module has
* not been registered yet, it will be registered.
* Optional. List of dependencies.
* @type string|array ...$0 {
* An array of script module identifiers of the dependencies of this script
* module. The dependencies can be strings or arrays. If they are arrays,
* they need an `id` key with the script module identifier, and can contain
* an `import` key with either `static` or `dynamic`. By default,
* dependencies that don't contain an `import` key are considered static.
* @type string $id The script module identifier.
* @type string $import Optional. Import type. May be either `static` or
* `dynamic`. Defaults to `static`.
* @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
* It is added to the URL as a query string for cache busting purposes. If $version
* is set to false, the version number is the currently installed WordPress version.
* If $version is set to null, no version is added.
* Optional. An array of additional args. Default empty array.
* @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
* @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
public function register( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) {
_doing_it_wrong( __METHOD__, __( 'Non-empty string required for id.' ), '6.9.0' );
if ( ! isset( $this->registered[ $id ] ) ) {
foreach ( $deps as $dependency ) {
if ( is_array( $dependency ) ) {
if ( ! isset( $dependency['id'] ) || ! is_string( $dependency['id'] ) ) {
_doing_it_wrong( __METHOD__, __( 'Missing required id key in entry among dependencies array.' ), '6.5.0' );
'id' => $dependency['id'],
'import' => isset( $dependency['import'] ) && 'dynamic' === $dependency['import'] ? 'dynamic' : 'static',
} elseif ( is_string( $dependency ) ) {
_doing_it_wrong( __METHOD__, __( 'Entries in dependencies array must be either strings or arrays with an id key.' ), '6.5.0' );
$in_footer = isset( $args['in_footer'] ) && (bool) $args['in_footer'];
if ( isset( $args['fetchpriority'] ) ) {
if ( $this->is_valid_fetchpriority( $args['fetchpriority'] ) ) {
$fetchpriority = $args['fetchpriority'];
/* translators: 1: $fetchpriority, 2: $id */
__( 'Invalid fetchpriority `%1$s` defined for `%2$s` during script registration.' ),
is_string( $args['fetchpriority'] ) ? $args['fetchpriority'] : gettype( $args['fetchpriority'] ),
$this->registered[ $id ] = array(
'dependencies' => $dependencies,
'in_footer' => $in_footer,
'fetchpriority' => $fetchpriority,
* Gets IDs for queued script modules.
* @return string[] Script module IDs.
public function get_queue(): array {
* Checks if the provided fetchpriority is valid.
* @param string|mixed $priority Fetch priority.
* @return bool Whether valid fetchpriority.
private function is_valid_fetchpriority( $priority ): bool {
return in_array( $priority, $this->priorities, true );
* Sets the fetch priority for a script module.
* @param string $id Script module identifier.
* @param 'auto'|'low'|'high' $priority Fetch priority for the script module.
* @return bool Whether setting the fetchpriority was successful.
public function set_fetchpriority( string $id, string $priority ): bool {
if ( ! isset( $this->registered[ $id ] ) ) {
if ( '' === $priority ) {
if ( ! $this->is_valid_fetchpriority( $priority ) ) {
/* translators: %s: Invalid fetchpriority. */
sprintf( __( 'Invalid fetchpriority: %s' ), $priority ),
$this->registered[ $id ]['fetchpriority'] = $priority;
* Sets whether a script module should be printed in the footer.
* This is only relevant in block themes.
* @param string $id Script module identifier.
* @param bool $in_footer Whether to print in the footer.
* @return bool Whether setting the printing location was successful.
public function set_in_footer( string $id, bool $in_footer ): bool {
if ( ! isset( $this->registered[ $id ] ) ) {
$this->registered[ $id ]['in_footer'] = $in_footer;
* Marks the script module to be enqueued in the page.
* If a src is provided and the script module has not been registered yet, it
* @since 6.9.0 Added the $args parameter.
* @param string $id The identifier of the script module. Should be unique. It will be used in the
* @param string $src Optional. Full URL of the script module, or path of the script module relative
* to the WordPress root directory. If it is provided and the script module has
* not been registered yet, it will be registered.
* Optional. List of dependencies.
* @type string|array ...$0 {
* An array of script module identifiers of the dependencies of this script
* module. The dependencies can be strings or arrays. If they are arrays,
* they need an `id` key with the script module identifier, and can contain
* an `import` key with either `static` or `dynamic`. By default,
* dependencies that don't contain an `import` key are considered static.
* @type string $id The script module identifier.
* @type string $import Optional. Import type. May be either `static` or
* `dynamic`. Defaults to `static`.
* @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
* It is added to the URL as a query string for cache busting purposes. If $version
* is set to false, the version number is the currently installed WordPress version.
* If $version is set to null, no version is added.
* Optional. An array of additional args. Default empty array.
* @type bool $in_footer Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
* @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
public function enqueue( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) {
_doing_it_wrong( __METHOD__, __( 'Non-empty string required for id.' ), '6.9.0' );
if ( ! in_array( $id, $this->queue, true ) ) {
if ( ! isset( $this->registered[ $id ] ) && $src ) {
$this->register( $id, $src, $deps, $version, $args );
* Unmarks the script module so it will no longer be enqueued in the page.
* @param string $id The identifier of the script module.
public function dequeue( string $id ) {
$this->queue = array_values( array_diff( $this->queue, array( $id ) ) );
* Removes a registered script module.
* @param string $id The identifier of the script module.
public function deregister( string $id ) {
unset( $this->registered[ $id ] );
* Adds the hooks to print the import map, enqueued script modules and script
* In classic themes, the script modules used by the blocks are not yet known
* when the `wp_head` actions is fired, so it needs to print everything in the
public function add_hooks() {
$is_block_theme = wp_is_block_theme();
$position = $is_block_theme ? 'wp_head' : 'wp_footer';
add_action( $position, array( $this, 'print_import_map' ) );
* Modules can only be printed in the head for block themes because only with
* block themes will import map be fully populated by modules discovered by
* rendering the block template. In classic themes, modules are enqueued during
* template rendering, thus the import map must be printed in the footer,
* followed by all enqueued modules.
add_action( 'wp_head', array( $this, 'print_head_enqueued_script_modules' ) );
add_action( 'wp_footer', array( $this, 'print_enqueued_script_modules' ) );
add_action( $position, array( $this, 'print_script_module_preloads' ) );
add_action( 'admin_print_footer_scripts', array( $this, 'print_import_map' ) );
add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) );
add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_preloads' ) );
add_action( 'wp_footer', array( $this, 'print_script_module_data' ) );
add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_data' ) );
add_action( 'wp_footer', array( $this, 'print_a11y_script_module_html' ), 20 );
add_action( 'admin_print_footer_scripts', array( $this, 'print_a11y_script_module_html' ), 20 );
* Gets the highest fetch priority for the provided script IDs.
* @param string[] $ids Script module IDs.
* @return 'auto'|'low'|'high' Highest fetch priority for the provided script module IDs.
private function get_highest_fetchpriority( array $ids ): string {
static $high_priority_index = null;
if ( null === $high_priority_index ) {
$high_priority_index = count( $this->priorities ) - 1;
$highest_priority_index = 0;
foreach ( $ids as $id ) {
if ( isset( $this->registered[ $id ] ) ) {
$highest_priority_index = (int) max(
(int) array_search( $this->registered[ $id ]['fetchpriority'], $this->priorities, true )
if ( $high_priority_index === $highest_priority_index ) {
return $this->priorities[ $highest_priority_index ];
* Prints the enqueued script modules in head.
* This is only used in block themes.
public function print_head_enqueued_script_modules() {
foreach ( $this->get_sorted_dependencies( $this->queue ) as $id ) {
isset( $this->registered[ $id ] ) &&
! $this->registered[ $id ]['in_footer']
// If any dependency is set to be printed in footer, skip printing this module in head.
$dependencies = array_keys( $this->get_dependencies( array( $id ) ) );
foreach ( $dependencies as $dependency_id ) {
in_array( $dependency_id, $this->queue, true ) &&
isset( $this->registered[ $dependency_id ] ) &&
$this->registered[ $dependency_id ]['in_footer']
$this->print_script_module( $id );
* Prints the enqueued script modules in footer.
public function print_enqueued_script_modules() {
foreach ( $this->get_sorted_dependencies( $this->queue ) as $id ) {
$this->print_script_module( $id );
* Prints the enqueued script module using script tags with type="module"
* @param string $id The script module identifier.
private function print_script_module( string $id ) {
if ( in_array( $id, $this->done, true ) || ! in_array( $id, $this->queue, true ) ) {
$src = $this->get_src( $id );
'id' => $id . '-js-module',
$script_module = $this->registered[ $id ];
$dependents = $this->get_recursive_dependents( $id );
$fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $dependents ) );
if ( 'auto' !== $fetchpriority ) {
$attributes['fetchpriority'] = $fetchpriority;
if ( $fetchpriority !== $script_module['fetchpriority'] ) {
$attributes['data-wp-fetchpriority'] = $script_module['fetchpriority'];
wp_print_script_tag( $attributes );
* Prints the static dependencies of the enqueued script modules using
* link tags with rel="modulepreload" attributes.
* If a script module is marked for enqueue, it will not be preloaded.
public function print_script_module_preloads() {
$dependency_ids = $this->get_sorted_dependencies( $this->queue, array( 'static' ) );
foreach ( $dependency_ids as $id ) {
// Don't preload if it's marked for enqueue.
if ( in_array( $id, $this->queue, true ) ) {
$src = $this->get_src( $id );
$enqueued_dependents = array_intersect( $this->get_recursive_dependents( $id ), $this->queue );
$highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents );
'<link rel="modulepreload" href="%s" id="%s"',
esc_attr( $id . '-js-modulepreload' )
if ( 'auto' !== $highest_fetchpriority ) {
printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) );
if ( $highest_fetchpriority !== $this->registered[ $id ]['fetchpriority'] && 'auto' !== $this->registered[ $id ]['fetchpriority'] ) {
printf( ' data-wp-fetchpriority="%s"', esc_attr( $this->registered[ $id ]['fetchpriority'] ) );