From 507697aae3860bd0b702aad2caf77978f3cfbd47 Mon Sep 17 00:00:00 2001 From: Khokan Sardar Date: Sun, 10 May 2026 17:56:42 +0530 Subject: [PATCH 01/16] Script Loader: Warn when classic scripts depend on modules without footer/defer. Scripts registered or enqueued with a `module_dependencies` arg may evaluate before the script modules import map is printed if they are loaded 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. Existing tests in `wpScriptModules.php` that exercised this path are updated to use `in_footer => true` so they continue to validate the import map behavior without tripping the new warning. See #65165, #61500. Co-authored-by: Cursor --- src/wp-includes/functions.wp-scripts.php | 34 +++ tests/phpunit/tests/dependencies/scripts.php | 197 ++++++++++++++++++ .../tests/script-modules/wpScriptModules.php | 4 + 3 files changed, 235 insertions(+) diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index 59e4e54a1a1ad..96b48ef33169c 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -108,6 +108,32 @@ function _wp_scripts_add_args_data( WP_Scripts $wp_scripts, string $handle, arra } if ( ! empty( $args['module_dependencies'] ) ) { $wp_scripts->add_data( $handle, 'module_dependencies', $args['module_dependencies'] ); + + /* + * A classic script with module dependencies must either be printed in the + * footer or use the 'defer' loading strategy. Otherwise, the script may be + * evaluated before the script modules import map is printed, causing + * dynamic imports to fail with a "Failed to resolve module specifier" error. + */ + $is_in_footer = ! empty( $args['in_footer'] ); + $is_deferred = isset( $args['strategy'] ) && 'defer' === $args['strategy']; + if ( ! $is_in_footer && ! $is_deferred ) { + $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 ); + $function_name = ( $trace[1]['class'] ?? '' ) . ( $trace[1]['type'] ?? '' ) . $trace[1]['function']; + _doing_it_wrong( + $function_name, + sprintf( + /* translators: 1: 'module_dependencies', 2: Script handle, 3: 'in_footer', 4: 'strategy', 5: 'defer'. */ + __( 'When the %1$s arg is provided, the "%2$s" script must either be printed in the footer (%3$s set to true) or use a deferred loading %4$s (%5$s) so that the import map is printed before the script is evaluated.' ), + 'module_dependencies', + $handle, + 'in_footer', + 'strategy', + 'defer' + ), + '7.0.0' + ); + } } } @@ -221,6 +247,10 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) { * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. * @type array> $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. * For the full data format, see the `$deps` param of {@see wp_register_script_module()}. + * When provided, the script must either be printed in the footer (with + * `in_footer` set to true) or use a deferred loading `strategy` (`defer`), + * so that the script modules import map is printed before the script + * is evaluated. Otherwise dynamic imports may fail to resolve. * } * @return bool Whether the script has been registered. True on success, false on failure. */ @@ -403,6 +433,10 @@ function wp_deregister_script( $handle ) { * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. * @type array> $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. * For the full data format, see the `$deps` param of {@see wp_register_script_module()}. + * When provided, the script must either be printed in the footer (with + * `in_footer` set to true) or use a deferred loading `strategy` (`defer`), + * so that the script modules import map is printed before the script + * is evaluated. Otherwise dynamic imports may fail to resolve. * } */ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) { diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 5f1c30fe4cf47..abd0b8f4881d7 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -1396,6 +1396,203 @@ public function data_add_data_module_dependencies_validation(): array { ); } + /** + * Tests that registering a script with `module_dependencies` triggers `_doing_it_wrong` + * when the script is not printed in the footer and does not use the `defer` strategy. + * + * @ticket 65165 + * + * @covers ::wp_register_script + * @covers ::wp_enqueue_script + * @covers ::_wp_scripts_add_args_data + * + * @dataProvider data_module_dependencies_require_footer_or_defer + * + * @param string $function_name Function name to call. + * @param array $args Arguments to pass to the function. + * @param bool $should_warn Whether the call is expected to trigger a `_doing_it_wrong` warning. + */ + public function test_module_dependencies_require_footer_or_defer( string $function_name, array $args, bool $should_warn ) { + if ( $should_warn ) { + $this->setExpectedIncorrectUsage( $function_name ); + } + + call_user_func_array( $function_name, $args ); + + if ( $should_warn ) { + $this->assertStringContainsString( + 'module_dependencies', + $this->caught_doing_it_wrong[ $function_name ], + 'The _doing_it_wrong message should reference module_dependencies.' + ); + $this->assertStringContainsString( + 'in_footer', + $this->caught_doing_it_wrong[ $function_name ], + 'The _doing_it_wrong message should reference the in_footer requirement.' + ); + $this->assertStringContainsString( + 'defer', + $this->caught_doing_it_wrong[ $function_name ], + 'The _doing_it_wrong message should reference the defer strategy.' + ); + } else { + $this->assertArrayNotHasKey( + $function_name, + $this->caught_doing_it_wrong, + 'No _doing_it_wrong warning should be triggered when in_footer is true or strategy is defer.' + ); + } + } + + /** + * Data provider for test_module_dependencies_require_footer_or_defer. + * + * @return array + */ + public function data_module_dependencies_require_footer_or_defer(): array { + $base_args = array( + '/script.js', + array(), + null, + ); + + return array( + 'register_blocking_warns' => array( + 'function_name' => 'wp_register_script', + 'args' => array_merge( + array( 'module-deps-blocking-register' ), + $base_args, + array( + array( + 'module_dependencies' => array( 'foo' ), + ), + ) + ), + 'should_warn' => true, + ), + 'enqueue_blocking_warns' => array( + 'function_name' => 'wp_enqueue_script', + 'args' => array_merge( + array( 'module-deps-blocking-enqueue' ), + $base_args, + array( + array( + 'module_dependencies' => array( 'foo' ), + ), + ) + ), + 'should_warn' => true, + ), + 'register_async_warns' => array( + 'function_name' => 'wp_register_script', + 'args' => array_merge( + array( 'module-deps-async-register' ), + $base_args, + array( + array( + 'module_dependencies' => array( 'foo' ), + 'strategy' => 'async', + ), + ) + ), + 'should_warn' => true, + ), + 'register_in_footer_does_not_warn' => array( + 'function_name' => 'wp_register_script', + 'args' => array_merge( + array( 'module-deps-footer-register' ), + $base_args, + array( + array( + 'module_dependencies' => array( 'foo' ), + 'in_footer' => true, + ), + ) + ), + 'should_warn' => false, + ), + 'enqueue_in_footer_does_not_warn' => array( + 'function_name' => 'wp_enqueue_script', + 'args' => array_merge( + array( 'module-deps-footer-enqueue' ), + $base_args, + array( + array( + 'module_dependencies' => array( 'foo' ), + 'in_footer' => true, + ), + ) + ), + 'should_warn' => false, + ), + 'register_defer_does_not_warn' => array( + 'function_name' => 'wp_register_script', + 'args' => array_merge( + array( 'module-deps-defer-register' ), + $base_args, + array( + array( + 'module_dependencies' => array( 'foo' ), + 'strategy' => 'defer', + ), + ) + ), + 'should_warn' => false, + ), + 'enqueue_defer_does_not_warn' => array( + 'function_name' => 'wp_enqueue_script', + 'args' => array_merge( + array( 'module-deps-defer-enqueue' ), + $base_args, + array( + array( + 'module_dependencies' => array( 'foo' ), + 'strategy' => 'defer', + ), + ) + ), + 'should_warn' => false, + ), + 'register_footer_and_defer_no_warn' => array( + 'function_name' => 'wp_register_script', + 'args' => array_merge( + array( 'module-deps-footer-defer-register' ), + $base_args, + array( + array( + 'module_dependencies' => array( 'foo' ), + 'in_footer' => true, + 'strategy' => 'defer', + ), + ) + ), + 'should_warn' => false, + ), + 'register_no_module_deps_no_warn' => array( + 'function_name' => 'wp_register_script', + 'args' => array_merge( + array( 'no-module-deps-register' ), + $base_args, + array( array() ) + ), + 'should_warn' => false, + ), + 'register_empty_module_deps_no_warn' => array( + 'function_name' => 'wp_register_script', + 'args' => array_merge( + array( 'empty-module-deps-register' ), + $base_args, + array( + array( + 'module_dependencies' => array(), + ), + ) + ), + 'should_warn' => false, + ), + ); + } + /** * Data provider. * diff --git a/tests/phpunit/tests/script-modules/wpScriptModules.php b/tests/phpunit/tests/script-modules/wpScriptModules.php index 330736431dffd..2f4a330b40981 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -2041,6 +2041,7 @@ public function test_included_module_appears_in_importmap() { array( 'classic-dependency' ), false, array( + 'in_footer' => true, 'module_dependencies' => array( 'example', array( @@ -2109,6 +2110,7 @@ public function test_import_map_includes_dependencies_of_classic_scripts_recursi array(), false, array( + 'in_footer' => true, 'module_dependencies' => array( 'classic-transitive-dependency' ), ) ); @@ -2118,6 +2120,7 @@ public function test_import_map_includes_dependencies_of_classic_scripts_recursi array( 'classic-transitive-dep' ), false, array( + 'in_footer' => true, 'module_dependencies' => array( 'not-enqueued' ), ) ); @@ -2153,6 +2156,7 @@ public function test_wp_scripts_doing_it_wrong_for_missing_script_module_depende array(), null, array( + 'in_footer' => true, 'module_dependencies' => array( 'does-not-exist' ), ) ); From 34a953e0e4f7288c94ffbc0ec6d4e3895c09d7f5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 16:43:42 -0700 Subject: [PATCH 02/16] Fix array index alignment for PHPCS --- tests/phpunit/tests/dependencies/scripts.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index abd0b8f4881d7..25fb6a3c04714 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -1457,7 +1457,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ); return array( - 'register_blocking_warns' => array( + 'register_blocking_warns' => array( 'function_name' => 'wp_register_script', 'args' => array_merge( array( 'module-deps-blocking-register' ), @@ -1470,7 +1470,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => true, ), - 'enqueue_blocking_warns' => array( + 'enqueue_blocking_warns' => array( 'function_name' => 'wp_enqueue_script', 'args' => array_merge( array( 'module-deps-blocking-enqueue' ), @@ -1483,7 +1483,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => true, ), - 'register_async_warns' => array( + 'register_async_warns' => array( 'function_name' => 'wp_register_script', 'args' => array_merge( array( 'module-deps-async-register' ), @@ -1497,7 +1497,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => true, ), - 'register_in_footer_does_not_warn' => array( + 'register_in_footer_does_not_warn' => array( 'function_name' => 'wp_register_script', 'args' => array_merge( array( 'module-deps-footer-register' ), @@ -1511,7 +1511,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => false, ), - 'enqueue_in_footer_does_not_warn' => array( + 'enqueue_in_footer_does_not_warn' => array( 'function_name' => 'wp_enqueue_script', 'args' => array_merge( array( 'module-deps-footer-enqueue' ), @@ -1525,7 +1525,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => false, ), - 'register_defer_does_not_warn' => array( + 'register_defer_does_not_warn' => array( 'function_name' => 'wp_register_script', 'args' => array_merge( array( 'module-deps-defer-register' ), @@ -1539,7 +1539,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => false, ), - 'enqueue_defer_does_not_warn' => array( + 'enqueue_defer_does_not_warn' => array( 'function_name' => 'wp_enqueue_script', 'args' => array_merge( array( 'module-deps-defer-enqueue' ), @@ -1553,7 +1553,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => false, ), - 'register_footer_and_defer_no_warn' => array( + 'register_footer_and_defer_no_warn' => array( 'function_name' => 'wp_register_script', 'args' => array_merge( array( 'module-deps-footer-defer-register' ), @@ -1568,7 +1568,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => false, ), - 'register_no_module_deps_no_warn' => array( + 'register_no_module_deps_no_warn' => array( 'function_name' => 'wp_register_script', 'args' => array_merge( array( 'no-module-deps-register' ), @@ -1577,7 +1577,7 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => false, ), - 'register_empty_module_deps_no_warn' => array( + 'register_empty_module_deps_no_warn' => array( 'function_name' => 'wp_register_script', 'args' => array_merge( array( 'empty-module-deps-register' ), From cc69aaaba2d6620c0c6d3f76d58069e58a0ba4af Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 17:03:26 -0700 Subject: [PATCH 03/16] Fix phpstan issues in tests and improve phpdoc --- tests/phpunit/includes/abstract-testcase.php | 4 ++- tests/phpunit/tests/dependencies/scripts.php | 31 ++++++++++++++++---- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php index 3a5d52b0706a9..b8e8598362ec5 100644 --- a/tests/phpunit/includes/abstract-testcase.php +++ b/tests/phpunit/includes/abstract-testcase.php @@ -18,7 +18,9 @@ abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase { protected $expected_deprecated = array(); protected $caught_deprecated = array(); protected $expected_doing_it_wrong = array(); - protected $caught_doing_it_wrong = array(); + + /** @var non-empty-string[] */ + protected $caught_doing_it_wrong = array(); protected static $hooks_saved = array(); protected static $ignore_files; diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 25fb6a3c04714..a1845e523a3e8 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -8,6 +8,19 @@ * @covers ::wp_script_add_data * @covers ::wp_add_inline_script * @covers ::wp_set_script_translations + * + * @phpstan-type ScriptArgs array{ + * in_footer?: bool, + * module_dependencies?: non-empty-string[], + * strategy?: 'async'|'defer', + * } + * @phpstan-type WpEnqueueScriptArgs array{ + * 0: non-empty-string, // $handle + * 1?: non-empty-string, // $src + * 2?: non-empty-string[], // $deps + * 3?: null|bool|string, // $version + * 4?: ScriptArgs, + * } */ class Tests_Dependencies_Scripts extends WP_UnitTestCase { @@ -1408,11 +1421,13 @@ public function data_add_data_module_dependencies_validation(): array { * * @dataProvider data_module_dependencies_require_footer_or_defer * - * @param string $function_name Function name to call. - * @param array $args Arguments to pass to the function. - * @param bool $should_warn Whether the call is expected to trigger a `_doing_it_wrong` warning. + * @param callable-string $function_name Function name to call. + * @param array $args Arguments to pass to the function. + * @param bool $should_warn Whether the call is expected to trigger a `_doing_it_wrong` warning. + * + * @phpstan-param WpEnqueueScriptArgs $args */ - public function test_module_dependencies_require_footer_or_defer( string $function_name, array $args, bool $should_warn ) { + public function test_module_dependencies_require_footer_or_defer( string $function_name, array $args, bool $should_warn ): void { if ( $should_warn ) { $this->setExpectedIncorrectUsage( $function_name ); } @@ -1445,9 +1460,13 @@ public function test_module_dependencies_require_footer_or_defer( string $functi } /** - * Data provider for test_module_dependencies_require_footer_or_defer. + * Data provider for {@see self::test_module_dependencies_require_footer_or_defer()}. * - * @return array + * @phpstan-return array */ public function data_module_dependencies_require_footer_or_defer(): array { $base_args = array( From 22b2849ddf79b58dda7b26e174b71654b3f40302 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 17:10:44 -0700 Subject: [PATCH 04/16] Use defer strategy instead of in_footer for half of scripts --- tests/phpunit/tests/script-modules/wpScriptModules.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/script-modules/wpScriptModules.php b/tests/phpunit/tests/script-modules/wpScriptModules.php index 2f4a330b40981..09b5a91e2896f 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -2041,7 +2041,7 @@ public function test_included_module_appears_in_importmap() { array( 'classic-dependency' ), false, array( - 'in_footer' => true, + 'strategy' => 'defer', 'module_dependencies' => array( 'example', array( @@ -2156,7 +2156,7 @@ public function test_wp_scripts_doing_it_wrong_for_missing_script_module_depende array(), null, array( - 'in_footer' => true, + 'strategy' => 'defer', 'module_dependencies' => array( 'does-not-exist' ), ) ); From 155b3fc198e63a22cc362aa9bcf74c4fea746b90 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 17:36:30 -0700 Subject: [PATCH 05/16] Prevent passing non-string value into sprintf() --- src/wp-includes/class-wp-scripts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index cb37b2b653877..6f633d465bb2c 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -885,7 +885,7 @@ public function add_data( $handle, $key, $value ) { sprintf( /* translators: 1: $strategy, 2: $handle */ __( 'Invalid strategy `%1$s` defined for `%2$s` during script registration.' ), - $value, + is_string( $value ) ? $value : gettype( $value ), $handle ), '6.3.0' @@ -897,7 +897,7 @@ public function add_data( $handle, $key, $value ) { sprintf( /* translators: 1: $strategy, 2: $handle */ __( 'Cannot supply a strategy `%1$s` for script `%2$s` because it is an alias (it lacks a `src` value).' ), - $value, + is_string( $value ) ? $value : gettype( $value ), $handle ), '6.3.0' From 20e80c1312558d2e67caa20ef92d584b89c5540d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 17:41:05 -0700 Subject: [PATCH 06/16] Add missing phpstan types --- src/wp-includes/functions.wp-scripts.php | 96 +++++++++++++------- tests/phpunit/tests/dependencies/scripts.php | 3 +- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index 96b48ef33169c..346148d8dd19b 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -71,14 +71,25 @@ function _wp_scripts_maybe_doing_it_wrong( $function_name, $handle = '' ) { /** * Adds the data for the recognized args and warns for unrecognized args. * + * @see wp_enqueue_script() + * @see wp_register_script() + * * @ignore * @since 7.0.0 * * @param WP_Scripts $wp_scripts WP_Scripts instance. * @param string $handle Script handle. * @param array $args Array of extra args for the script. + * + * @phpstan-param non-empty-string $handle + * @phpstan-param array{ + * in_footer?: bool, + * strategy?: 'async'|'defer', + * fetchpriority?: 'low'|'auto'|'high', + * module_dependencies?: array, + * } $args */ -function _wp_scripts_add_args_data( WP_Scripts $wp_scripts, string $handle, array $args ) { +function _wp_scripts_add_args_data( WP_Scripts $wp_scripts, string $handle, array $args ): void { $allowed_keys = array( 'strategy', 'in_footer', 'fetchpriority', 'module_dependencies' ); $unknown_keys = array_diff( array_keys( $args ), $allowed_keys ); if ( ! empty( $unknown_keys ) ) { @@ -230,22 +241,22 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) { * @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array. * @since 7.0.0 The $module_dependencies parameter of type string[] was added to the $args parameter of type array. * - * @param string $handle Name of the script. Should be unique. - * @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory. - * If source is set to false, script is an alias of other scripts it depends on. - * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. - * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL - * as a query string for cache busting purposes. If version is set to false, a version - * number is automatically added equal to current installed WordPress version. - * If set to null, no version is added. - * @param array>>|bool $args { + * @param string $handle Name of the script. Should be unique. + * @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory. + * If source is set to false, script is an alias of other scripts it depends on. + * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. + * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL + * as a query string for cache busting purposes. If version is set to false, a version + * number is automatically added equal to current installed WordPress version. + * If set to null, no version is added. + * @param array|bool $args { * Optional. An array of extra args for the script. Default empty array. * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. * - * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. - * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. - * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. - * @type array> $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. + * @type array $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. * For the full data format, see the `$deps` param of {@see wp_register_script_module()}. * When provided, the script must either be printed in the footer (with * `in_footer` set to true) or use a deferred loading `strategy` (`defer`), @@ -253,6 +264,16 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) { * is evaluated. Otherwise dynamic imports may fail to resolve. * } * @return bool Whether the script has been registered. True on success, false on failure. + * + * @phpstan-param non-empty-string $handle + * @phpstan-param non-empty-string|false $src + * @phpstan-param non-empty-string[] $deps + * @phpstan-param array{ + * in_footer?: bool, + * strategy?: 'async'|'defer', + * fetchpriority?: 'low'|'auto'|'high', + * module_dependencies?: array, + * }|bool $args */ function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args = array() ) { if ( ! is_array( $args ) ) { @@ -416,28 +437,38 @@ function wp_deregister_script( $handle ) { * @since 6.9.0 The $fetchpriority parameter of type string was added to the $args parameter of type array. * @since 7.0.0 The $module_dependencies parameter of type string[] was added to the $args parameter of type array. * - * @param string $handle Name of the script. Should be unique. - * @param string $src Full URL of the script, or path of the script relative to the WordPress root directory. - * Default empty. - * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. - * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL - * as a query string for cache busting purposes. If version is set to false, a version - * number is automatically added equal to current installed WordPress version. - * If set to null, no version is added. - * @param array>>|bool $args { + * @param string $handle Name of the script. Should be unique. + * @param string $src Full URL of the script, or path of the script relative to the WordPress root directory. + * Default empty. + * @param string[] $deps Optional. An array of registered script handles this script depends on. Default empty array. + * @param string|bool|null $ver Optional. String specifying script version number, if it has one, which is added to the URL + * as a query string for cache busting purposes. If version is set to false, a version + * number is automatically added equal to current installed WordPress version. + * If set to null, no version is added. + * @param array|bool $args { * Optional. An array of extra args for the script. Default empty array. * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. * - * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. - * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. - * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. - * @type array> $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. - * For the full data format, see the `$deps` param of {@see wp_register_script_module()}. - * When provided, the script must either be printed in the footer (with - * `in_footer` set to true) or use a deferred loading `strategy` (`defer`), - * so that the script modules import map is printed before the script - * is evaluated. Otherwise dynamic imports may fail to resolve. + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * @type string $fetchpriority Optional. The fetch priority for the script. Default 'auto'. + * @type array $module_dependencies Optional. IDs for module dependencies loaded via dynamic import. Default empty array. + * For the full data format, see the `$deps` param of {@see wp_register_script_module()}. + * When provided, the script must either be printed in the footer (with + * `in_footer` set to true) or use a deferred loading `strategy` (`defer`), + * so that the script modules import map is printed before the script + * is evaluated. Otherwise dynamic imports may fail to resolve. * } + * + * @phpstan-param non-empty-string $handle + * @phpstan-param string $src + * @phpstan-param non-empty-string[] $deps + * @phpstan-param array{ + * in_footer?: bool, + * strategy?: 'async'|'defer', + * fetchpriority?: 'low'|'auto'|'high', + * module_dependencies?: non-empty-string[], + * }|bool $args */ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); @@ -445,6 +476,7 @@ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $ $wp_scripts = wp_scripts(); if ( $src || ! empty( $args ) ) { + /** @var array{ 0: non-empty-string, 1?: string } $_handle */ $_handle = explode( '?', $handle ); if ( ! is_array( $args ) ) { $args = array( diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index a1845e523a3e8..260614c46c265 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -11,8 +11,9 @@ * * @phpstan-type ScriptArgs array{ * in_footer?: bool, - * module_dependencies?: non-empty-string[], * strategy?: 'async'|'defer', + * fetchpriority?: 'low'|'auto'|'high', + * module_dependencies?: array, * } * @phpstan-type WpEnqueueScriptArgs array{ * 0: non-empty-string, // $handle From 1e44dd78bfe1fd8cd7e26b73da3df8d8a05f2f49 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 17:54:22 -0700 Subject: [PATCH 07/16] Use null coalescing and re-use computed in_footer --- src/wp-includes/functions.wp-scripts.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index 346148d8dd19b..99a5c1f62e395 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -108,7 +108,8 @@ function _wp_scripts_add_args_data( WP_Scripts $wp_scripts, string $handle, arra ); } - if ( ! empty( $args['in_footer'] ) ) { + $in_footer = ! empty( $args['in_footer'] ); + if ( $in_footer ) { $wp_scripts->add_data( $handle, 'group', 1 ); } if ( ! empty( $args['strategy'] ) ) { @@ -126,9 +127,8 @@ function _wp_scripts_add_args_data( WP_Scripts $wp_scripts, string $handle, arra * evaluated before the script modules import map is printed, causing * dynamic imports to fail with a "Failed to resolve module specifier" error. */ - $is_in_footer = ! empty( $args['in_footer'] ); - $is_deferred = isset( $args['strategy'] ) && 'defer' === $args['strategy']; - if ( ! $is_in_footer && ! $is_deferred ) { + $is_deferred = 'defer' === ( $args['strategy'] ?? null ); + if ( ! $in_footer && ! $is_deferred ) { $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 ); $function_name = ( $trace[1]['class'] ?? '' ) . ( $trace[1]['type'] ?? '' ) . $trace[1]['function']; _doing_it_wrong( From 10b6e8bfd702b655fa462433e67ecb9385699195 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 18:10:24 -0700 Subject: [PATCH 08/16] Align module_dependencies phpstan shape with wp_register_script wp_enqueue_script() and wp_register_script() flow through the same _wp_scripts_add_args_data(), so their @phpstan-param shapes for `module_dependencies` should match. Use the union shape that allows both bare handle strings and `{ id: ... }` arrays. Co-Authored-By: Claude Opus 4.7 (1M context) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/functions.wp-scripts.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index 99a5c1f62e395..fe9deadfafeb4 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -467,7 +467,7 @@ function wp_deregister_script( $handle ) { * in_footer?: bool, * strategy?: 'async'|'defer', * fetchpriority?: 'low'|'auto'|'high', - * module_dependencies?: non-empty-string[], + * module_dependencies?: array, * }|bool $args */ function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) { From c0e166d304a35fcaf54714c398d0c9f3906ec221 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 18:12:25 -0700 Subject: [PATCH 09/16] Add async test case Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpunit/tests/dependencies/scripts.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 260614c46c265..a9ff51095f3dc 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -1517,6 +1517,20 @@ public function data_module_dependencies_require_footer_or_defer(): array { ), 'should_warn' => true, ), + 'enqueue_async_warns' => array( + 'function_name' => 'wp_enqueue_script', + 'args' => array_merge( + array( 'module-deps-async-enqueue' ), + $base_args, + array( + array( + 'module_dependencies' => array( 'foo' ), + 'strategy' => 'async', + ), + ) + ), + 'should_warn' => true, + ), 'register_in_footer_does_not_warn' => array( 'function_name' => 'wp_register_script', 'args' => array_merge( From 21d5f553522c5bc1e8790213bc653367b21cca0a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 18:18:33 -0700 Subject: [PATCH 10/16] Fall back to __FUNCTION__ when debug_backtrace lacks a caller frame `$trace[1]['function']` was read without a null-coalesce, which would emit a PHP notice if _wp_scripts_add_args_data() were ever invoked without a caller frame (e.g. directly from the top level). Default to the current function name so the _doing_it_wrong() attribution always has a non-null value. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/wp-includes/functions.wp-scripts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/functions.wp-scripts.php b/src/wp-includes/functions.wp-scripts.php index fe9deadfafeb4..5e9589252124b 100644 --- a/src/wp-includes/functions.wp-scripts.php +++ b/src/wp-includes/functions.wp-scripts.php @@ -94,7 +94,7 @@ function _wp_scripts_add_args_data( WP_Scripts $wp_scripts, string $handle, arra $unknown_keys = array_diff( array_keys( $args ), $allowed_keys ); if ( ! empty( $unknown_keys ) ) { $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 ); - $function_name = ( $trace[1]['class'] ?? '' ) . ( $trace[1]['type'] ?? '' ) . $trace[1]['function']; + $function_name = ( $trace[1]['class'] ?? '' ) . ( $trace[1]['type'] ?? '' ) . ( $trace[1]['function'] ?? __FUNCTION__ ); _doing_it_wrong( $function_name, sprintf( @@ -130,7 +130,7 @@ function _wp_scripts_add_args_data( WP_Scripts $wp_scripts, string $handle, arra $is_deferred = 'defer' === ( $args['strategy'] ?? null ); if ( ! $in_footer && ! $is_deferred ) { $trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS, 2 ); - $function_name = ( $trace[1]['class'] ?? '' ) . ( $trace[1]['type'] ?? '' ) . $trace[1]['function']; + $function_name = ( $trace[1]['class'] ?? '' ) . ( $trace[1]['type'] ?? '' ) . ( $trace[1]['function'] ?? __FUNCTION__ ); _doing_it_wrong( $function_name, sprintf( From ec1ff7146f91df7eccc6342561e9db90cf577906 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 22:01:52 -0700 Subject: [PATCH 11/16] Bypass use of importmap --- .../lib/codemirror/javascript-lint.js | 7 ++++- src/wp-includes/general-template.php | 31 ++++++++++--------- src/wp-includes/script-loader.php | 1 - 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/js/_enqueues/lib/codemirror/javascript-lint.js b/src/js/_enqueues/lib/codemirror/javascript-lint.js index 592d077b80914..0e7e862f61280 100644 --- a/src/js/_enqueues/lib/codemirror/javascript-lint.js +++ b/src/js/_enqueues/lib/codemirror/javascript-lint.js @@ -30,6 +30,7 @@ import CodeMirror from 'codemirror'; * @property {boolean} [es3] - "This option tells JSHint that your code needs to adhere to ECMAScript 3 specification. Use this option if you need your program to be executable in older browsers—such as Internet Explorer 6/7/8/9—and other legacy JavaScript environments." * @property {boolean} [module] - "This option informs JSHint that the input code describes an ECMAScript 6 module. All module code is interpreted as strict mode code." * @property {'implied'} [strict] - "This option requires the code to run in ECMAScript 5's strict mode." + * @property {string|null} espreeModuleUrl - The URL to the espree script module. */ /** @@ -42,9 +43,13 @@ import CodeMirror from 'codemirror'; * @returns {Promise} */ async function validator( text, options ) { + if ( ! options.espreeModuleUrl ) { + return []; + } + const errors = /** @type {CodeMirrorLintError[]} */ []; try { - const espree = await import( /* webpackIgnore: true */ 'espree' ); + const espree = await import( /* webpackIgnore: true */ options.espreeModuleUrl ); espree.parse( text, { ...getEspreeOptions( options ), loc: true, diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 47e2aeb2ebb05..63df169a7933c 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -4155,27 +4155,28 @@ function wp_get_code_editor_settings( $args ) { 'outline-none' => true, ), 'jshint' => array( - 'esversion' => 11, - 'module' => str_ends_with( $args['file'] ?? '', '.mjs' ), + 'esversion' => 11, + 'module' => str_ends_with( $args['file'] ?? '', '.mjs' ), + 'espreeModuleUrl' => wp_script_modules()->get_registered( 'espree' )['src'] ?? null, // The following JSHint *linting rule* options are copied from // . // Parsing-related options such as `esversion` (and, in other contexts, `es5`, `es3`, `module`, `strict`) // are honored by the Espree-based integration, but these linting-rule options are not interpreted by Espree // and are kept only for compatibility/documentation with the original JSHint configuration. - 'boss' => true, - 'curly' => true, - 'eqeqeq' => true, - 'eqnull' => true, - 'expr' => true, - 'immed' => true, - 'noarg' => true, - 'nonbsp' => true, - 'quotmark' => 'single', - 'undef' => true, - 'unused' => true, - 'browser' => true, - 'globals' => array( + 'boss' => true, + 'curly' => true, + 'eqeqeq' => true, + 'eqnull' => true, + 'expr' => true, + 'immed' => true, + 'noarg' => true, + 'nonbsp' => true, + 'quotmark' => 'single', + 'undef' => true, + 'unused' => true, + 'browser' => true, + 'globals' => array( '_' => false, 'Backbone' => false, 'jQuery' => false, diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 42d42b3f8781d..2592310c23d70 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1200,7 +1200,6 @@ function wp_default_scripts( $scripts ) { ); $scripts->add( 'wp-codemirror', '/wp-includes/js/codemirror/codemirror.min.js', array(), '5.65.20' ); - did_action( 'init' ) && $scripts->add_data( 'wp-codemirror', 'module_dependencies', array( 'espree' ) ); $scripts->add( 'csslint', '/wp-includes/js/codemirror/csslint.js', array(), '1.0.5' ); $scripts->add( 'esprima', '/wp-includes/js/codemirror/esprima.js', array(), '4.0.1' ); // Deprecated. Use 'espree' script module. $scripts->add( 'jshint', '/wp-includes/js/codemirror/fakejshint.js', array( 'esprima' ), '2.9.5' ); // Deprecated. From 527e40f2b65eba813549d1df244b960186c3b24c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 13 May 2026 22:16:27 -0700 Subject: [PATCH 12/16] Add missing espreeModuleUrl key to unit tests --- tests/phpunit/tests/dependencies/scripts.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index a9ff51095f3dc..41c9673915b93 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -3400,6 +3400,7 @@ public function test_wp_enqueue_code_editor_when_php_file_will_be_passed() { 'unused', 'browser', 'globals', + 'espreeModuleUrl', ), array_keys( $wp_enqueue_code_editor['jshint'] ) ); @@ -3483,6 +3484,7 @@ public function test_wp_enqueue_code_editor_when_generated_array_by_compact_will 'unused', 'browser', 'globals', + 'espreeModuleUrl', ), array_keys( $wp_enqueue_code_editor['jshint'] ) ); @@ -3580,6 +3582,7 @@ public function test_wp_enqueue_code_editor_when_generated_array_by_array_merge_ 'unused', 'browser', 'globals', + 'espreeModuleUrl', ), array_keys( $wp_enqueue_code_editor['jshint'] ) ); @@ -3674,6 +3677,7 @@ public function test_wp_enqueue_code_editor_when_simple_array_will_be_passed() { 'unused', 'browser', 'globals', + 'espreeModuleUrl', ), array_keys( $wp_enqueue_code_editor['jshint'] ) ); From afff995c140b9158250652bea3f9b1504f0a7021 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 14 May 2026 12:26:48 -0700 Subject: [PATCH 13/16] Eliminate espree from being a publicly-registered script module Co-Authored-By: Jon Surrell --- src/wp-includes/general-template.php | 5 ++++- src/wp-includes/script-loader.php | 2 +- src/wp-includes/script-modules.php | 7 ------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index 63df169a7933c..34233c35b0cc3 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -4157,7 +4157,10 @@ function wp_get_code_editor_settings( $args ) { 'jshint' => array( 'esversion' => 11, 'module' => str_ends_with( $args['file'] ?? '', '.mjs' ), - 'espreeModuleUrl' => wp_script_modules()->get_registered( 'espree' )['src'] ?? null, + + // This script module URL is intentionally referenced here instead of registering an espree script module + // in wp_default_script_modules(). This is a first stab at a core-only private module. + 'espreeModuleUrl' => add_query_arg( 'ver', '9.6.1', includes_url( 'js/codemirror/espree.min.js' ) ), // The following JSHint *linting rule* options are copied from // . diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 2592310c23d70..f4fad38f3eefe 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1201,7 +1201,7 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'wp-codemirror', '/wp-includes/js/codemirror/codemirror.min.js', array(), '5.65.20' ); $scripts->add( 'csslint', '/wp-includes/js/codemirror/csslint.js', array(), '1.0.5' ); - $scripts->add( 'esprima', '/wp-includes/js/codemirror/esprima.js', array(), '4.0.1' ); // Deprecated. Use 'espree' script module. + $scripts->add( 'esprima', '/wp-includes/js/codemirror/esprima.js', array(), '4.0.1' ); // Deprecated. $scripts->add( 'jshint', '/wp-includes/js/codemirror/fakejshint.js', array( 'esprima' ), '2.9.5' ); // Deprecated. $scripts->add( 'jsonlint', '/wp-includes/js/codemirror/jsonlint.js', array(), '1.6.3' ); $scripts->add( 'htmlhint', '/wp-includes/js/codemirror/htmlhint.js', array(), '1.8.0' ); diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index b3a89fdf71844..32863a6a8ab00 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -219,13 +219,6 @@ function wp_default_script_modules() { $module_deps = $script_module_data['module_dependencies'] ?? array(); wp_register_script_module( $script_module_id, $path, $module_deps, $script_module_data['version'], $args ); } - - wp_register_script_module( - 'espree', - includes_url( 'js/codemirror/espree.min.js' ), - array(), - '9.6.1' - ); } /** From 3866c5547ab59ec74cf674bb375dcdf1bc06e1d0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 14 May 2026 13:14:00 -0700 Subject: [PATCH 14/16] Warn when invoking wp.codeEditor.initialize() before DCL --- src/js/_enqueues/wp/code-editor.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/js/_enqueues/wp/code-editor.js b/src/js/_enqueues/wp/code-editor.js index 86d5e03254166..7145f4c7ba9ed 100644 --- a/src/js/_enqueues/wp/code-editor.js +++ b/src/js/_enqueues/wp/code-editor.js @@ -2,6 +2,8 @@ * @output wp-admin/js/code-editor.js */ +/* global console */ + /* eslint-env es2020 */ if ( 'undefined' === typeof window.wp ) { @@ -412,6 +414,10 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { * @return {CodeEditorInstance} Instance. */ wp.codeEditor.initialize = function initialize( textarea, settings ) { + if ( document.readyState === 'loading' ) { + console.warn( 'wp.codeEditor.initialize() should be invoked at or after DOMContentLoaded. This is to ensure the importmap has been parsed prior to performing the dynamic import of espree in javascript-lint.js.' ); + } + let $textarea; if ( 'string' === typeof textarea ) { $textarea = $( '#' + textarea ); From 6de4e00337ce4c5c7c58d5e3054436852b082334 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 15 May 2026 10:03:47 -0700 Subject: [PATCH 15/16] Refine console warning Co-authored-by: Jon Surrell --- src/js/_enqueues/wp/code-editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/_enqueues/wp/code-editor.js b/src/js/_enqueues/wp/code-editor.js index 7145f4c7ba9ed..ed8be9d6a5580 100644 --- a/src/js/_enqueues/wp/code-editor.js +++ b/src/js/_enqueues/wp/code-editor.js @@ -415,7 +415,7 @@ if ( 'undefined' === typeof window.wp.codeEditor ) { */ wp.codeEditor.initialize = function initialize( textarea, settings ) { if ( document.readyState === 'loading' ) { - console.warn( 'wp.codeEditor.initialize() should be invoked at or after DOMContentLoaded. This is to ensure the importmap has been parsed prior to performing the dynamic import of espree in javascript-lint.js.' ); + console.warn( 'wp.codeEditor.initialize() ran too early. Invoke this function in a `DOMContentLoaded` event listener.' ); } let $textarea; From 903391f76d97ecb726ec860bc24d597d06b66303 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 15 May 2026 22:00:10 -0700 Subject: [PATCH 16/16] Correct espreeModuleUrl JSDoc type It is always a string URL (never null) and is optional. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/js/_enqueues/lib/codemirror/javascript-lint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/_enqueues/lib/codemirror/javascript-lint.js b/src/js/_enqueues/lib/codemirror/javascript-lint.js index 0e7e862f61280..2c96798a20ae3 100644 --- a/src/js/_enqueues/lib/codemirror/javascript-lint.js +++ b/src/js/_enqueues/lib/codemirror/javascript-lint.js @@ -30,7 +30,7 @@ import CodeMirror from 'codemirror'; * @property {boolean} [es3] - "This option tells JSHint that your code needs to adhere to ECMAScript 3 specification. Use this option if you need your program to be executable in older browsers—such as Internet Explorer 6/7/8/9—and other legacy JavaScript environments." * @property {boolean} [module] - "This option informs JSHint that the input code describes an ECMAScript 6 module. All module code is interpreted as strict mode code." * @property {'implied'} [strict] - "This option requires the code to run in ECMAScript 5's strict mode." - * @property {string|null} espreeModuleUrl - The URL to the espree script module. + * @property {string} [espreeModuleUrl] - The URL to the espree script module. */ /**