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