Make WordPress Core

Changeset 60931


Ignore:
Timestamp:
10/14/2025 05:45:17 AM (10 months ago)
Author:
westonruter
Message:

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 #12009.

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 [60704].

Props westonruter, jonsurrell.
Fixes #61734.

Location:
trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-dependency.php

    r60656 r60931  
    5151         *
    5252         * @since 2.6.0
    53          * @var bool|string
     53         * @var string|false|null
    5454         */
    5555        public $ver = false;
  • trunk/src/wp-includes/class-wp-script-modules.php

    r60930 r60931  
    4141         */
    4242        private $a11y_available = false;
     43
     44        /**
     45         * Holds a mapping of dependents (as IDs) for a given script ID.
     46         * Used to optimize recursive dependency tree checks.
     47         *
     48         * @since 6.9.0
     49         * @var array<string, string[]>
     50         */
     51        private $dependents_map = array();
    4352
    4453        /**
     
    271280
    272281        /**
     282         * Gets the highest fetch priority for the provided script IDs.
     283         *
     284         * @since 6.9.0
     285         *
     286         * @param string[] $ids Script module IDs.
     287         * @return string Highest fetch priority for the provided script module IDs.
     288         */
     289        private function get_highest_fetchpriority( array $ids ): string {
     290                static $priorities   = array(
     291                        'low',
     292                        'auto',
     293                        'high',
     294                );
     295                $high_priority_index = count( $priorities ) - 1;
     296
     297                $highest_priority_index = 0;
     298                foreach ( $ids as $id ) {
     299                        if ( isset( $this->registered[ $id ] ) ) {
     300                                $highest_priority_index = max(
     301                                        $highest_priority_index,
     302                                        array_search( $this->registered[ $id ]['fetchpriority'], $priorities, true )
     303                                );
     304                                if ( $high_priority_index === $highest_priority_index ) {
     305                                        break;
     306                                }
     307                        }
     308                }
     309
     310                return $priorities[ $highest_priority_index ];
     311        }
     312
     313        /**
    273314         * Prints the enqueued script modules using script tags with type="module"
    274315         * attributes.
     
    283324                                'id'   => $id . '-js-module',
    284325                        );
    285                         if ( 'auto' !== $script_module['fetchpriority'] ) {
    286                                 $args['fetchpriority'] = $script_module['fetchpriority'];
     326
     327                        $dependents    = $this->get_recursive_dependents( $id );
     328                        $fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $dependents ) );
     329                        if ( 'auto' !== $fetchpriority ) {
     330                                $args['fetchpriority'] = $fetchpriority;
     331                        }
     332                        if ( $fetchpriority !== $script_module['fetchpriority'] ) {
     333                                $args['data-wp-fetchpriority'] = $script_module['fetchpriority'];
    287334                        }
    288335                        wp_print_script_tag( $args );
     
    291338
    292339        /**
    293          * Prints the the static dependencies of the enqueued script modules using
     340         * Prints the static dependencies of the enqueued script modules using
    294341         * link tags with rel="modulepreload" attributes.
    295342         *
     
    302349                        // Don't preload if it's marked for enqueue.
    303350                        if ( ! in_array( $id, $this->queue, true ) ) {
    304                                 echo sprintf(
    305                                         '<link rel="modulepreload" href="%s" id="%s"%s>',
     351                                $enqueued_dependents   = array_intersect( $this->get_recursive_dependents( $id ), $this->queue );
     352                                $highest_fetchpriority = $this->get_highest_fetchpriority( $enqueued_dependents );
     353                                printf(
     354                                        '<link rel="modulepreload" href="%s" id="%s"',
    306355                                        esc_url( $this->get_src( $id ) ),
    307                                         esc_attr( $id . '-js-modulepreload' ),
    308                                         'auto' !== $script_module['fetchpriority'] ? sprintf( ' fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) ) : ''
     356                                        esc_attr( $id . '-js-modulepreload' )
    309357                                );
     358                                if ( 'auto' !== $highest_fetchpriority ) {
     359                                        printf( ' fetchpriority="%s"', esc_attr( $highest_fetchpriority ) );
     360                                }
     361                                if ( $highest_fetchpriority !== $script_module['fetchpriority'] && 'auto' !== $script_module['fetchpriority'] ) {
     362                                        printf( ' data-wp-fetchpriority="%s"', esc_attr( $script_module['fetchpriority'] ) );
     363                                }
     364                                echo ">\n";
    310365                        }
    311366                }
     
    375430         * @return array[] List of dependencies, keyed by script module identifier.
    376431         */
    377         private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ) {
     432        private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
    378433                return array_reduce(
    379434                        $ids,
    380435                        function ( $dependency_script_modules, $id ) use ( $import_types ) {
    381436                                $dependencies = array();
    382                                 foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
    383                                         if (
    384                                         in_array( $dependency['import'], $import_types, true ) &&
    385                                         isset( $this->registered[ $dependency['id'] ] ) &&
    386                                         ! isset( $dependency_script_modules[ $dependency['id'] ] )
    387                                         ) {
    388                                                 $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
     437                                if ( isset( $this->registered[ $id ] ) ) {
     438                                        foreach ( $this->registered[ $id ]['dependencies'] as $dependency ) {
     439                                                if (
     440                                                        in_array( $dependency['import'], $import_types, true ) &&
     441                                                        isset( $this->registered[ $dependency['id'] ] ) &&
     442                                                        ! isset( $dependency_script_modules[ $dependency['id'] ] )
     443                                                ) {
     444                                                        $dependencies[ $dependency['id'] ] = $this->registered[ $dependency['id'] ];
     445                                                }
    389446                                        }
    390447                                }
     
    393450                        array()
    394451                );
     452        }
     453
     454        /**
     455         * Gets all dependents of a script module.
     456         *
     457         * This is not recursive.
     458         *
     459         * @since 6.9.0
     460         *
     461         * @see WP_Scripts::get_dependents()
     462         *
     463         * @param string $id The script ID.
     464         * @return string[] Script module IDs.
     465         */
     466        private function get_dependents( string $id ): array {
     467                // Check if dependents map for the handle in question is present. If so, use it.
     468                if ( isset( $this->dependents_map[ $id ] ) ) {
     469                        return $this->dependents_map[ $id ];
     470                }
     471
     472                $dependents = array();
     473
     474                // Iterate over all registered scripts, finding dependents of the script passed to this method.
     475                foreach ( $this->registered as $registered_id => $args ) {
     476                        if ( in_array( $id, wp_list_pluck( $args['dependencies'], 'id' ), true ) ) {
     477                                $dependents[] = $registered_id;
     478                        }
     479                }
     480
     481                // Add the module's dependents to the map to ease future lookups.
     482                $this->dependents_map[ $id ] = $dependents;
     483
     484                return $dependents;
     485        }
     486
     487        /**
     488         * Gets all recursive dependents of a script module.
     489         *
     490         * @since 6.9.0
     491         *
     492         * @see WP_Scripts::get_dependents()
     493         *
     494         * @param string $id The script ID.
     495         * @return string[] Script module IDs.
     496         */
     497        private function get_recursive_dependents( string $id ): array {
     498                $get = function ( string $id, array $checked = array() ) use ( &$get ): array {
     499
     500                        // If by chance an unregistered script module is checked or there is a recursive dependency, return early.
     501                        if ( ! isset( $this->registered[ $id ] ) || isset( $checked[ $id ] ) ) {
     502                                return array();
     503                        }
     504
     505                        // Mark this script module as checked to guard against infinite recursion.
     506                        $checked[ $id ] = true;
     507
     508                        $dependents = array();
     509                        foreach ( $this->get_dependents( $id ) as $dependent ) {
     510                                $dependents = array_merge(
     511                                        $dependents,
     512                                        array( $dependent ),
     513                                        $get( $dependent, $checked )
     514                                );
     515                        }
     516
     517                        return $dependents;
     518                };
     519
     520                return array_unique( $get( $id ) );
    395521        }
    396522
  • trunk/src/wp-includes/class-wp-scripts.php

    r60722 r60931  
    128128         *
    129129         * @since 6.3.0
    130          * @var array
     130         * @var array<string, string[]>
    131131         */
    132132        private $dependents_map = array();
     
    440440                        $attr['data-wp-strategy'] = $intended_strategy;
    441441                }
    442                 if ( isset( $obj->extra['fetchpriority'] ) && 'auto' !== $obj->extra['fetchpriority'] && $this->is_valid_fetchpriority( $obj->extra['fetchpriority'] ) ) {
    443                         $attr['fetchpriority'] = $obj->extra['fetchpriority'];
    444                 }
     442
     443                // Determine fetchpriority.
     444                $original_fetchpriority = isset( $obj->extra['fetchpriority'] ) ? $obj->extra['fetchpriority'] : null;
     445                if ( null === $original_fetchpriority || ! $this->is_valid_fetchpriority( $original_fetchpriority ) ) {
     446                        $original_fetchpriority = 'auto';
     447                }
     448                $actual_fetchpriority = $this->get_highest_fetchpriority_with_dependents( $handle );
     449                if ( null === $actual_fetchpriority ) {
     450                        // If null, it's likely this script was not explicitly enqueued, so in this case use the original priority.
     451                        $actual_fetchpriority = $original_fetchpriority;
     452                }
     453                if ( is_string( $actual_fetchpriority ) && 'auto' !== $actual_fetchpriority ) {
     454                        $attr['fetchpriority'] = $actual_fetchpriority;
     455                }
     456                if ( $original_fetchpriority !== $actual_fetchpriority ) {
     457                        $attr['data-wp-fetchpriority'] = $original_fetchpriority;
     458                }
     459
    445460                $tag  = $translations . $ie_conditional_prefix . $before_script;
    446461                $tag .= wp_get_script_tag( $attr );
     
    899914         * Gets all dependents of a script.
    900915         *
     916         * This is not recursive.
     917         *
    901918         * @since 6.3.0
    902919         *
     
    10501067
    10511068                return $eligible_strategies;
     1069        }
     1070
     1071        /**
     1072         * Gets the highest fetch priority for a given script and all of its dependent scripts.
     1073         *
     1074         * @since 6.9.0
     1075         * @see self::filter_eligible_strategies()
     1076         * @see WP_Script_Modules::get_highest_fetchpriority_with_dependents()
     1077         *
     1078         * @param string              $handle  Script module ID.
     1079         * @param array<string, true> $checked Optional. An array of already checked script handles, used to avoid recursive loops.
     1080         * @return string|null Highest fetch priority for the script and its dependents.
     1081         */
     1082        private function get_highest_fetchpriority_with_dependents( string $handle, array $checked = array() ): ?string {
     1083                // If there is a recursive dependency, return early.
     1084                if ( isset( $checked[ $handle ] ) ) {
     1085                        return null;
     1086                }
     1087
     1088                // Mark this handle as checked to guard against infinite recursion.
     1089                $checked[ $handle ] = true;
     1090
     1091                // Abort if the script is not enqueued or a dependency of an enqueued script.
     1092                if ( ! $this->query( $handle, 'enqueued' ) ) {
     1093                        return null;
     1094                }
     1095
     1096                $fetchpriority = $this->get_data( $handle, 'fetchpriority' );
     1097                if ( ! $this->is_valid_fetchpriority( $fetchpriority ) ) {
     1098                        $fetchpriority = 'auto';
     1099                }
     1100
     1101                static $priorities   = array(
     1102                        'low',
     1103                        'auto',
     1104                        'high',
     1105                );
     1106                $high_priority_index = count( $priorities ) - 1;
     1107
     1108                $highest_priority_index = (int) array_search( $fetchpriority, $priorities, true );
     1109                if ( $highest_priority_index !== $high_priority_index ) {
     1110                        foreach ( $this->get_dependents( $handle ) as $dependent_handle ) {
     1111                                $dependent_priority = $this->get_highest_fetchpriority_with_dependents( $dependent_handle, $checked );
     1112                                if ( is_string( $dependent_priority ) ) {
     1113                                        $highest_priority_index = max(
     1114                                                $highest_priority_index,
     1115                                                (int) array_search( $dependent_priority, $priorities, true )
     1116                                        );
     1117                                        if ( $highest_priority_index === $high_priority_index ) {
     1118                                                break;
     1119                                        }
     1120                                }
     1121                        }
     1122                }
     1123
     1124                return $priorities[ $highest_priority_index ];
    10521125        }
    10531126
  • trunk/src/wp-includes/script-modules.php

    r60704 r60931  
    182182                }
    183183
    184                 // 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.
     184                /*
     185                 * The Interactivity API is designed with server-side rendering as its primary goal, so all of its script modules
     186                 * should be loaded with low fetchpriority since they should not be needed in the critical rendering path.
     187                 * Also, the @wordpress/a11y script module is intended to be used as a dynamic import dependency, in which case
     188                 * the fetchpriority is irrelevant. See <https://make.wordpress.org/core/2024/10/14/updates-to-script-modules-in-6-7/>.
     189                 * However, in case it is added as a static import dependency, the fetchpriority is explicitly set to be 'low'
     190                 * since the module should not be involved in the critical rendering path, and if it is, its fetchpriority will
     191                 * be bumped to match the fetchpriority of the dependent script.
     192                 */
    185193                $args = array();
    186                 if ( str_starts_with( $script_module_id, '@wordpress/interactivity' ) || str_starts_with( $script_module_id, '@wordpress/block-library' ) ) {
     194                if (
     195                        str_starts_with( $script_module_id, '@wordpress/interactivity' ) ||
     196                        str_starts_with( $script_module_id, '@wordpress/block-library' ) ||
     197                        '@wordpress/a11y' === $script_module_id
     198                ) {
    187199                        $args['fetchpriority'] = 'low';
    188200                }
  • trunk/tests/phpunit/tests/dependencies/scripts.php

    r60729 r60931  
    12331233                        )
    12341234                );
     1235                // Note: All of these scripts have fetchpriority=high because the leaf dependent script has that fetch priority.
    12351236                $output    = get_echo( 'wp_print_scripts' );
    1236                 $expected  = "<script type='text/javascript' src='/main-script-d4.js' id='main-script-d4-js' defer='defer' data-wp-strategy='defer'></script>\n";
    1237                 $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";
    1238                 $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";
     1237                $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";
     1238                $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";
     1239                $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";
    12391240                $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";
    12401241
     
    13421343                wp_register_script( 'alias', false, array(), null, array( 'fetchpriority' => 'low' ) );
    13431344                $this->assertArrayNotHasKey( 'fetchpriority', wp_scripts()->registered['alias']->extra );
     1345        }
     1346
     1347        /**
     1348         * Data provider.
     1349         *
     1350         * @return array<string, array{enqueues: string[], expected: string}>
     1351         */
     1352        public function data_provider_to_test_fetchpriority_bumping(): array {
     1353                return array(
     1354                        'enqueue_bajo' => array(
     1355                                'enqueues' => array( 'bajo' ),
     1356                                'expected' => '<script fetchpriority="low" id="bajo-js" src="/bajo.js" type="text/javascript"></script>',
     1357                        ),
     1358                        'enqueue_auto' => array(
     1359                                'enqueues' => array( 'auto' ),
     1360                                'expected' => '
     1361                                        <script type="text/javascript" src="/bajo.js" id="bajo-js" data-wp-fetchpriority="low"></script>
     1362                                        <script type="text/javascript" src="/auto.js" id="auto-js"></script>
     1363                                ',
     1364                        ),
     1365                        'enqueue_alto' => array(
     1366                                'enqueues' => array( 'alto' ),
     1367                                'expected' => '
     1368                                        <script type="text/javascript" src="/bajo.js" id="bajo-js" fetchpriority="high" data-wp-fetchpriority="low"></script>
     1369                                        <script type="text/javascript" src="/auto.js" id="auto-js" fetchpriority="high" data-wp-fetchpriority="auto"></script>
     1370                                        <script type="text/javascript" src="/alto.js" id="alto-js" fetchpriority="high"></script>
     1371                                ',
     1372                        ),
     1373                );
     1374        }
     1375
     1376        /**
     1377         * Tests a higher fetchpriority on a dependent script module causes the fetchpriority of a dependency script module to be bumped.
     1378         *
     1379         * @ticket 61734
     1380         *
     1381         * @covers WP_Scripts::get_dependents
     1382         * @covers WP_Scripts::get_highest_fetchpriority_with_dependents
     1383         * @covers WP_Scripts::do_item
     1384         *
     1385         * @dataProvider data_provider_to_test_fetchpriority_bumping
     1386         */
     1387        public function test_fetchpriority_bumping( array $enqueues, string $expected ) {
     1388                wp_register_script( 'bajo', '/bajo.js', array(), null, array( 'fetchpriority' => 'low' ) );
     1389                wp_register_script( 'auto', '/auto.js', array( 'bajo' ), null, array( 'fetchpriority' => 'auto' ) );
     1390                wp_register_script( 'alto', '/alto.js', array( 'auto' ), null, array( 'fetchpriority' => 'high' ) );
     1391
     1392                foreach ( $enqueues as $enqueue ) {
     1393                        wp_enqueue_script( $enqueue );
     1394                }
     1395
     1396                $actual = get_echo( 'wp_print_scripts' );
     1397                $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1398        }
     1399
     1400        /**
     1401         * Tests bumping fetchpriority with complex dependency graph.
     1402         *
     1403         * @ticket 61734
     1404         * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3280065818
     1405         *
     1406         * @covers WP_Scripts::get_dependents
     1407         * @covers WP_Scripts::get_highest_fetchpriority_with_dependents
     1408         * @covers WP_Scripts::do_item
     1409         */
     1410        public function test_fetchpriority_bumping_a_to_z() {
     1411                wp_register_script( 'a', '/a.js', array( 'b' ), null, array( 'fetchpriority' => 'low' ) );
     1412                wp_register_script( 'b', '/b.js', array( 'c' ), null, array( 'fetchpriority' => 'auto' ) );
     1413                wp_register_script( 'c', '/c.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'auto' ) );
     1414                wp_register_script( 'd', '/d.js', array( 'z' ), null, array( 'fetchpriority' => 'high' ) );
     1415                wp_register_script( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'auto' ) );
     1416
     1417                wp_register_script( 'x', '/x.js', array( 'd', 'y' ), null, array( 'fetchpriority' => 'high' ) );
     1418                wp_register_script( 'y', '/y.js', array( 'z' ), null, array( 'fetchpriority' => 'auto' ) );
     1419                wp_register_script( 'z', '/z.js', array(), null, array( 'fetchpriority' => 'auto' ) );
     1420
     1421                wp_enqueue_script( 'a' );
     1422                wp_enqueue_script( 'x' );
     1423
     1424                $actual   = get_echo( 'wp_print_scripts' );
     1425                $expected = '
     1426                        <script type="text/javascript" src="/z.js" id="z-js" fetchpriority="high" data-wp-fetchpriority="auto"></script>
     1427                        <script type="text/javascript" src="/d.js" id="d-js" fetchpriority="high"></script>
     1428                        <script type="text/javascript" src="/e.js" id="e-js"></script>
     1429                        <script type="text/javascript" src="/c.js" id="c-js"></script>
     1430                        <script type="text/javascript" src="/b.js" id="b-js"></script>
     1431                        <script type="text/javascript" src="/a.js" id="a-js" fetchpriority="low"></script>
     1432                        <script type="text/javascript" src="/y.js" id="y-js" fetchpriority="high" data-wp-fetchpriority="auto"></script>
     1433                        <script type="text/javascript" src="/x.js" id="x-js" fetchpriority="high"></script>
     1434                ';
     1435                $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1436        }
     1437
     1438        /**
     1439         * Tests that printing a script without enqueueing has the same output as when it is enqueued.
     1440         *
     1441         * @ticket 61734
     1442         *
     1443         * @covers WP_Scripts::do_item
     1444         * @covers WP_Scripts::do_items
     1445         * @covers ::wp_default_scripts
     1446         *
     1447         * @dataProvider data_provider_enqueue_or_not_to_enqueue
     1448         */
     1449        public function test_printing_default_script_comment_reply_enqueued_or_not_enqueued( bool $enqueue ) {
     1450                $wp_scripts = wp_scripts();
     1451                wp_default_scripts( $wp_scripts );
     1452
     1453                $this->assertArrayHasKey( 'comment-reply', $wp_scripts->registered );
     1454                $wp_scripts->registered['comment-reply']->ver = null;
     1455                $this->assertArrayHasKey( 'fetchpriority', $wp_scripts->registered['comment-reply']->extra );
     1456                $this->assertSame( 'low', $wp_scripts->registered['comment-reply']->extra['fetchpriority'] );
     1457                $this->assertArrayHasKey( 'strategy', $wp_scripts->registered['comment-reply']->extra );
     1458                $this->assertSame( 'async', $wp_scripts->registered['comment-reply']->extra['strategy'] );
     1459                if ( $enqueue ) {
     1460                        wp_enqueue_script( 'comment-reply' );
     1461                        $markup = get_echo( array( $wp_scripts, 'do_items' ), array( false ) );
     1462                } else {
     1463                        $markup = get_echo( array( $wp_scripts, 'do_items' ), array( array( 'comment-reply' ) ) );
     1464                }
     1465
     1466                $this->assertEqualHTML(
     1467                        sprintf(
     1468                                '<script type="text/javascript" src="%s" id="comment-reply-js" async="async" data-wp-strategy="async" fetchpriority="low"></script>',
     1469                                includes_url( 'js/comment-reply.js' )
     1470                        ),
     1471                        $markup
     1472                );
     1473        }
     1474
     1475        /**
     1476         * Data provider for test_default_scripts_comment_reply_not_enqueued.
     1477         *
     1478         * @return array[]
     1479         */
     1480        public static function data_provider_enqueue_or_not_to_enqueue(): array {
     1481                return array(
     1482                        'not_enqueued' => array(
     1483                                false,
     1484                        ),
     1485                        'enqueued'     => array(
     1486                                true,
     1487                        ),
     1488                );
    13441489        }
    13451490
  • trunk/tests/phpunit/tests/script-modules/wpScriptModules.php

    r60930 r60931  
    6868                        $id             = preg_replace( '/-js-module$/', '', (string) $p->get_attribute( 'id' ) );
    6969                        $fetchpriority  = $p->get_attribute( 'fetchpriority' );
    70                         $modules[ $id ] = array(
    71                                 'url'           => $p->get_attribute( 'src' ),
    72                                 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     70                        $modules[ $id ] = array_merge(
     71                                array(
     72                                        'url'           => $p->get_attribute( 'src' ),
     73                                        'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     74                                ),
     75                                ...array_map(
     76                                        static function ( $attribute_name ) use ( $p ) {
     77                                                return array( $attribute_name => $p->get_attribute( $attribute_name ) );
     78                                        },
     79                                        $p->get_attribute_names_with_prefix( 'data-' )
     80                                )
    7381                        );
    7482                }
     
    113121                        $id              = preg_replace( '/-js-modulepreload$/', '', $p->get_attribute( 'id' ) );
    114122                        $fetchpriority   = $p->get_attribute( 'fetchpriority' );
    115                         $preloads[ $id ] = array(
    116                                 'url'           => $p->get_attribute( 'href' ),
    117                                 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     123                        $preloads[ $id ] = array_merge(
     124                                array(
     125                                        'url'           => $p->get_attribute( 'href' ),
     126                                        'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto',
     127                                ),
     128                                ...array_map(
     129                                        static function ( $attribute_name ) use ( $p ) {
     130                                                return array( $attribute_name => $p->get_attribute( $attribute_name ) );
     131                                        },
     132                                        $p->get_attribute_names_with_prefix( 'data-' )
     133                                )
    118134                        );
    119135                }
     
    272288                                        'b-dep'        => array(
    273289                                                'url'           => '/b-dep.js?ver=99.9.9',
    274                                                 'fetchpriority' => 'auto',
     290                                                'fetchpriority' => 'low', // Propagates from 'b'.
    275291                                        ),
    276292                                        'c-dep'        => array(
    277                                                 'url'           => '/c-static.js?ver=99.9.9',
    278                                                 'fetchpriority' => 'low',
     293                                                'url'                   => '/c-static.js?ver=99.9.9',
     294                                                'fetchpriority'         => 'auto', // Not 'low' because the dependent script 'c' has a fetchpriority of 'auto'.
     295                                                'data-wp-fetchpriority' => 'low',
    279296                                        ),
    280297                                        'c-static-dep' => array(
    281                                                 'url'           => '/c-static-dep.js?ver=99.9.9',
    282                                                 'fetchpriority' => 'high',
     298                                                'url'                   => '/c-static-dep.js?ver=99.9.9',
     299                                                'fetchpriority'         => 'auto', // Propagated from 'c'.
     300                                                'data-wp-fetchpriority' => 'high',
    283301                                        ),
    284302                                        'd-static-dep' => array(
     
    733751                                ),
    734752                        )
     753                        // Note: The default fetchpriority=auto is upgraded to high because the dependent script module 'static-dep' has a high fetch priority.
    735754                );
    736755                $this->script_modules->register(
     
    760779                $this->assertCount( 2, $preloaded_script_modules );
    761780                $this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep']['url'] );
    762                 $this->assertSame( 'high', $preloaded_script_modules['static-dep']['fetchpriority'] );
     781                $this->assertSame( 'auto', $preloaded_script_modules['static-dep']['fetchpriority'] ); // Not 'high'
    763782                $this->assertStringStartsWith( '/nested-static-dep.js', $preloaded_script_modules['nested-static-dep']['url'] );
    764                 $this->assertSame( 'auto', $preloaded_script_modules['nested-static-dep']['fetchpriority'] );
     783                $this->assertSame( 'auto', $preloaded_script_modules['nested-static-dep']['fetchpriority'] ); // Auto because the enqueued script foo has the fetchpriority of auto.
    765784                $this->assertArrayNotHasKey( 'dynamic-dep', $preloaded_script_modules );
    766785                $this->assertArrayNotHasKey( 'nested-dynamic-dep', $preloaded_script_modules );
     
    972991                $preloaded_script_modules = $this->get_preloaded_script_modules();
    973992                $this->assertSame( '/dep.js?ver=2.0', $preloaded_script_modules['dep']['url'] );
    974                 $this->assertSame( 'high', $preloaded_script_modules['dep']['fetchpriority'] );
     993                $this->assertSame( 'auto', $preloaded_script_modules['dep']['fetchpriority'] ); // Because 'foo' has a priority of 'auto'.
    975994        }
    976995
     
    13451364
    13461365        /**
     1366         * Data provider.
     1367         *
     1368         * @return array<string, array{enqueues: string[], expected: array}>
     1369         */
     1370        public function data_provider_to_test_fetchpriority_bumping(): array {
     1371                return array(
     1372                        'enqueue_bajo' => array(
     1373                                'enqueues' => array( 'bajo' ),
     1374                                'expected' => array(
     1375                                        'preload_links' => array(),
     1376                                        'script_tags'   => array(
     1377                                                'bajo' => array(
     1378                                                        'url'                   => '/bajo.js',
     1379                                                        'fetchpriority'         => 'high',
     1380                                                        'data-wp-fetchpriority' => 'low',
     1381                                                ),
     1382                                        ),
     1383                                        'import_map'    => array(
     1384                                                'dyno' => '/dyno.js',
     1385                                        ),
     1386                                ),
     1387                        ),
     1388                        'enqueue_auto' => array(
     1389                                'enqueues' => array( 'auto' ),
     1390                                'expected' => array(
     1391                                        'preload_links' => array(
     1392                                                'bajo' => array(
     1393                                                        'url'                   => '/bajo.js',
     1394                                                        'fetchpriority'         => 'auto',
     1395                                                        'data-wp-fetchpriority' => 'low',
     1396                                                ),
     1397                                        ),
     1398                                        'script_tags'   => array(
     1399                                                'auto' => array(
     1400                                                        'url'                   => '/auto.js',
     1401                                                        'fetchpriority'         => 'high',
     1402                                                        'data-wp-fetchpriority' => 'auto',
     1403                                                ),
     1404                                        ),
     1405                                        'import_map'    => array(
     1406                                                'bajo' => '/bajo.js',
     1407                                                'dyno' => '/dyno.js',
     1408                                        ),
     1409                                ),
     1410                        ),
     1411                        'enqueue_alto' => array(
     1412                                'enqueues' => array( 'alto' ),
     1413                                'expected' => array(
     1414                                        'preload_links' => array(
     1415                                                'auto' => array(
     1416                                                        'url'           => '/auto.js',
     1417                                                        'fetchpriority' => 'high',
     1418                                                ),
     1419                                                'bajo' => array(
     1420                                                        'url'                   => '/bajo.js',
     1421                                                        'fetchpriority'         => 'high',
     1422                                                        'data-wp-fetchpriority' => 'low',
     1423                                                ),
     1424                                        ),
     1425                                        'script_tags'   => array(
     1426                                                'alto' => array(
     1427                                                        'url'           => '/alto.js',
     1428                                                        'fetchpriority' => 'high',
     1429                                                ),
     1430                                        ),
     1431                                        'import_map'    => array(
     1432                                                'auto' => '/auto.js',
     1433                                                'bajo' => '/bajo.js',
     1434                                                'dyno' => '/dyno.js',
     1435                                        ),
     1436                                ),
     1437                        ),
     1438                );
     1439        }
     1440
     1441        /**
     1442         * Tests a higher fetchpriority on a dependent script module causes the fetchpriority of a dependency script module to be bumped.
     1443         *
     1444         * @ticket 61734
     1445         *
     1446         * @covers WP_Script_Modules::print_enqueued_script_modules
     1447         * @covers WP_Script_Modules::get_dependents
     1448         * @covers WP_Script_Modules::get_recursive_dependents
     1449         * @covers WP_Script_Modules::get_highest_fetchpriority
     1450         * @covers WP_Script_Modules::print_script_module_preloads
     1451         *
     1452         * @dataProvider data_provider_to_test_fetchpriority_bumping
     1453         */
     1454        public function test_fetchpriority_bumping( array $enqueues, array $expected ) {
     1455                $this->script_modules->register(
     1456                        'dyno',
     1457                        '/dyno.js',
     1458                        array(),
     1459                        null,
     1460                        array( 'fetchpriority' => 'low' ) // This won't show up anywhere since it is a dynamic import dependency.
     1461                );
     1462
     1463                $this->script_modules->register(
     1464                        'bajo',
     1465                        '/bajo.js',
     1466                        array(
     1467                                array(
     1468                                        'id'     => 'dyno',
     1469                                        'import' => 'dynamic',
     1470                                ),
     1471                        ),
     1472                        null,
     1473                        array( 'fetchpriority' => 'low' )
     1474                );
     1475
     1476                $this->script_modules->register(
     1477                        'auto',
     1478                        '/auto.js',
     1479                        array(
     1480                                array(
     1481                                        'id'     => 'bajo',
     1482                                        'import' => 'static',
     1483                                ),
     1484                        ),
     1485                        null,
     1486                        array( 'fetchpriority' => 'auto' )
     1487                );
     1488                $this->script_modules->register(
     1489                        'alto',
     1490                        '/alto.js',
     1491                        array( 'auto' ),
     1492                        null,
     1493                        array( 'fetchpriority' => 'high' )
     1494                );
     1495
     1496                foreach ( $enqueues as $enqueue ) {
     1497                        $this->script_modules->enqueue( $enqueue );
     1498                }
     1499
     1500                $actual = array(
     1501                        'preload_links' => $this->get_preloaded_script_modules(),
     1502                        'script_tags'   => $this->get_enqueued_script_modules(),
     1503                        'import_map'    => $this->get_import_map(),
     1504                );
     1505                $this->assertSame(
     1506                        $expected,
     1507                        $actual,
     1508                        "Snapshot:\n" . var_export( $actual, true )
     1509                );
     1510        }
     1511
     1512        /**
     1513         * Tests bumping fetchpriority with complex dependency graph.
     1514         *
     1515         * @ticket 61734
     1516         * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3280065818
     1517         *
     1518         * @covers WP_Script_Modules::print_enqueued_script_modules
     1519         * @covers WP_Script_Modules::get_dependents
     1520         * @covers WP_Script_Modules::get_recursive_dependents
     1521         * @covers WP_Script_Modules::get_highest_fetchpriority
     1522         * @covers WP_Script_Modules::print_script_module_preloads
     1523         */
     1524        public function test_fetchpriority_bumping_a_to_z() {
     1525                wp_register_script_module( 'a', '/a.js', array( 'b' ), null, array( 'fetchpriority' => 'low' ) );
     1526                wp_register_script_module( 'b', '/b.js', array( 'c' ), null, array( 'fetchpriority' => 'auto' ) );
     1527                wp_register_script_module( 'c', '/c.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'auto' ) );
     1528                wp_register_script_module( 'd', '/d.js', array( 'z' ), null, array( 'fetchpriority' => 'high' ) );
     1529                wp_register_script_module( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'auto' ) );
     1530
     1531                wp_register_script_module( 'x', '/x.js', array( 'd', 'y' ), null, array( 'fetchpriority' => 'high' ) );
     1532                wp_register_script_module( 'y', '/y.js', array( 'z' ), null, array( 'fetchpriority' => 'auto' ) );
     1533                wp_register_script_module( 'z', '/z.js', array(), null, array( 'fetchpriority' => 'auto' ) );
     1534
     1535                // The fetch priorities are derived from these enqueued dependents.
     1536                wp_enqueue_script_module( 'a' );
     1537                wp_enqueue_script_module( 'x' );
     1538
     1539                $actual   = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     1540                $actual  .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1541                $expected = '
     1542                        <link rel="modulepreload" href="/b.js" id="b-js-modulepreload" fetchpriority="low">
     1543                        <link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="low">
     1544                        <link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="high">
     1545                        <link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="low">
     1546                        <link rel="modulepreload" href="/z.js" id="z-js-modulepreload" fetchpriority="high">
     1547                        <link rel="modulepreload" href="/y.js" id="y-js-modulepreload" fetchpriority="high">
     1548                        <script type="module" src="/a.js" id="a-js-module" fetchpriority="low"></script>
     1549                        <script type="module" src="/x.js" id="x-js-module" fetchpriority="high"></script>
     1550                ';
     1551                $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1552        }
     1553
     1554        /**
     1555         * Tests bumping fetchpriority with complex dependency graph.
     1556         *
     1557         * @ticket 61734
     1558         * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3284266884
     1559         *
     1560         * @covers WP_Script_Modules::print_enqueued_script_modules
     1561         * @covers WP_Script_Modules::get_dependents
     1562         * @covers WP_Script_Modules::get_recursive_dependents
     1563         * @covers WP_Script_Modules::get_highest_fetchpriority
     1564         * @covers WP_Script_Modules::print_script_module_preloads
     1565         */
     1566        public function test_fetchpriority_propagation() {
     1567                // The high fetchpriority for this module will be disregarded because its enqueued dependent has a non-high priority.
     1568                wp_register_script_module( 'a', '/a.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'high' ) );
     1569                wp_register_script_module( 'b', '/b.js', array( 'e' ), null );
     1570                wp_register_script_module( 'c', '/c.js', array( 'e', 'f' ), null );
     1571                wp_register_script_module( 'd', '/d.js', array(), null );
     1572                // The low fetchpriority for this module will be disregarded because its enqueued dependent has a non-low priority.
     1573                wp_register_script_module( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'low' ) );
     1574                wp_register_script_module( 'f', '/f.js', array(), null );
     1575
     1576                wp_register_script_module( 'x', '/x.js', array( 'a' ), null, array( 'fetchpriority' => 'low' ) );
     1577                wp_register_script_module( 'y', '/y.js', array( 'b' ), null, array( 'fetchpriority' => 'auto' ) );
     1578                wp_register_script_module( 'z', '/z.js', array( 'c' ), null, array( 'fetchpriority' => 'high' ) );
     1579
     1580                wp_enqueue_script_module( 'x' );
     1581                wp_enqueue_script_module( 'y' );
     1582                wp_enqueue_script_module( 'z' );
     1583
     1584                $actual   = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     1585                $actual  .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1586                $expected = '
     1587                        <link rel="modulepreload" href="/a.js" id="a-js-modulepreload" fetchpriority="low" data-wp-fetchpriority="high">
     1588                        <link rel="modulepreload" href="/d.js" id="d-js-modulepreload" fetchpriority="low">
     1589                        <link rel="modulepreload" href="/e.js" id="e-js-modulepreload" fetchpriority="high" data-wp-fetchpriority="low">
     1590                        <link rel="modulepreload" href="/b.js" id="b-js-modulepreload">
     1591                        <link rel="modulepreload" href="/c.js" id="c-js-modulepreload" fetchpriority="high">
     1592                        <link rel="modulepreload" href="/f.js" id="f-js-modulepreload" fetchpriority="high">
     1593                        <script type="module" src="/x.js" id="x-js-module" fetchpriority="low"></script>
     1594                        <script type="module" src="/y.js" id="y-js-module"></script>
     1595                        <script type="module" src="/z.js" id="z-js-module" fetchpriority="high"></script>
     1596                ';
     1597                $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1598        }
     1599
     1600        /**
     1601         * Tests that default script modules are printed as expected.
     1602         *
     1603         * @covers ::wp_default_script_modules
     1604         * @covers WP_Script_Modules::print_script_module_preloads
     1605         * @covers WP_Script_Modules::print_enqueued_script_modules
     1606         */
     1607        public function test_default_script_modules() {
     1608                wp_default_script_modules();
     1609                wp_enqueue_script_module( '@wordpress/a11y' );
     1610                wp_enqueue_script_module( '@wordpress/block-library/navigation/view' );
     1611
     1612                $actual  = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     1613                $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1614
     1615                $actual = $this->normalize_markup_for_snapshot( $actual );
     1616
     1617                $expected = '
     1618                        <link rel="modulepreload" href="/wp-includes/js/dist/script-modules/interactivity/debug.min.js" id="@wordpress/interactivity-js-modulepreload" fetchpriority="low">
     1619                        <script type="module" src="/wp-includes/js/dist/script-modules/a11y/index.min.js" id="@wordpress/a11y-js-module" fetchpriority="low"></script>
     1620                        <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>
     1621                ';
     1622                $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1623        }
     1624
     1625        /**
     1626         * Tests that a dependent with high priority for default script modules with a low fetch priority are printed as expected.
     1627         *
     1628         * @covers ::wp_default_script_modules
     1629         * @covers WP_Script_Modules::print_script_module_preloads
     1630         * @covers WP_Script_Modules::print_enqueued_script_modules
     1631         */
     1632        public function test_dependent_of_default_script_modules() {
     1633                wp_default_script_modules();
     1634                wp_enqueue_script_module(
     1635                        'super-important',
     1636                        '/super-important-module.js',
     1637                        array( '@wordpress/a11y', '@wordpress/block-library/navigation/view' ),
     1638                        null,
     1639                        array( 'fetchpriority' => 'high' )
     1640                );
     1641
     1642                $actual  = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) );
     1643                $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) );
     1644
     1645                $actual = $this->normalize_markup_for_snapshot( $actual );
     1646
     1647                $expected = '
     1648                        <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">
     1649                        <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">
     1650                        <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">
     1651                        <script type="module" src="/super-important-module.js" id="super-important-js-module" fetchpriority="high"></script>
     1652                ';
     1653                $this->assertEqualHTML( $expected, $actual, '<body>', "Snapshot:\n$actual" );
     1654        }
     1655
     1656        /**
     1657         * Normalizes markup for snapshot.
     1658         *
     1659         * @param string $markup Markup.
     1660         * @return string Normalized markup.
     1661         */
     1662        private function normalize_markup_for_snapshot( string $markup ): string {
     1663                $processor = new WP_HTML_Tag_Processor( $markup );
     1664                $clean_url = static function ( string $url ): string {
     1665                        $url = preg_replace( '#^https?://[^/]+#', '', $url );
     1666                        return remove_query_arg( 'ver', $url );
     1667                };
     1668                while ( $processor->next_tag() ) {
     1669                        if ( 'LINK' === $processor->get_tag() && is_string( $processor->get_attribute( 'href' ) ) ) {
     1670                                $processor->set_attribute( 'href', $clean_url( $processor->get_attribute( 'href' ) ) );
     1671                        } elseif ( 'SCRIPT' === $processor->get_tag() && is_string( $processor->get_attribute( 'src' ) ) ) {
     1672                                $processor->set_attribute( 'src', $clean_url( $processor->get_attribute( 'src' ) ) );
     1673                        }
     1674                }
     1675                return $processor->get_updated_html();
     1676        }
     1677
     1678        /**
    13471679         * Tests that directly manipulating the queue works as expected.
    13481680         *
Note: See TracChangeset for help on using the changeset viewer.