Make WordPress Core


Ignore:
Timestamp:
05/19/2026 12:23:04 PM (4 months ago)
Author:
ellatrix
Message:

Script Loader: Warn when classic scripts with module dependencies lack footer/defer.

A classic script with module_dependencies may be evaluated before the script modules import map is printed if it loads blocking in the document head, causing a "Failed to resolve module specifier" error on dynamic imports.

  • Trigger _doing_it_wrong() from _wp_scripts_add_args_data() when a classic script provides module_dependencies without setting in_footer to true or using a defer loading strategy, and document this requirement in the wp_register_script() and wp_enqueue_script() docblocks.
  • Remove the module_dependencies arg from the wp-codemirror script registration in favor of passing the espree module URL directly through wp_get_code_editor_settings(). This avoids registering espree as a publicly-available script module when it is only ever used internally as a private implementation detail of the code editor.
  • Add a console.warn() in wp.codeEditor.initialize() when invoked before DOMContentLoaded, so developers are alerted if the function is called too early for the import map to have been parsed.
  • Add PHPStan types which were missing when module_dependencies were initially introduced.
  • Harden WP_Scripts::add_data() against non-string strategy values being passed to sprintf().

Developed in https://github.com/WordPress/wordpress-develop/pull/11788

Follow-up to r61587.

Reviewed by jonsurrell.
Merges [62368] to the 7.0 branch.
Props khokansardar, westonruter, jonsurrell, jorbin.
See #61500, #64238.
Fixes #65165.

