ue, $this->get_recursive_dependents( $id ) );
$fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $queued_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.
*
* @since 6.5.0
*/
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 ) ) {
continue;
}
$src = $this->get_src( $id );
if ( '' === $src ) {
continue;
}
$enqueued_dependents = array_intersect( $this->get_recursive_dependents( $id ), $this->queue );
$highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents );
printf(
'registered[ $id ]['fetchpriority'] && 'auto' !== $this->registered[ $id ]['fetchpriority'] ) {
printf( ' data-wp-fetchpriority="%s"', esc_attr( $this->registered[ $id ]['fetchpriority'] ) );
}
echo ">\n";
}
}
/**
* Prints the import map using a script tag with a type="importmap" attribute.
*
* @since 6.5.0
*/
public function print_import_map() {
$import_map = $this->get_import_map();
if ( ! empty( $import_map['imports'] ) ) {
wp_print_inline_script_tag(
(string) wp_json_encode( $import_map, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
array(
'type' => 'importmap',
'id' => 'wp-importmap',
)
);
}
}
/**
* Returns the import map array.
*
* @since 6.5.0
*
* @return array Array with an `imports` key mapping to an array of script module identifiers and their respective
* URLs, including the version query.
*/
private function get_import_map(): array {
$imports = array();
foreach ( array_keys( $this->get_dependencies( $this->queue ) ) as $id ) {
$src = $this->get_src( $id );
if ( '' !== $src ) {
$imports[ $id ] = $src;
}
}
return array( 'imports' => $imports );
}
/**
* Retrieves the list of script modules marked for enqueue.
*
* Even though this is a private method and is unused in core, there are ecosystem plugins accessing it via the
* Reflection API. The ecosystem should rather use {@see self::get_queue()}.
*
* @since 6.5.0
*
* @return array Script modules marked for enqueue, keyed by script module identifier.
*/
private function get_marked_for_enqueue(): array {
return wp_array_slice_assoc(
$this->registered,
$this->queue
);
}
/**
* Retrieves all the dependencies for the given script module identifiers, filtered by import types.
*
* It will consolidate an array containing a set of unique dependencies based
* on the requested import types: 'static', 'dynamic', or both. This method is
* recursive and also retrieves dependencies of the dependencies.
*
* @since 6.5.0
*
* @param string[] $ids The identifiers of the script modules for which to gather dependencies.
* @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
* Default is both.
* @return array List of dependencies, keyed by script module identifier.
*/
private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
$all_dependencies = array();
$id_queue = $ids;
while ( ! empty( $id_queue ) ) {
$id = array_shift( $id_queue );
if ( ! isset( $this->registered[ $id ] ) ) {
continue;
}
foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
if (
! isset( $all_dependencies[ $dependency['id'] ] ) &&
in_array( $dependency['import'], $import_types, true ) &&
isset( $this->registered[ $dependency['id'] ] )
) {
$all_dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
// Add this dependency to the list to get dependencies for.
$id_queue[] = $dependency['id'];
}
}
}
return $all_dependencies;
}
/**
* Gets all dependents of a script module.
*
* This is not recursive.
*
* @since 6.9.0
*
* @see WP_Scripts::get_dependents()
*
* @param string $id The script ID.
* @return string[] Script module IDs.
*/
private function get_dependents( string $id ): array {
// Check if dependents map for the handle in question is present. If so, use it.
if ( isset( $this->dependents_map[ $id ] ) ) {
return $this->dependents_map[ $id ];
}
$dependents = array();
// Iterate over all registered scripts, finding dependents of the script passed to this method.
foreach ( $this->registered as $registered_id => $args ) {
if ( in_array( $id, wp_list_pluck( $args['dependencies'], 'id' ), true ) ) {
$dependents[] = $registered_id;
}
}
// Add the module's dependents to the map to ease future lookups.
$this->dependents_map[ $id ] = $dependents;
return $dependents;
}
/**
* Gets all recursive dependents of a script module.
*
* @since 6.9.0
*
* @see WP_Scripts::get_dependents()
*
* @param string $id The script ID.
* @return string[] Script module IDs.
*/
private function get_recursive_dependents( string $id ): array {
$dependents = array();
$id_queue = array( $id );
$processed = array();
while ( ! empty( $id_queue ) ) {
$current_id = array_shift( $id_queue );
// Skip unregistered or already-processed script modules.
if ( ! isset( $this->registered[ $current_id ] ) || isset( $processed[ $current_id ] ) ) {
continue;
}
// Mark as processed to guard against infinite loops from circular dependencies.
$processed[ $current_id ] = true;
// Find the direct dependents of the current script.
foreach ( $this->get_dependents( $current_id ) as $dependent_id ) {
// Only add the dependent if we haven't found it before.
if ( ! isset( $dependents[ $dependent_id ] ) ) {
$dependents[ $dependent_id ] = true;
// Add dependency to the queue.
$id_queue[] = $dependent_id;
}
}
}
return array_keys( $dependents );
}
/**
* Sorts the given script module identifiers based on their dependencies.
*
* It will return a list of script module identifiers sorted in the order
* they should be printed, so that dependencies are printed before the script
* modules that depend on them.
*
* @since 6.9.0
*
* @param string[] $ids The identifiers of the script modules to sort.
* @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
* Default is both.
* @return string[] Sorted list of script module identifiers.
*/
private function get_sorted_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
$sorted = array();
foreach ( $ids as $id ) {
$this->sort_item_dependencies( $id, $import_types, $sorted );
}
return array_unique( $sorted );
}
/**
* Recursively sorts the dependencies for a single script module identifier.
*
* @since 6.9.0
*
* @param string $id The identifier of the script module to sort.
* @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
* @param string[] &$sorted The array of sorted identifiers, passed by reference.
* @return bool True on success, false on failure (e.g., missing dependency).
*/
private function sort_item_dependencies( string $id, array $import_types, array &$sorted ): bool {
// If already processed, don't do it again.
if ( in_array( $id, $sorted, true ) ) {
return true;
}
// If the item doesn't exist, fail.
if ( ! isset( $this->registered[ $id ] ) ) {
return false;
}
$dependency_ids = array();
foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
if ( in_array( $dependency['import'], $import_types, true ) ) {
$dependency_ids[] = $dependency['id'];
}
}
// If the item requires dependencies that do not exist, fail.
$missing_dependencies = array_diff( $dependency_ids, array_keys( $this->registered ) );
if ( count( $missing_dependencies ) > 0 ) {
if ( ! in_array( $id, $this->modules_with_missing_dependencies, true ) ) {
_doing_it_wrong(
get_class( $this ) . '::register',
sprintf(
/* translators: 1: Script module ID, 2: List of missing dependency IDs. */
__( 'The script module with the ID "%1$s" was enqueued with dependencies that are not registered: %2$s.' ),
$id,
implode( wp_get_list_item_separator(), $missing_dependencies )
),
'6.9.1'
);
$this->modules_with_missing_dependencies[] = $id;
}
return false;
}
// Recursively process dependencies.
foreach ( $dependency_ids as $dependency_id ) {
if ( ! $this->sort_item_dependencies( $dependency_id, $import_types, $sorted ) ) {
// A dependency failed to resolve, so this branch fails.
return false;
}
}
// All dependencies are sorted, so we can now add the current item.
$sorted[] = $id;
return true;
}
/**
* Gets the versioned URL for a script module src.
*
* 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.
* Otherwise, the string passed in $version is used.
*
* @since 6.5.0
*
* @param string $id The script module identifier.
* @return string The script module src with a version if relevant.
*/
private function get_src( string $id ): string {
if ( ! isset( $this->registered[ $id ] ) ) {
return '';
}
$script_module = $this->registered[ $id ];
$src = $script_module['src'];
if ( '' !== $src ) {
if ( false === $script_module['version'] ) {
$src = add_query_arg( 'ver', get_bloginfo( 'version' ), $src );
} elseif ( null !== $script_module['version'] ) {
$src = add_query_arg( 'ver', $script_module['version'], $src );
}
}
/**
* Filters the script module source.
*
* @since 6.5.0
*
* @param string $src Module source URL.
* @param string $id Module identifier.
*/
$src = apply_filters( 'script_module_loader_src', $src, $id );
if ( ! is_string( $src ) ) {
$src = '';
}
return $src;
}
/**
* Print data associated with Script Modules.
*
* The data will be embedded in the page HTML and can be read by Script Modules on page load.
*
* @since 6.7.0
*
* Data can be associated with a Script Module via the
* {@see "script_module_data_{$module_id}"} filter.
*
* The data for a Script Module will be serialized as JSON in a script tag with an ID of the
* form `wp-script-module-data-{$module_id}`.
*/
public function print_script_module_data(): void {
$modules = array();
foreach ( array_unique( $this->queue ) as $id ) {
if ( '@wordpress/a11y' === $id ) {
$this->a11y_available = true;
}
$modules[ $id ] = true;
}
foreach ( array_keys( $this->get_import_map()['imports'] ) as $id ) {
if ( '@wordpress/a11y' === $id ) {
$this->a11y_available = true;
}
$modules[ $id ] = true;
}
foreach ( array_keys( $modules ) as $module_id ) {
/**
* Filters data associated with a given Script Module.
*
* Script Modules may require data that is required for initialization or is essential
* to have immediately available on page load. These are suitable use cases for
* this data.
*
* The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID
* that the data is associated with.
*
* This is best suited to pass essential data that must be available to the module for
* initialization or immediately on page load. It does not replace the REST API or
* fetching data from the client.
*
* Example:
*
* add_filter(
* 'script_module_data_MyScriptModuleID',
* function ( array $data ): array {
* $data['dataForClient'] = 'ok';
* return $data;
* }
* );
*
* If the filter returns no data (an empty array), nothing will be embedded in the page.
*
* The data for a given Script Module, if provided, will be JSON serialized in a script
* tag with an ID of the form `wp-script-module-data-{$module_id}`.
*
* The data can be read on the client with a pattern like this:
*
* Example:
*
* const dataContainer = document.getElementById( 'wp-script-module-data-MyScriptModuleID' );
* let data = {};
* if ( dataContainer ) {
* try {
* data = JSON.parse( dataContainer.textContent );
* } catch {}
* }
* // data.dataForClient === 'ok';
* initMyScriptModuleWithData( data );
*
* @since 6.7.0
*
* @param array $data The data associated with the Script Module.
*/
$data = apply_filters( "script_module_data_{$module_id}", array() );
if ( is_array( $data ) && array() !== $data ) {
/*
* This data will be printed as JSON inside a script tag like this:
*
*
* A script tag must be closed by a sequence beginning with `` will be printed as `\u003C/script\u00E3`.
*
* - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E.
* - JSON_UNESCAPED_SLASHES: Don't escape /.
*
* If the page will use UTF-8 encoding, it's safe to print unescaped unicode:
*
* - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`).
* - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when
* JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was
* before PHP 7.1 without this constant. Available as of PHP 7.1.0.
*
* The JSON specification requires encoding in UTF-8, so if the generated HTML page
* is not encoded in UTF-8 then it's not safe to include those literals. They must
* be escaped to avoid encoding issues.
*
* @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements.
* @see https://www.php.net/manual/en/json.constants.php for details on these constants.
* @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing.
*/
$json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS;
if ( ! is_utf8_charset() ) {
$json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
}
wp_print_inline_script_tag(
(string) wp_json_encode(
$data,
$json_encode_flags
),
array(
'type' => 'application/json',
'id' => "wp-script-module-data-{$module_id}",
)
);
}
}
}
/**
* @access private This is only intended to be called by the registered actions.
*
* @since 6.7.0
*/
public function print_a11y_script_module_html() {
if ( ! $this->a11y_available ) {
return;
}
echo '