Make WordPress Core


Ignore:
Timestamp:
02/04/2026 01:41:25 AM (8 months ago)
Author:
westonruter
Message:

Script Loader: Allow classic scripts to depend on script modules.

This allows classic scripts to declare dependencies on script modules by passing module_dependencies in the $args param for wp_register_script() or wp_enqueue_script(). The WP_Script_Modules::get_import_map() method is updated to traverse the dependency tree of all enqueued classic scripts to find any associated script module dependencies and include them in the importmap, enabling dynamic imports of modules within classic scripts.

A _wp_scripts_add_args_data() helper function is introduced to consolidate argument validation and processing for wp_register_script() and wp_enqueue_script(), reducing code duplication. This function validates that the $args array only contains recognized keys (strategy, in_footer, fetchpriority, module_dependencies) and triggers a _doing_it_wrong() notice for any unrecognized keys. Similarly, WP_Scripts::add_data() is updated to do early type checking for the data passed to $args. The script modules in module_dependencies may be referenced by a module ID string or by an array that has an id key, following the same pattern as dependencies in WP_Script_Modules.

When a script module is added to the module_dependencies for a classic script, but it does not exist at the time the importmap is printed, a _doing_it_wrong() notice is emitted.

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

Follow-up to [61323].

Props sirreal, westonruter.
See #64229.
Fixes #61500.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/script-modules/wpScriptModules.php

    r61519 r61587  
    1212class Tests_Script_Modules_WpScriptModules extends WP_UnitTestCase {
    1313
    14         /**
    15          * @var WP_Script_Modules
    16          */
    17         protected $original_script_modules;
    18 
    19         /**
    20          * @var string
    21          */
    22         protected $original_wp_version;
    23 
    24         /**
    25          * Instance of WP_Script_Modules.
    26          *
    27          * @var WP_Script_Modules
    28          */
    29         protected $script_modules;
     14        protected WP_Script_Modules $original_script_modules;
     15
     16        protected string $original_wp_version;
     17
     18        protected ?WP_Scripts $original_wp_scripts;
     19
     20        protected WP_Script_Modules $script_modules;
    3021
    3122        /**
    … …  
    3324         */
    3425        public function set_up() {
    35                 global $wp_script_modules, $wp_version;
     26                global $wp_script_modules, $wp_scripts, $wp_version;
    3627                parent::set_up();
    3728                $this->original_script_modules = $wp_script_modules;
    3829                $this->original_wp_version     = $wp_version;
     30                $this->original_wp_scripts     = $wp_scripts ?? null;
    3931                $wp_script_modules             = null;
    4032                $this->script_modules          = wp_script_modules();
     33
     34                $wp_scripts                  = new WP_Scripts();
     35                $wp_scripts->default_version = get_bloginfo( 'version' );
    4136        }
    4237
    … …  
    4540         */
    4641        public function tear_down() {
    47                 global $wp_script_modules, $wp_version;
    4842                parent::tear_down();
     43                global $wp_script_modules, $wp_scripts, $wp_version;
    4944                $wp_script_modules = $this->original_script_modules;
    5045                $wp_version        = $this->original_wp_version;
     46                $wp_scripts        = $this->original_wp_scripts;
    5147        }
    5248
    … …  
    19871983
    19881984        /**
     1985         * Tests that script modules identified as dependencies of classic scripts are included in the import map.
     1986         *
     1987         * @ticket 61500
     1988         *
     1989         * @covers WP_Script_Modules::get_import_map
     1990         */
     1991        public function test_included_module_appears_in_importmap() {
     1992                $this->script_modules->register( 'dependency', '/dep.js' );
     1993                $this->script_modules->register( 'example', '/example.js', array( 'dependency' ) );
     1994                $this->script_modules->register( 'example2', '/example2.js' );
     1995
     1996                // Nothing printed now.
     1997                $this->assertSame( array(), $this->get_enqueued_script_modules(), 'Initial enqueued script modules was wrong.' );
     1998                $this->assertSame( array(), $this->get_preloaded_script_modules(), 'Initial module preloads was wrong.' );
     1999                $this->assertSame( array(), $this->get_import_map(), 'Initial import map was wrong.' );
     2000
     2001                // Enqueuing a script with a module dependency should add it to the import map.
     2002                wp_enqueue_script(
     2003                        'classic',
     2004                        '/classic.js',
     2005                        array( 'classic-dependency' ),
     2006                        false,
     2007                        array(
     2008                                'module_dependencies' => array(
     2009                                        'example',
     2010                                        array(
     2011                                                'id' => 'example2',
     2012                                        ),
     2013                                ),
     2014                        )
     2015                );
     2016
     2017                $this->assertSame( array(), $this->get_enqueued_script_modules(), 'Final enqueued script modules was wrong.' );
     2018                $this->assertSame( array(), $this->get_preloaded_script_modules(), 'Final module preloads was wrong.' );
     2019                $this->assertEqualSets(
     2020                        array( 'example', 'example2', 'dependency' ),
     2021                        array_keys( $this->get_import_map() ),
     2022                        'Import map keys were wrong.'
     2023                );
     2024        }
     2025
     2026        /**
     2027         * Tests that dynamic dependencies of enqueued script modules are included in the import map.
     2028         *
     2029         * @ticket 61500
     2030         *
     2031         * @covers WP_Script_Modules::get_import_map
     2032         */
     2033        public function test_import_map_includes_dynamic_dependencies_of_enqueued_modules() {
     2034                $this->script_modules->register( 'dependency-of-enqueued', '/dependency-of-enqueued.js' );
     2035                $this->script_modules->enqueue(
     2036                        'enqueued',
     2037                        '/enqueued.js',
     2038                        array(
     2039                                array(
     2040                                        'id'     => 'dependency-of-enqueued',
     2041                                        'import' => 'dynamic',
     2042                                ),
     2043                        )
     2044                );
     2045
     2046                $enqueued = $this->get_enqueued_script_modules();
     2047                $this->assertCount( 1, $enqueued, 'Enqueue count was wrong.' );
     2048                $this->assertArrayHasKey( 'enqueued', $enqueued, 'Missing "enqueued" script module enqueue.' );
     2049                $this->assertCount( 0, $this->get_preloaded_script_modules(), 'Module preload count was wrong.' );
     2050                $this->assertEqualSets(
     2051                        array( 'dependency-of-enqueued' ),
     2052                        array_keys( $this->get_import_map() ),
     2053                        'Import map keys were wrong.'
     2054                );
     2055        }
     2056
     2057        /**
     2058         * Tests that script module dependencies of enqueued classic scripts (including transitive ones) are included in the import map.
     2059         *
     2060         * @ticket 61500
     2061         *
     2062         * @covers WP_Script_Modules::get_import_map
     2063         */
     2064        public function test_import_map_includes_dependencies_of_classic_scripts_recursive() {
     2065                $this->script_modules->register( 'classic-transitive-dependency', '/classic-transitive-dependency.js' );
     2066                $this->script_modules->register( 'dependency-of-not-enqueued', '/dependency-of-not-enqueued.js' );
     2067                $this->script_modules->register( 'not-enqueued', '/not-enqueued.js', array( 'dependency-of-not-enqueued' ) );
     2068
     2069                // Enqueuing a script with a module dependency should add it to the import map.
     2070                wp_register_script(
     2071                        'classic-transitive-dep',
     2072                        '/classic-transitive-dep.js',
     2073                        array(),
     2074                        false,
     2075                        array(
     2076                                'module_dependencies' => array( 'classic-transitive-dependency' ),
     2077                        )
     2078                );
     2079                wp_enqueue_script(
     2080                        'classic',
     2081                        '/classic.js',
     2082                        array( 'classic-transitive-dep' ),
     2083                        false,
     2084                        array(
     2085                                'module_dependencies' => array( 'not-enqueued' ),
     2086                        )
     2087                );
     2088
     2089                $enqueued = $this->get_enqueued_script_modules();
     2090                $this->assertCount( 0, $enqueued, 'Enqueue count was wrong.' );
     2091                $this->assertCount( 0, $this->get_preloaded_script_modules(), 'Module preload count was wrong.' );
     2092                $this->assertEqualSets(
     2093                        array(
     2094                                'classic-transitive-dependency',
     2095                                'not-enqueued',
     2096                                'dependency-of-not-enqueued',
     2097                        ),
     2098                        array_keys( $this->get_import_map() ),
     2099                        'Import map keys were wrong.'
     2100                );
     2101        }
     2102
     2103        /**
     2104         * Tests that WP_Scripts emits a _doing_it_wrong() notice for missing script module dependencies.
     2105         *
     2106         * @ticket 61500
     2107         * @ticket 64229
     2108         * @covers WP_Script_Modules::get_import_map
     2109         */
     2110        public function test_wp_scripts_doing_it_wrong_for_missing_script_module_dependencies() {
     2111                $expected_incorrect_usage = 'WP_Scripts::add_data';
     2112                $this->setExpectedIncorrectUsage( $expected_incorrect_usage );
     2113
     2114                wp_enqueue_script(
     2115                        'registered-dep',
     2116                        '/registered-dep.js',
     2117                        array(),
     2118                        null,
     2119                        array(
     2120                                'module_dependencies' => array( 'does-not-exist' ),
     2121                        )
     2122                );
     2123
     2124                $import_map = $this->get_import_map();
     2125                $this->assertSame( array(), $import_map, 'Expected importmap to be empty.' );
     2126                $markup = get_echo( 'wp_print_scripts' );
     2127
     2128                /*
     2129                 * In the future, we may want to have missing script module dependencies for classic scripts to cause the
     2130                 * classic script to not be printed. This would align the behavior with script modules that have missing
     2131                 * script module dependencies, and classic scripts that have missing classic script dependencies. Nevertheless,
     2132                 * since script module dependencies rely on dynamic imports, the dependency may not be as strong. This means
     2133                 * the classic script may still work or have a fallback in case the script module fails to dynamically import.
     2134                 * This same change could be made for script modules as well, where if a script module has a missing dynamic
     2135                 * script module dependency, this might similarly not be sufficient reason to omit printing the dependent script module.
     2136                 */
     2137                $this->assertStringContainsString( 'registered-dep.js', $markup, 'Expected script to be present, even though it has a missing script module dependency.' );
     2138
     2139                $this->assertArrayHasKey(
     2140                        $expected_incorrect_usage,
     2141                        $this->caught_doing_it_wrong,
     2142                        "Expected $expected_incorrect_usage to trigger a _doing_it_wrong() notice for missing dependency."
     2143                );
     2144
     2145                $this->assertStringContainsString(
     2146                        'The script with the handle "registered-dep" was enqueued with script module dependencies ("module_dependencies") that are not registered: does-not-exist',
     2147                        $this->caught_doing_it_wrong[ $expected_incorrect_usage ],
     2148                        'Expected _doing_it_wrong() notice to indicate missing script module dependencies for enqueued script.'
     2149                );
     2150        }
     2151
     2152        /**
    19892153         * Tests various ways of printing and dependency ordering of script modules.
    19902154         *
Note: See TracChangeset for help on using the changeset viewer.