Location:
branches/7.0
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • branches/7.0

  • branches/7.0/tests/phpunit/tests/dependencies/scripts.php

    r61927 r62379  
    99 * @covers ::wp_add_inline_script
    1010 * @covers ::wp_set_script_translations
     11 *
     12 * @phpstan-type ScriptArgs array{
     13 *     in_footer?: bool,
     14 *     strategy?: 'async'|'defer',
     15 *     fetchpriority?: 'low'|'auto'|'high',
     16 *     module_dependencies?: array<non-empty-string|array{ id: non-empty-string, ... }>,
     17 * }
     18 * @phpstan-type WpEnqueueScriptArgs array{
     19 *     0: non-empty-string, // $handle
     20 *     1?: non-empty-string, // $src
     21 *     2?: non-empty-string[], // $deps
     22 *     3?: null|bool|string, // $version
     23 *     4?: ScriptArgs,
     24 * }
    1125 */
    1226class Tests_Dependencies_Scripts extends WP_UnitTestCase {
     
    13931407                                'expected' => true,
    13941408                                'stored'   => array( 'valid', array( 'id' => 'valid2' ) ),
     1409                        ),
     1410                );
     1411        }
     1412
     1413        /**
     1414         * Tests that registering a script with `module_dependencies` triggers `_doing_it_wrong`
     1415         * when the script is not printed in the footer and does not use the `defer` strategy.
     1416         *
     1417         * @ticket 65165
     1418         *
     1419         * @covers ::wp_register_script
     1420         * @covers ::wp_enqueue_script
     1421         * @covers ::_wp_scripts_add_args_data
     1422         *
     1423         * @dataProvider data_module_dependencies_require_footer_or_defer
     1424         *
     1425         * @param callable-string $function_name Function name to call.
     1426         * @param array           $args          Arguments to pass to the function.
     1427         * @param bool            $should_warn   Whether the call is expected to trigger a `_doing_it_wrong` warning.
     1428         *
     1429         * @phpstan-param WpEnqueueScriptArgs $args
     1430         */
     1431        public function test_module_dependencies_require_footer_or_defer( string $function_name, array $args, bool $should_warn ): void {
     1432                if ( $should_warn ) {
     1433                        $this->setExpectedIncorrectUsage( $function_name );
     1434                }
     1435
     1436                call_user_func_array( $function_name, $args );
     1437
     1438                if ( $should_warn ) {
     1439                        $this->assertStringContainsString(
     1440                                'module_dependencies',
     1441                                $this->caught_doing_it_wrong[ $function_name ],
     1442                                'The _doing_it_wrong message should reference module_dependencies.'
     1443                        );
     1444                        $this->assertStringContainsString(
     1445                                'in_footer',
     1446                                $this->caught_doing_it_wrong[ $function_name ],
     1447                                'The _doing_it_wrong message should reference the in_footer requirement.'
     1448                        );
     1449                        $this->assertStringContainsString(
     1450                                'defer',
     1451                                $this->caught_doing_it_wrong[ $function_name ],
     1452                                'The _doing_it_wrong message should reference the defer strategy.'
     1453                        );
     1454                } else {
     1455                        $this->assertArrayNotHasKey(
     1456                                $function_name,
     1457                                $this->caught_doing_it_wrong,
     1458                                'No _doing_it_wrong warning should be triggered when in_footer is true or strategy is defer.'
     1459                        );
     1460                }
     1461        }
     1462
     1463        /**
     1464         * Data provider for {@see self::test_module_dependencies_require_footer_or_defer()}.
     1465         *
     1466         * @phpstan-return array<string, array{
     1467         *     function_name: callable-string,
     1468         *     args: WpEnqueueScriptArgs,
     1469         *     should_warn: bool,
     1470         * }>
     1471         */
     1472        public function data_module_dependencies_require_footer_or_defer(): array {
     1473                $base_args = array(
     1474                        '/script.js',
     1475                        array(),
     1476                        null,
     1477                );
     1478
     1479                return array(
     1480                        'register_blocking_warns'            => array(
     1481                                'function_name' => 'wp_register_script',
     1482                                'args'          => array_merge(
     1483                                        array( 'module-deps-blocking-register' ),
     1484                                        $base_args,
     1485                                        array(
     1486                                                array(
     1487                                                        'module_dependencies' => array( 'foo' ),
     1488                                                ),
     1489                                        )
     1490                                ),
     1491                                'should_warn'   => true,
     1492                        ),
     1493                        'enqueue_blocking_warns'             => array(
     1494                                'function_name' => 'wp_enqueue_script',
     1495                                'args'          => array_merge(
     1496                                        array( 'module-deps-blocking-enqueue' ),
     1497                                        $base_args,
     1498                                        array(
     1499                                                array(
     1500                                                        'module_dependencies' => array( 'foo' ),
     1501                                                ),
     1502                                        )
     1503                                ),
     1504                                'should_warn'   => true,
     1505                        ),
     1506                        'register_async_warns'               => array(
     1507                                'function_name' => 'wp_register_script',
     1508                                'args'          => array_merge(
     1509                                        array( 'module-deps-async-register' ),
     1510                                        $base_args,
     1511                                        array(
     1512                                                array(
     1513                                                        'module_dependencies' => array( 'foo' ),
     1514                                                        'strategy'            => 'async',
     1515                                                ),
     1516                                        )
     1517                                ),
     1518                                'should_warn'   => true,
     1519                        ),
     1520                        'enqueue_async_warns'                => array(
     1521                                'function_name' => 'wp_enqueue_script',
     1522                                'args'          => array_merge(
     1523                                        array( 'module-deps-async-enqueue' ),
     1524                                        $base_args,
     1525                                        array(
     1526                                                array(
     1527                                                        'module_dependencies' => array( 'foo' ),
     1528                                                        'strategy'            => 'async',
     1529                                                ),
     1530                                        )
     1531                                ),
     1532                                'should_warn'   => true,
     1533                        ),
     1534                        'register_in_footer_does_not_warn'   => array(
     1535                                'function_name' => 'wp_register_script',
     1536                                'args'          => array_merge(
     1537                                        array( 'module-deps-footer-register' ),
     1538                                        $base_args,
     1539                                        array(
     1540                                                array(
     1541                                                        'module_dependencies' => array( 'foo' ),
     1542                                                        'in_footer'           => true,
     1543                                                ),
     1544                                        )
     1545                                ),
     1546                                'should_warn'   => false,
     1547                        ),
     1548                        'enqueue_in_footer_does_not_warn'    => array(
     1549                                'function_name' => 'wp_enqueue_script',
     1550                                'args'          => array_merge(
     1551                                        array( 'module-deps-footer-enqueue' ),
     1552                                        $base_args,
     1553                                        array(
     1554                                                array(
     1555                                                        'module_dependencies' => array( 'foo' ),
     1556                                                        'in_footer'           => true,
     1557                                                ),
     1558                                        )
     1559                                ),
     1560                                'should_warn'   => false,
     1561                        ),
     1562                        'register_defer_does_not_warn'       => array(
     1563                                'function_name' => 'wp_register_script',
     1564                                'args'          => array_merge(
     1565                                        array( 'module-deps-defer-register' ),
     1566                                        $base_args,
     1567                                        array(
     1568                                                array(
     1569                                                        'module_dependencies' => array( 'foo' ),
     1570                                                        'strategy'            => 'defer',
     1571                                                ),
     1572                                        )
     1573                                ),
     1574                                'should_warn'   => false,
     1575                        ),
     1576                        'enqueue_defer_does_not_warn'        => array(
     1577                                'function_name' => 'wp_enqueue_script',
     1578                                'args'          => array_merge(
     1579                                        array( 'module-deps-defer-enqueue' ),
     1580                                        $base_args,
     1581                                        array(
     1582                                                array(
     1583                                                        'module_dependencies' => array( 'foo' ),
     1584                                                        'strategy'            => 'defer',
     1585                                                ),
     1586                                        )
     1587                                ),
     1588                                'should_warn'   => false,
     1589                        ),
     1590                        'register_footer_and_defer_no_warn'  => array(
     1591                                'function_name' => 'wp_register_script',
     1592                                'args'          => array_merge(
     1593                                        array( 'module-deps-footer-defer-register' ),
     1594                                        $base_args,
     1595                                        array(
     1596                                                array(
     1597                                                        'module_dependencies' => array( 'foo' ),
     1598                                                        'in_footer'           => true,
     1599                                                        'strategy'            => 'defer',
     1600                                                ),
     1601                                        )
     1602                                ),
     1603                                'should_warn'   => false,
     1604                        ),
     1605                        'register_no_module_deps_no_warn'    => array(
     1606                                'function_name' => 'wp_register_script',
     1607                                'args'          => array_merge(
     1608                                        array( 'no-module-deps-register' ),
     1609                                        $base_args,
     1610                                        array( array() )
     1611                                ),
     1612                                'should_warn'   => false,
     1613                        ),
     1614                        'register_empty_module_deps_no_warn' => array(
     1615                                'function_name' => 'wp_register_script',
     1616                                'args'          => array_merge(
     1617                                        array( 'empty-module-deps-register' ),
     1618                                        $base_args,
     1619                                        array(
     1620                                                array(
     1621                                                        'module_dependencies' => array(),
     1622                                                ),
     1623                                        )
     1624                                ),
     1625                                'should_warn'   => false,
    13951626                        ),
    13961627                );
     
    31703401                                'browser',
    31713402                                'globals',
     3403                                'espreeModuleUrl',
    31723404                        ),
    31733405                        array_keys( $wp_enqueue_code_editor['jshint'] )
     
    32533485                                'browser',
    32543486                                'globals',
     3487                                'espreeModuleUrl',
    32553488                        ),
    32563489                        array_keys( $wp_enqueue_code_editor['jshint'] )
     
    33503583                                'browser',
    33513584                                'globals',
     3585                                'espreeModuleUrl',
    33523586                        ),
    33533587                        array_keys( $wp_enqueue_code_editor['jshint'] )
     
    34443678                                'browser',
    34453679                                'globals',
     3680                                'espreeModuleUrl',
    34463681                        ),
    34473682                        array_keys( $wp_enqueue_code_editor['jshint'] )
Note: See TracChangeset for help on using the changeset viewer.