<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[60931] trunk: Script Loader: Propagate `fetchpriority` from dependents to dependencies.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/60931">60931</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/60931","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>westonruter</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2025-10-14 05:45:17 +0000 (Tue, 14 Oct 2025)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Script Loader: Propagate `fetchpriority` from dependents to dependencies.
This introduces a "fetchpriority bumping" mechanism for both classic scripts (`WP_Scripts`) and script modules (`WP_Script_Modules`). When a script with a higher `fetchpriority` is enqueued, any of its dependencies will have their `fetchpriority` elevated to match that of the highest-priority dependent. This ensures that all assets in a critical dependency chain are loaded with the appropriate priority, preventing a high-priority script from being blocked by a low-priority dependency. This is similar to logic used in script loading strategies to ensure that a blocking dependent causes delayed (`async`/`defer`) dependencies to also become blocking. See <a href="https://core.trac.wordpress.org/ticket/12009">#12009</a>.
When a script's `fetchpriority` is escalated, its original, registered priority is added to the tag via a `data-wp-fetchpriority` attribute. This matches the addition of the `data-wp-strategy` parameter added when the resulting loading strategy does not match the original.
Developed in https://github.com/WordPress/wordpress-develop/pull/9770.
Follow-up to <a href="https://core.trac.wordpress.org/changeset/60704">[60704]</a>.
Props westonruter, jonsurrell.
Fixes <a href="https://core.trac.wordpress.org/ticket/61734">#61734</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesclasswpdependencyphp">trunk/src/wp-includes/class-wp-dependency.php</a></li>
<li><a href="#trunksrcwpincludesclasswpscriptmodulesphp">trunk/src/wp-includes/class-wp-script-modules.php</a></li>
<li><a href="#trunksrcwpincludesclasswpscriptsphp">trunk/src/wp-includes/class-wp-scripts.php</a></li>
<li><a href="#trunksrcwpincludesscriptmodulesphp">trunk/src/wp-includes/script-modules.php</a></li>
<li><a href="#trunktestsphpunittestsdependenciesscriptsphp">trunk/tests/phpunit/tests/dependencies/scripts.php</a></li>
<li><a href="#trunktestsphpunittestsscriptmoduleswpScriptModulesphp">trunk/tests/phpunit/tests/script-modules/wpScriptModules.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesclasswpdependencyphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-dependency.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-dependency.php 2025-10-14 00:10:31 UTC (rev 60930)
+++ trunk/src/wp-includes/class-wp-dependency.php 2025-10-14 05:45:17 UTC (rev 60931)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -50,7 +50,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * Used for cache-busting.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 2.6.0
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @var bool|string
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @var string|false|null
</ins><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> public $ver = false;
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpscriptmodulesphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-script-modules.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-script-modules.php 2025-10-14 00:10:31 UTC (rev 60930)
+++ trunk/src/wp-includes/class-wp-script-modules.php 2025-10-14 05:45:17 UTC (rev 60931)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -42,6 +42,15 @@
</span><span class="cx" style="display: block; padding: 0 10px"> private $a11y_available = false;
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Holds a mapping of dependents (as IDs) for a given script ID.
+ * Used to optimize recursive dependency tree checks.
+ *
+ * @since 6.9.0
+ * @var array<string, string[]>
+ */
+ private $dependents_map = array();
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Registers the script module if no script module with that script module
</span><span class="cx" style="display: block; padding: 0 10px"> * identifier has already been registered.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -270,6 +279,38 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Gets the highest fetch priority for the provided script IDs.
+ *
+ * @since 6.9.0
+ *
+ * @param string[] $ids Script module IDs.
+ * @return string Highest fetch priority for the provided script module IDs.
+ */
+ private function get_highest_fetchpriority( array $ids ): string {
+ static $priorities = array(
+ 'low',
+ 'auto',
+ 'high',
+ );
+ $high_priority_index = count( $priorities ) - 1;
+
+ $highest_priority_index = 0;
+ foreach ( $ids as $id ) {
+ if ( isset( $this->registered[ $id ] ) ) {
+ $highest_priority_index = max(
+ $highest_priority_index,
+ array_search( $this->registered[ $id ]['fetchpriority'], $priorities, true )
+ );
+ if ( $high_priority_index === $highest_priority_index ) {
+ break;
+ }
+ }
+ }
+
+ return $priorities[ $highest_priority_index ];
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Prints the enqueued script modules using script tags with type="module"
</span><span class="cx" style="display: block; padding: 0 10px"> * attributes.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -282,15 +323,21 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'src' => $this->get_src( $id ),
</span><span class="cx" style="display: block; padding: 0 10px"> 'id' => $id . '-js-module',
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( 'auto' !== $script_module['fetchpriority'] ) {
- $args['fetchpriority'] = $script_module['fetchpriority'];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ $dependents = $this->get_recursive_dependents( $id );
+ $fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $dependents ) );
+ if ( 'auto' !== $fetchpriority ) {
+ $args['fetchpriority'] = $fetchpriority;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( $fetchpriority !== $script_module['fetchpriority'] ) {
+ $args['data-wp-fetchpriority'] = $script_module['fetchpriority'];
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> wp_print_script_tag( $args );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * Prints the the static dependencies of the enqueued script modules using
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Prints the static dependencies of the enqueued script modules using
</ins><span class="cx" style="display: block; padding: 0 10px"> * link tags with rel="modulepreload" attributes.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * If a script module is marked for enqueue, it will not be preloaded.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -301,12 +348,20 @@
</span><span class="cx" style="display: block; padding: 0 10px"> foreach ( $this->get_dependencies( array_unique( $this->queue ), array( 'static' ) ) as $id => $script_module ) {
</span><span class="cx" style="display: block; padding: 0 10px"> // Don't preload if it's marked for enqueue.
</span><span class="cx" style="display: block; padding: 0 10px"> if ( ! in_array( $id, $this->queue, true ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- echo sprintf(
- '<link rel="modulepreload" href="%s" id="%s"%s>',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $enqueued_dependents = array_intersect( $this->get_recursive_dependents( $id ), $this->queue );
+ $highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents );
+ printf(
+ '<link rel="modulepreload" href="%s" id="%s"',
</ins><span class="cx" style="display: block; padding: 0 10px"> esc_url( $this->get_src( $id ) ),
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- esc_attr( $id . '-js-modulepreload' ),
- 'auto' !== $script_module['fetchpriority'] ? sprintf( ' fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) ) : ''
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ esc_attr( $id . '-js-modulepreload' )
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( 'auto' !== $highest_fetchpriority ) {
+ printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) );
+ }
+ if ( $highest_fetchpriority !== $script_module['fetchpriority'] && 'auto' !== $script_module['fetchpriority'] ) {
+ printf( ' data-wp-fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) );
+ }
+ echo ">\n";
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -374,18 +429,20 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * Default is both.
</span><span class="cx" style="display: block; padding: 0 10px"> * @return array[] List of dependencies, keyed by script module identifier.
</span><span class="cx" style="display: block; padding: 0 10px"> */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
</ins><span class="cx" style="display: block; padding: 0 10px"> return array_reduce(
</span><span class="cx" style="display: block; padding: 0 10px"> $ids,
</span><span class="cx" style="display: block; padding: 0 10px"> function ( $dependency_script_modules, $id ) use ( $import_types ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $dependencies = array();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
- if (
- in_array( $dependency['import'], $import_types, true ) &&
- isset( $this->registered[ $dependency['id'] ] ) &&
- ! isset( $dependency_script_modules[ $dependency['id'] ] )
- ) {
- $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if ( isset( $this->registered[ $id ] ) ) {
+ foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
+ if (
+ in_array( $dependency['import'], $import_types, true ) &&
+ isset( $this->registered[ $dependency['id'] ] ) &&
+ ! isset( $dependency_script_modules[ $dependency['id'] ] )
+ ) {
+ $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> return array_merge( $dependency_script_modules, $dependencies, $this->get_dependencies( array_keys( $dependencies ), $import_types ) );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -395,6 +452,75 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * 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 {
+ $get = function ( string $id, array $checked = array() ) use ( &$get ): array {
+
+ // If by chance an unregistered script module is checked or there is a recursive dependency, return early.
+ if ( ! isset( $this->registered[ $id ] ) || isset( $checked[ $id ] ) ) {
+ return array();
+ }
+
+ // Mark this script module as checked to guard against infinite recursion.
+ $checked[ $id ] = true;
+
+ $dependents = array();
+ foreach ( $this->get_dependents( $id ) as $dependent ) {
+ $dependents = array_merge(
+ $dependents,
+ array( $dependent ),
+ $get( $dependent, $checked )
+ );
+ }
+
+ return $dependents;
+ };
+
+ return array_unique( $get( $id ) );
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Gets the versioned URL for a script module src.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * If $version is set to false, the version number is the currently installed
</span></span></pre></div>
<a id="trunksrcwpincludesclasswpscriptsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/class-wp-scripts.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/class-wp-scripts.php 2025-10-14 00:10:31 UTC (rev 60930)
+++ trunk/src/wp-includes/class-wp-scripts.php 2025-10-14 05:45:17 UTC (rev 60931)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -127,7 +127,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> * Used to optimize recursive dependency tree checks.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 6.3.0
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @var array
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @var array<string, string[]>
</ins><span class="cx" style="display: block; padding: 0 10px"> */
</span><span class="cx" style="display: block; padding: 0 10px"> private $dependents_map = array();
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -439,9 +439,24 @@
</span><span class="cx" style="display: block; padding: 0 10px"> if ( $intended_strategy ) {
</span><span class="cx" style="display: block; padding: 0 10px"> $attr['data-wp-strategy'] = $intended_strategy;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( isset( $obj->extra['fetchpriority'] ) && 'auto' !== $obj->extra['fetchpriority'] && $this->is_valid_fetchpriority( $obj->extra['fetchpriority'] ) ) {
- $attr['fetchpriority'] = $obj->extra['fetchpriority'];
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ // Determine fetchpriority.
+ $original_fetchpriority = isset( $obj->extra['fetchpriority'] ) ? $obj->extra['fetchpriority'] : null;
+ if ( null === $original_fetchpriority || ! $this->is_valid_fetchpriority( $original_fetchpriority ) ) {
+ $original_fetchpriority = 'auto';
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $actual_fetchpriority = $this->get_highest_fetchpriority_with_dependents( $handle );
+ if ( null === $actual_fetchpriority ) {
+ // If null, it's likely this script was not explicitly enqueued, so in this case use the original priority.
+ $actual_fetchpriority = $original_fetchpriority;
+ }
+ if ( is_string( $actual_fetchpriority ) && 'auto' !== $actual_fetchpriority ) {
+ $attr['fetchpriority'] = $actual_fetchpriority;
+ }
+ if ( $original_fetchpriority !== $actual_fetchpriority ) {
+ $attr['data-wp-fetchpriority'] = $original_fetchpriority;
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> $tag = $translations . $ie_conditional_prefix . $before_script;
</span><span class="cx" style="display: block; padding: 0 10px"> $tag .= wp_get_script_tag( $attr );
</span><span class="cx" style="display: block; padding: 0 10px"> $tag .= $after_script . $ie_conditional_suffix;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -898,6 +913,8 @@
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="cx" style="display: block; padding: 0 10px"> * Gets all dependents of a script.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * This is not recursive.
+ *
</ins><span class="cx" style="display: block; padding: 0 10px"> * @since 6.3.0
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @param string $handle The script handle.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1052,6 +1069,62 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Gets the highest fetch priority for a given script and all of its dependent scripts.
+ *
+ * @since 6.9.0
+ * @see self::filter_eligible_strategies()
+ * @see WP_Script_Modules::get_highest_fetchpriority_with_dependents()
+ *
+ * @param string $handle Script module ID.
+ * @param array<string, true> $checked Optional. An array of already checked script handles, used to avoid recursive loops.
+ * @return string|null Highest fetch priority for the script and its dependents.
+ */
+ private function get_highest_fetchpriority_with_dependents( string $handle, array $checked = array() ): ?string {
+ // If there is a recursive dependency, return early.
+ if ( isset( $checked[ $handle ] ) ) {
+ return null;
+ }
+
+ // Mark this handle as checked to guard against infinite recursion.
+ $checked[ $handle ] = true;
+
+ // Abort if the script is not enqueued or a dependency of an enqueued script.
+ if ( ! $this->query( $handle, 'enqueued' ) ) {
+ return null;
+ }
+
+ $fetchpriority = $this->get_data( $handle, 'fetchpriority' );
+ if ( ! $this->is_valid_fetchpriority( $fetchpriority ) ) {
+ $fetchpriority = 'auto';
+ }
+
+ static $priorities = array(
+ 'low',
+ 'auto',
+ 'high',
+ );
+ $high_priority_index = count( $priorities ) - 1;
+
+ $highest_priority_index = (int) array_search( $fetchpriority, $priorities, true );
+ if ( $highest_priority_index !== $high_priority_index ) {
+ foreach ( $this->get_dependents( $handle ) as $dependent_handle ) {
+ $dependent_priority = $this->get_highest_fetchpriority_with_dependents( $dependent_handle, $checked );
+ if ( is_string( $dependent_priority ) ) {
+ $highest_priority_index = max(
+ $highest_priority_index,
+ (int) array_search( $dependent_priority, $priorities, true )
+ );
+ if ( $highest_priority_index === $high_priority_index ) {
+ break;
+ }
+ }
+ }
+ }
+
+ return $priorities[ $highest_priority_index ];
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Gets data for inline scripts registered for a specific handle.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @since 6.3.0
</span></span></pre></div>
<a id="trunksrcwpincludesscriptmodulesphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/script-modules.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/script-modules.php 2025-10-14 00:10:31 UTC (rev 60930)
+++ trunk/src/wp-includes/script-modules.php 2025-10-14 05:45:17 UTC (rev 60931)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -181,9 +181,21 @@
</span><span class="cx" style="display: block; padding: 0 10px"> break;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- // The Interactivity API is designed with server-side rendering as its primary goal, so all of its script modules should be loaded with low fetch priority since they should not be needed in the critical rendering path.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ /*
+ * The Interactivity API is designed with server-side rendering as its primary goal, so all of its script modules
+ * should be loaded with low fetchpriority since they should not be needed in the critical rendering path.
+ * Also, the @wordpress/a11y script module is intended to be used as a dynamic import dependency, in which case
+ * the fetchpriority is irrelevant. See <https://make.wordpress.org/core/2024/10/14/updates-to-script-modules-in-6-7/>.
+ * However, in case it is added as a static import dependency, the fetchpriority is explicitly set to be 'low'
+ * since the module should not be involved in the critical rendering path, and if it is, its fetchpriority will
+ * be bumped to match the fetchpriority of the dependent script.
+ */
</ins><span class="cx" style="display: block; padding: 0 10px"> $args = array();
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- if ( str_starts_with( $script_module_id, '@wordpress/interactivity' ) || str_starts_with( $script_module_id, '@wordpress/block-library' ) ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ if (
+ str_starts_with( $script_module_id, '@wordpress/interactivity' ) ||
+ str_starts_with( $script_module_id, '@wordpress/block-library' ) ||
+ '@wordpress/a11y' === $script_module_id
+ ) {
</ins><span class="cx" style="display: block; padding: 0 10px"> $args['fetchpriority'] = 'low';
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunktestsphpunittestsdependenciesscriptsphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/dependencies/scripts.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/dependencies/scripts.php 2025-10-14 00:10:31 UTC (rev 60930)
+++ trunk/tests/phpunit/tests/dependencies/scripts.php 2025-10-14 05:45:17 UTC (rev 60931)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1232,10 +1232,11 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'fetchpriority' => 'high',
</span><span class="cx" style="display: block; padding: 0 10px"> )
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // Note: All of these scripts have fetchpriority=high because the leaf dependent script has that fetch priority.
</ins><span class="cx" style="display: block; padding: 0 10px"> $output = get_echo( 'wp_print_scripts' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $expected = "<script type='text/javascript' src='/main-script-d4.js' id='main-script-d4-js' defer='defer' data-wp-strategy='defer'></script>\n";
- $expected .= "<script type='text/javascript' src='/dependent-script-d4-1.js' id='dependent-script-d4-1-js' defer='defer' data-wp-strategy='defer'></script>\n";
- $expected .= "<script type='text/javascript' src='/dependent-script-d4-2.js' id='dependent-script-d4-2-js' defer='defer' data-wp-strategy='async' fetchpriority='low'></script>\n";
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $expected = "<script type='text/javascript' src='/main-script-d4.js' id='main-script-d4-js' defer='defer' data-wp-strategy='defer' fetchpriority='high' data-wp-fetchpriority='auto'></script>\n";
+ $expected .= "<script type='text/javascript' src='/dependent-script-d4-1.js' id='dependent-script-d4-1-js' defer='defer' data-wp-strategy='defer' fetchpriority='high' data-wp-fetchpriority='auto'></script>\n";
+ $expected .= "<script type='text/javascript' src='/dependent-script-d4-2.js' id='dependent-script-d4-2-js' defer='defer' data-wp-strategy='async' fetchpriority='high' data-wp-fetchpriority='low'></script>\n";
</ins><span class="cx" style="display: block; padding: 0 10px"> $expected .= "<script type='text/javascript' src='/dependent-script-d4-3.js' id='dependent-script-d4-3-js' defer='defer' data-wp-strategy='defer' fetchpriority='high'></script>\n";
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $this->assertEqualHTML( $expected, $output, '<body>', 'Scripts registered as defer but that have dependents that are async are expected to have said dependents deferred.' );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1344,6 +1345,150 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Data provider.
+ *
+ * @return array<string, array{enqueues: string[], expected: string}>
+ */
+ public function data_provider_to_test_fetchpriority_bumping(): array {
+ return array(
+ 'enqueue_bajo' => array(
+ 'enqueues' => array( 'bajo' ),
+ 'expected' => '<script fetchpriority="low" id="bajo-js" src="/bajo.js" type="text/javascript"></script>',
+ ),
+ 'enqueue_auto' => array(
+ 'enqueues' => array( 'auto' ),
+ 'expected' => '
+ <script type="text/javascript" src="/bajo.js" id="bajo-js" data-wp-fetchpriority="low"></script>
+ <script type="text/javascript" src="/auto.js" id="auto-js"></script>
+ ',
+ ),
+ 'enqueue_alto' => array(
+ 'enqueues' => array( 'alto' ),
+ 'expected' => '
+ <script type="text/javascript" src="/bajo.js" id="bajo-js" fetchpriority="high" data-wp-fetchpriority="low"></script>
+ <script type="text/javascript" src="/auto.js" id="auto-js" fetchpriority="high" data-wp-fetchpriority="auto"></script>
+ <script type="text/javascript" src="/alto.js" id="alto-js" fetchpriority="high"></script>
+ ',
+ ),
+ );
+ }
+
+ /**
+ * Tests a higher fetchpriority on a dependent script module causes the fetchpriority of a dependency script module to be bumped.
+ *
+ * @ticket 61734
+ *
+ * @covers WP_Scripts::get_dependents
+ * @covers WP_Scripts::get_highest_fetchpriority_with_dependents
+ * @covers WP_Scripts::do_item
+ *
+ * @dataProvider data_provider_to_test_fetchpriority_bumping
+ */
+ public function test_fetchpriority_bumping( array $enqueues, string $expected ) {
+ wp_register_script( 'bajo', '/bajo.js', array(), null, array( 'fetchpriority' => 'low' ) );
+ wp_register_script( 'auto', '/auto.js', array( 'bajo' ), null, array( 'fetchpriority' => 'auto' ) );
+ wp_register_script( 'alto', '/alto.js', array( 'auto' ), null, array( 'fetchpriority' => 'high' ) );
+
+ foreach ( $enqueues as $enqueue ) {
+ wp_enqueue_script( $enqueue );
+ }
+
+ $actual = get_echo( 'wp_print_scripts' );
+ $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
+ }
+
+ /**
+ * Tests bumping fetchpriority with complex dependency graph.
+ *
+ * @ticket 61734
+ * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3280065818
+ *
+ * @covers WP_Scripts::get_dependents
+ * @covers WP_Scripts::get_highest_fetchpriority_with_dependents
+ * @covers WP_Scripts::do_item
+ */
+ public function test_fetchpriority_bumping_a_to_z() {
+ wp_register_script( 'a', '/a.js', array( 'b' ), null, array( 'fetchpriority' => 'low' ) );
+ wp_register_script( 'b', '/b.js', array( 'c' ), null, array( 'fetchpriority' => 'auto' ) );
+ wp_register_script( 'c', '/c.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'auto' ) );
+ wp_register_script( 'd', '/d.js', array( 'z' ), null, array( 'fetchpriority' => 'high' ) );
+ wp_register_script( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'auto' ) );
+
+ wp_register_script( 'x', '/x.js', array( 'd', 'y' ), null, array( 'fetchpriority' => 'high' ) );
+ wp_register_script( 'y', '/y.js', array( 'z' ), null, array( 'fetchpriority' => 'auto' ) );
+ wp_register_script( 'z', '/z.js', array(), null, array( 'fetchpriority' => 'auto' ) );
+
+ wp_enqueue_script( 'a' );
+ wp_enqueue_script( 'x' );
+
+ $actual = get_echo( 'wp_print_scripts' );
+ $expected = '
+ <script type="text/javascript" src="/z.js" id="z-js" fetchpriority="high" data-wp-fetchpriority="auto"></script>
+ <script type="text/javascript" src="/d.js" id="d-js" fetchpriority="high"></script>
+ <script type="text/javascript" src="/e.js" id="e-js"></script>
+ <script type="text/javascript" src="/c.js" id="c-js"></script>
+ <script type="text/javascript" src="/b.js" id="b-js"></script>
+ <script type="text/javascript" src="/a.js" id="a-js" fetchpriority="low"></script>
+ <script type="text/javascript" src="/y.js" id="y-js" fetchpriority="high" data-wp-fetchpriority="auto"></script>
+ <script type="text/javascript" src="/x.js" id="x-js" fetchpriority="high"></script>
+ ';
+ $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
+ }
+
+ /**
+ * Tests that printing a script without enqueueing has the same output as when it is enqueued.
+ *
+ * @ticket 61734
+ *
+ * @covers WP_Scripts::do_item
+ * @covers WP_Scripts::do_items
+ * @covers ::wp_default_scripts
+ *
+ * @dataProvider data_provider_enqueue_or_not_to_enqueue
+ */
+ public function test_printing_default_script_comment_reply_enqueued_or_not_enqueued( bool $enqueue ) {
+ $wp_scripts = wp_scripts();
+ wp_default_scripts( $wp_scripts );
+
+ $this->assertArrayHasKey( 'comment-reply', $wp_scripts->registered );
+ $wp_scripts->registered['comment-reply']->ver = null;
+ $this->assertArrayHasKey( 'fetchpriority', $wp_scripts->registered['comment-reply']->extra );
+ $this->assertSame( 'low', $wp_scripts->registered['comment-reply']->extra['fetchpriority'] );
+ $this->assertArrayHasKey( 'strategy', $wp_scripts->registered['comment-reply']->extra );
+ $this->assertSame( 'async', $wp_scripts->registered['comment-reply']->extra['strategy'] );
+ if ( $enqueue ) {
+ wp_enqueue_script( 'comment-reply' );
+ $markup = get_echo( array( $wp_scripts, 'do_items' ), array( false ) );
+ } else {
+ $markup = get_echo( array( $wp_scripts, 'do_items' ), array( array( 'comment-reply' ) ) );
+ }
+
+ $this->assertEqualHTML(
+ sprintf(
+ '<script type="text/javascript" src="%s" id="comment-reply-js" async="async" data-wp-strategy="async" fetchpriority="low"></script>',
+ includes_url( 'js/comment-reply.js' )
+ ),
+ $markup
+ );
+ }
+
+ /**
+ * Data provider for test_default_scripts_comment_reply_not_enqueued.
+ *
+ * @return array[]
+ */
+ public static function data_provider_enqueue_or_not_to_enqueue(): array {
+ return array(
+ 'not_enqueued' => array(
+ false,
+ ),
+ 'enqueued' => array(
+ true,
+ ),
+ );
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Tests that scripts registered as defer become blocking when their dependents chain are all blocking.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @ticket 12009
</span></span></pre></div>
<a id="trunktestsphpunittestsscriptmoduleswpScriptModulesphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/script-modules/wpScriptModules.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/script-modules/wpScriptModules.php 2025-10-14 00:10:31 UTC (rev 60930)
+++ trunk/tests/phpunit/tests/script-modules/wpScriptModules.php 2025-10-14 05:45:17 UTC (rev 60931)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -67,9 +67,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $id = preg_replace( '/-js-module$/', '', (string) $p->get_attribute( 'id' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> $fetchpriority = $p->get_attribute( 'fetchpriority' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $modules[ $id ] = array(
- 'url' => $p->get_attribute( 'src' ),
- 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $modules[ $id ] = array_merge(
+ array(
+ 'url' => $p->get_attribute( 'src' ),
+ 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
+ ),
+ ...array_map(
+ static function ( $attribute_name ) use ( $p ) {
+ return array( $attribute_name => $p->get_attribute( $attribute_name ) );
+ },
+ $p->get_attribute_names_with_prefix( 'data-' )
+ )
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -112,9 +120,17 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $id = preg_replace( '/-js-modulepreload$/', '', $p->get_attribute( 'id' ) );
</span><span class="cx" style="display: block; padding: 0 10px"> $fetchpriority = $p->get_attribute( 'fetchpriority' );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $preloads[ $id ] = array(
- 'url' => $p->get_attribute( 'href' ),
- 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $preloads[ $id ] = array_merge(
+ array(
+ 'url' => $p->get_attribute( 'href' ),
+ 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
+ ),
+ ...array_map(
+ static function ( $attribute_name ) use ( $p ) {
+ return array( $attribute_name => $p->get_attribute( $attribute_name ) );
+ },
+ $p->get_attribute_names_with_prefix( 'data-' )
+ )
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -271,15 +287,17 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'preload_links' => array(
</span><span class="cx" style="display: block; padding: 0 10px"> 'b-dep' => array(
</span><span class="cx" style="display: block; padding: 0 10px"> 'url' => '/b-dep.js?ver=99.9.9',
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'fetchpriority' => 'auto',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'fetchpriority' => 'low', // Propagates from 'b'.
</ins><span class="cx" style="display: block; padding: 0 10px"> ),
</span><span class="cx" style="display: block; padding: 0 10px"> 'c-dep' => array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'url' => '/c-static.js?ver=99.9.9',
- 'fetchpriority' => 'low',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'url' => '/c-static.js?ver=99.9.9',
+ 'fetchpriority' => 'auto', // Not 'low' because the dependent script 'c' has a fetchpriority of 'auto'.
+ 'data-wp-fetchpriority' => 'low',
</ins><span class="cx" style="display: block; padding: 0 10px"> ),
</span><span class="cx" style="display: block; padding: 0 10px"> 'c-static-dep' => array(
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- 'url' => '/c-static-dep.js?ver=99.9.9',
- 'fetchpriority' => 'high',
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ 'url' => '/c-static-dep.js?ver=99.9.9',
+ 'fetchpriority' => 'auto', // Propagated from 'c'.
+ 'data-wp-fetchpriority' => 'high',
</ins><span class="cx" style="display: block; padding: 0 10px"> ),
</span><span class="cx" style="display: block; padding: 0 10px"> 'd-static-dep' => array(
</span><span class="cx" style="display: block; padding: 0 10px"> 'url' => '/d-static-dep.js?ver=99.9.9',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -732,6 +750,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'import' => 'dynamic',
</span><span class="cx" style="display: block; padding: 0 10px"> ),
</span><span class="cx" style="display: block; padding: 0 10px"> )
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // Note: The default fetchpriority=auto is upgraded to high because the dependent script module 'static-dep' has a high fetch priority.
</ins><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> $this->script_modules->register(
</span><span class="cx" style="display: block; padding: 0 10px"> 'static-dep',
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -759,9 +778,9 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $this->assertCount( 2, $preloaded_script_modules );
</span><span class="cx" style="display: block; padding: 0 10px"> $this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep']['url'] );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->assertSame( 'high', $preloaded_script_modules['static-dep']['fetchpriority'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->assertSame( 'auto', $preloaded_script_modules['static-dep']['fetchpriority'] ); // Not 'high'
</ins><span class="cx" style="display: block; padding: 0 10px"> $this->assertStringStartsWith( '/nested-static-dep.js', $preloaded_script_modules['nested-static-dep']['url'] );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->assertSame( 'auto', $preloaded_script_modules['nested-static-dep']['fetchpriority'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->assertSame( 'auto', $preloaded_script_modules['nested-static-dep']['fetchpriority'] ); // Auto because the enqueued script foo has the fetchpriority of auto.
</ins><span class="cx" style="display: block; padding: 0 10px"> $this->assertArrayNotHasKey( 'dynamic-dep', $preloaded_script_modules );
</span><span class="cx" style="display: block; padding: 0 10px"> $this->assertArrayNotHasKey( 'nested-dynamic-dep', $preloaded_script_modules );
</span><span class="cx" style="display: block; padding: 0 10px"> $this->assertArrayNotHasKey( 'no-dep', $preloaded_script_modules );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -971,7 +990,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> $preloaded_script_modules = $this->get_preloaded_script_modules();
</span><span class="cx" style="display: block; padding: 0 10px"> $this->assertSame( '/dep.js?ver=2.0', $preloaded_script_modules['dep']['url'] );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- $this->assertSame( 'high', $preloaded_script_modules['dep']['fetchpriority'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $this->assertSame( 'auto', $preloaded_script_modules['dep']['fetchpriority'] ); // Because 'foo' has a priority of 'auto'.
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1344,6 +1363,319 @@
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * Data provider.
+ *
+ * @return array<string, array{enqueues: string[], expected: array}>
+ */
+ public function data_provider_to_test_fetchpriority_bumping(): array {
+ return array(
+ 'enqueue_bajo' => array(
+ 'enqueues' => array( 'bajo' ),
+ 'expected' => array(
+ 'preload_links' => array(),
+ 'script_tags' => array(
+ 'bajo' => array(
+ 'url' => '/bajo.js',
+ 'fetchpriority' => 'high',
+ 'data-wp-fetchpriority' => 'low',
+ ),
+ ),
+ 'import_map' => array(
+ 'dyno' => '/dyno.js',
+ ),
+ ),
+ ),
+ 'enqueue_auto' => array(
+ 'enqueues' => array( 'auto' ),
+ 'expected' => array(
+ 'preload_links' => array(
+ 'bajo' => array(
+ 'url' => '/bajo.js',
+ 'fetchpriority' => 'auto',
+ 'data-wp-fetchpriority' => 'low',
+ ),
+ ),
+ 'script_tags' => array(
+ 'auto' => array(
+ 'url' => '/auto.js',
+ 'fetchpriority' => 'high',
+ 'data-wp-fetchpriority' => 'auto',
+ ),
+ ),
+ 'import_map' => array(
+ 'bajo' => '/bajo.js',
+ 'dyno' => '/dyno.js',
+ ),
+ ),
+ ),
+ 'enqueue_alto' => array(
+ 'enqueues' => array( 'alto' ),
+ 'expected' => array(
+ 'preload_links' => array(
+ 'auto' => array(
+ 'url' => '/auto.js',
+ 'fetchpriority' => 'high',
+ ),
+ 'bajo' => array(
+ 'url' => '/bajo.js',
+ 'fetchpriority' => 'high',
+ 'data-wp-fetchpriority' => 'low',
+ ),
+ ),
+ 'script_tags' => array(
+ 'alto' => array(
+ 'url' => '/alto.js',
+ 'fetchpriority' => 'high',
+ ),
+ ),
+ 'import_map' => array(
+ 'auto' => '/auto.js',
+ 'bajo' => '/bajo.js',
+ 'dyno' => '/dyno.js',
+ ),
+ ),
+ ),
+ );
+ }
+
+ /**
+ * Tests a higher fetchpriority on a dependent script module causes the fetchpriority of a dependency script module to be bumped.
+ *
+ * @ticket 61734
+ *
+ * @covers WP_Script_Modules::print_enqueued_script_modules
+ * @covers WP_Script_Modules::get_dependents
+ * @covers WP_Script_Modules::get_recursive_dependents
+ * @covers WP_Script_Modules::get_highest_fetchpriority
+ * @covers WP_Script_Modules::print_script_module_preloads
+ *
+ * @dataProvider data_provider_to_test_fetchpriority_bumping
+ */
+ public function test_fetchpriority_bumping( array $enqueues, array $expected ) {
+ $this->script_modules->register(
+ 'dyno',
+ '/dyno.js',
+ array(),
+ null,
+ array( 'fetchpriority' => 'low' ) // This won't show up anywhere since it is a dynamic import dependency.
+ );
+
+ $this->script_modules->register(
+ 'bajo',
+ '/bajo.js',
+ array(
+ array(
+ 'id' => 'dyno',
+ 'import' => 'dynamic',
+ ),
+ ),
+ null,
+ array( 'fetchpriority' => 'low' )
+ );
+
+ $this->script_modules->register(
+ 'auto',
+ '/auto.js',
+ array(
+ array(
+ 'id' => 'bajo',
+ 'import' => 'static',
+ ),
+ ),
+ null,
+ array( 'fetchpriority' => 'auto' )
+ );
+ $this->script_modules->register(
+ 'alto',
+ '/alto.js',
+ array( 'auto' ),
+ null,
+ array( 'fetchpriority' => 'high' )
+ );
+
+ foreach ( $enqueues as $enqueue ) {
+ $this->script_modules->enqueue( $enqueue );
+ }
+
+ $actual = array(
+ 'preload_links' => $this->get_preloaded_script_modules(),
+ 'script_tags' => $this->get_enqueued_script_modules(),
+ 'import_map' => $this->get_import_map(),
+ );
+ $this->assertSame(
+ $expected,
+ $actual,
+ "Snapshot:\n" . var_export( $actual, true )
+ );
+ }
+
+ /**
+ * Tests bumping fetchpriority with complex dependency graph.
+ *
+ * @ticket 61734
+ * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3280065818
+ *
+ * @covers WP_Script_Modules::print_enqueued_script_modules
+ * @covers WP_Script_Modules::get_dependents
+ * @covers WP_Script_Modules::get_recursive_dependents
+ * @covers WP_Script_Modules::get_highest_fetchpriority
+ * @covers WP_Script_Modules::print_script_module_preloads
+ */
+ public function test_fetchpriority_bumping_a_to_z() {
+ wp_register_script_module( 'a', '/a.js', array( 'b' ), null, array( 'fetchpriority' => 'low' ) );
+ wp_register_script_module( 'b', '/b.js', array( 'c' ), null, array( 'fetchpriority' => 'auto' ) );
+ wp_register_script_module( 'c', '/c.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'auto' ) );
+ wp_register_script_module( 'd', '/d.js', array( 'z' ), null, array( 'fetchpriority' => 'high' ) );
+ wp_register_script_module( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'auto' ) );
+
+ wp_register_script_module( 'x', '/x.js', array( 'd', 'y' ), null, array( 'fetchpriority' => 'high' ) );
+ wp_register_script_module( 'y', '/y.js', array( 'z' ), null, array( 'fetchpriority' => 'auto' ) );
+ wp_register_script_module( 'z', '/z.js', array(), null, array( 'fetchpriority' => 'auto' ) );
+
+ // The fetch priorities are derived from these enqueued dependents.
+ wp_enqueue_script_module( 'a' );
+ wp_enqueue_script_module( 'x' );
+
+ $actual = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
+ $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
+ $expected = '
+ <link rel="modulepreload" href="/b.js" id="b-js-modulepreload" fetchpriority="low">
+ <link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="low">
+ <link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="high">
+ <link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="low">
+ <link rel="modulepreload" href="/z.js" id="z-js-modulepreload" fetchpriority="high">
+ <link rel="modulepreload" href="/y.js" id="y-js-modulepreload" fetchpriority="high">
+ <script type="module" src="/a.js" id="a-js-module" fetchpriority="low"></script>
+ <script type="module" src="/x.js" id="x-js-module" fetchpriority="high"></script>
+ ';
+ $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
+ }
+
+ /**
+ * Tests bumping fetchpriority with complex dependency graph.
+ *
+ * @ticket 61734
+ * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3284266884
+ *
+ * @covers WP_Script_Modules::print_enqueued_script_modules
+ * @covers WP_Script_Modules::get_dependents
+ * @covers WP_Script_Modules::get_recursive_dependents
+ * @covers WP_Script_Modules::get_highest_fetchpriority
+ * @covers WP_Script_Modules::print_script_module_preloads
+ */
+ public function test_fetchpriority_propagation() {
+ // The high fetchpriority for this module will be disregarded because its enqueued dependent has a non-high priority.
+ wp_register_script_module( 'a', '/a.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'high' ) );
+ wp_register_script_module( 'b', '/b.js', array( 'e' ), null );
+ wp_register_script_module( 'c', '/c.js', array( 'e', 'f' ), null );
+ wp_register_script_module( 'd', '/d.js', array(), null );
+ // The low fetchpriority for this module will be disregarded because its enqueued dependent has a non-low priority.
+ wp_register_script_module( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'low' ) );
+ wp_register_script_module( 'f', '/f.js', array(), null );
+
+ wp_register_script_module( 'x', '/x.js', array( 'a' ), null, array( 'fetchpriority' => 'low' ) );
+ wp_register_script_module( 'y', '/y.js', array( 'b' ), null, array( 'fetchpriority' => 'auto' ) );
+ wp_register_script_module( 'z', '/z.js', array( 'c' ), null, array( 'fetchpriority' => 'high' ) );
+
+ wp_enqueue_script_module( 'x' );
+ wp_enqueue_script_module( 'y' );
+ wp_enqueue_script_module( 'z' );
+
+ $actual = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
+ $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
+ $expected = '
+ <link rel="modulepreload" href="/a.js" id="a-js-modulepreload" fetchpriority="low" data-wp-fetchpriority="high">
+ <link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="low">
+ <link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
+ <link rel="modulepreload" href="/b.js" id="b-js-modulepreload">
+ <link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="high">
+ <link rel="modulepreload" href="/f.js" id="f-js-modulepreload" fetchpriority="high">
+ <script type="module" src="/x.js" id="x-js-module" fetchpriority="low"></script>
+ <script type="module" src="/y.js" id="y-js-module"></script>
+ <script type="module" src="/z.js" id="z-js-module" fetchpriority="high"></script>
+ ';
+ $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
+ }
+
+ /**
+ * Tests that default script modules are printed as expected.
+ *
+ * @covers ::wp_default_script_modules
+ * @covers WP_Script_Modules::print_script_module_preloads
+ * @covers WP_Script_Modules::print_enqueued_script_modules
+ */
+ public function test_default_script_modules() {
+ wp_default_script_modules();
+ wp_enqueue_script_module( '@wordpress/a11y' );
+ wp_enqueue_script_module( '@wordpress/block-library/navigation/view' );
+
+ $actual = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
+ $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
+
+ $actual = $this->normalize_markup_for_snapshot( $actual );
+
+ $expected = '
+ <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="low">
+ <script type="module" src="/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-module" fetchpriority="low"></script>
+ <script type="module" src="/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-module" fetchpriority="low"></script>
+ ';
+ $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
+ }
+
+ /**
+ * Tests that a dependent with high priority for default script modules with a low fetch priority are printed as expected.
+ *
+ * @covers ::wp_default_script_modules
+ * @covers WP_Script_Modules::print_script_module_preloads
+ * @covers WP_Script_Modules::print_enqueued_script_modules
+ */
+ public function test_dependent_of_default_script_modules() {
+ wp_default_script_modules();
+ wp_enqueue_script_module(
+ 'super-important',
+ '/super-important-module.js',
+ array( '@wordpress/a11y', '@wordpress/block-library/navigation/view' ),
+ null,
+ array( 'fetchpriority' => 'high' )
+ );
+
+ $actual = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
+ $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
+
+ $actual = $this->normalize_markup_for_snapshot( $actual );
+
+ $expected = '
+ <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
+ <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/block-library/navigation/view.min.js" id="@wordpress/block-library/navigation/view-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
+ <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
+ <script type="module" src="/super-important-module.js" id="super-important-js-module" fetchpriority="high"></script>
+ ';
+ $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
+ }
+
+ /**
+ * Normalizes markup for snapshot.
+ *
+ * @param string $markup Markup.
+ * @return string Normalized markup.
+ */
+ private function normalize_markup_for_snapshot( string $markup ): string {
+ $processor = new WP_HTML_Tag_Processor( $markup );
+ $clean_url = static function ( string $url ): string {
+ $url = preg_replace( '#^https?://[^/]+#', '', $url );
+ return remove_query_arg( 'ver', $url );
+ };
+ while ( $processor->next_tag() ) {
+ if ( 'LINK' === $processor->get_tag() && is_string( $processor->get_attribute( 'href' ) ) ) {
+ $processor->set_attribute( 'href', $clean_url( $processor->get_attribute( 'href' ) ) );
+ } elseif ( 'SCRIPT' === $processor->get_tag() && is_string( $processor->get_attribute( 'src' ) ) ) {
+ $processor->set_attribute( 'src', $clean_url( $processor->get_attribute( 'src' ) ) );
+ }
+ }
+ return $processor->get_updated_html();
+ }
+
+ /**
</ins><span class="cx" style="display: block; padding: 0 10px"> * Tests that directly manipulating the queue works as expected.
</span><span class="cx" style="display: block; padding: 0 10px"> *
</span><span class="cx" style="display: block; padding: 0 10px"> * @ticket 63676
</span></span></pre>
</div>
</div>
</body>
</html>