From c42ebcd009b93c211cb284535a29f1d3697907e0 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 10:56:29 +0400 Subject: [PATCH 1/3] Script Loader: Resolve the global script modules instance in hook callbacks. `WP_Script_Modules::add_hooks()` registered every print callback as `array( $this, 'method' )`, binding the callbacks to the instance that was the global when `wp-settings.php` ran `add_hooks()` on `after_setup_theme`. Replacing the `$wp_script_modules` global after that point changed where `wp_enqueue_script_module()` wrote, but not what was printed. Classic scripts and styles do not have this problem because their hooks are procedural functions that read the global when they run. Add procedural print functions that delegate to `wp_script_modules()` and register those instead. See #66100, #64484. --- src/wp-includes/class-wp-script-modules.php | 27 +++---- src/wp-includes/script-modules.php | 77 +++++++++++++++++++ .../tests/script-modules/wpScriptModules.php | 31 ++++++++ 3 files changed, 122 insertions(+), 13 deletions(-) diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index 9c9f8a762af5e..a41117d21c386 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -429,11 +429,12 @@ public function print_script_module_translations(): void { * footer. * * @since 6.5.0 + * @since 7.2.0 The callbacks are procedural functions that resolve the current global instance when they run. */ public function add_hooks() { $is_block_theme = wp_is_block_theme(); $position = $is_block_theme ? 'wp_head' : 'wp_footer'; - add_action( $position, array( $this, 'print_import_map' ) ); + add_action( $position, 'wp_print_script_module_import_map' ); if ( $is_block_theme ) { /* * Modules can only be printed in the head for block themes because only with @@ -442,14 +443,14 @@ public function add_hooks() { * template rendering, thus the import map must be printed in the footer, * followed by all enqueued modules. */ - add_action( 'wp_head', array( $this, 'print_head_enqueued_script_modules' ) ); + add_action( 'wp_head', 'wp_print_head_script_modules' ); } - add_action( 'wp_footer', array( $this, 'print_enqueued_script_modules' ) ); - add_action( $position, array( $this, 'print_script_module_preloads' ) ); + add_action( 'wp_footer', 'wp_print_script_modules' ); + add_action( $position, 'wp_print_script_module_preloads' ); - add_action( 'admin_print_footer_scripts', array( $this, 'print_import_map' ), 9 ); - add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) ); - add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_preloads' ) ); + add_action( 'admin_print_footer_scripts', 'wp_print_script_module_import_map', 9 ); + add_action( 'admin_print_footer_scripts', 'wp_print_script_modules' ); + add_action( 'admin_print_footer_scripts', 'wp_print_script_module_preloads' ); /* * Print translations after classic scripts like wp-i18n are loaded (at @@ -457,13 +458,13 @@ public function add_hooks() { * execute. Script modules with type="module" are deferred by default, * so inline translation scripts at priority 11 will execute before them. */ - add_action( 'wp_footer', array( $this, 'print_script_module_translations' ), 21 ); - add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_translations' ), 11 ); + add_action( 'wp_footer', 'wp_print_script_module_translations', 21 ); + add_action( 'admin_print_footer_scripts', 'wp_print_script_module_translations', 11 ); - add_action( 'wp_footer', array( $this, 'print_script_module_data' ) ); - add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_data' ) ); - add_action( 'wp_footer', array( $this, 'print_a11y_script_module_html' ), 20 ); - add_action( 'admin_print_footer_scripts', array( $this, 'print_a11y_script_module_html' ), 20 ); + add_action( 'wp_footer', 'wp_print_script_module_data' ); + add_action( 'admin_print_footer_scripts', 'wp_print_script_module_data' ); + add_action( 'wp_footer', 'wp_print_a11y_script_module_html', 20 ); + add_action( 'admin_print_footer_scripts', 'wp_print_a11y_script_module_html', 20 ); } /** diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index 23dc8a68641ad..8f75c5175a76d 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -159,6 +159,83 @@ function wp_set_script_module_translations( string $id, string $domain = 'defaul return wp_script_modules()->set_translations( $id, $domain, $path ); } +/** + * Prints the import map using a script tag with a type="importmap" attribute. + * + * @since 7.2.0 + * + * @see WP_Script_Modules::print_import_map() + */ +function wp_print_script_module_import_map() { + wp_script_modules()->print_import_map(); +} + +/** + * Prints the enqueued script modules in head. + * + * @since 7.2.0 + * + * @see WP_Script_Modules::print_head_enqueued_script_modules() + */ +function wp_print_head_script_modules() { + wp_script_modules()->print_head_enqueued_script_modules(); +} + +/** + * Prints the enqueued script modules in footer. + * + * @since 7.2.0 + * + * @see WP_Script_Modules::print_enqueued_script_modules() + */ +function wp_print_script_modules() { + wp_script_modules()->print_enqueued_script_modules(); +} + +/** + * Prints the static dependencies of the enqueued script modules using link tags with rel="modulepreload" attributes. + * + * @since 7.2.0 + * + * @see WP_Script_Modules::print_script_module_preloads() + */ +function wp_print_script_module_preloads() { + wp_script_modules()->print_script_module_preloads(); +} + +/** + * Prints the translations of the script modules that will be printed on the page. + * + * @since 7.2.0 + * + * @see WP_Script_Modules::print_script_module_translations() + */ +function wp_print_script_module_translations(): void { + wp_script_modules()->print_script_module_translations(); +} + +/** + * Prints the data associated with the script modules that will be printed on the page. + * + * @since 7.2.0 + * + * @see WP_Script_Modules::print_script_module_data() + */ +function wp_print_script_module_data(): void { + wp_script_modules()->print_script_module_data(); +} + +/** + * Prints the HTML regions used by the `@wordpress/a11y` script module. + * + * @since 7.2.0 + * + * @see WP_Script_Modules::print_a11y_script_module_html() + */ +function wp_print_a11y_script_module_html() { + wp_script_modules()->print_a11y_script_module_html(); +} + /** * Registers all the default WordPress Script Modules. * diff --git a/tests/phpunit/tests/script-modules/wpScriptModules.php b/tests/phpunit/tests/script-modules/wpScriptModules.php index 01c58898dcb19..7bf39bb23b8a9 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -2922,4 +2922,35 @@ static function ( $translations, $file, $handle, $domain ) use ( &$seen_domain ) $this->assertSame( 'my-plugin', $seen_domain, 'load_script_module_textdomain() should be called with the overridden domain.' ); $this->assertStringContainsString( 'Hola', $output, 'Output should contain the translated string loaded under the overridden domain.' ); } + + /** + * Tests that the hooks added by add_hooks() print the script modules of the + * current global instance, not the instance that added the hooks. + * + * @ticket 66100 + * @covers WP_Script_Modules::add_hooks + */ + public function test_add_hooks_callbacks_use_the_current_global_instance() { + global $wp_script_modules; + + // Add the hooks the way wp-settings.php does, with the instance that is the global at that moment. + wp_script_modules()->add_hooks(); + + // Replace the global instance after the hooks have been added. + $wp_script_modules = new WP_Script_Modules(); + + wp_enqueue_script_module( 'test-hooks-global', '/test-hooks-global.js' ); + + $output = get_echo( + static function () { + do_action( 'admin_print_footer_scripts' ); + } + ); + + $this->assertStringContainsString( + 'test-hooks-global', + $output, + 'The print callbacks added by add_hooks() must read the current global WP_Script_Modules instance.' + ); + } } From 89a0dbf1af51f14c29c8d8e3972e133694f8bc66 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 09:37:38 +0200 Subject: [PATCH 2/3] Remove redundant since tag --- src/wp-includes/class-wp-script-modules.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index a41117d21c386..ce2cbba16daad 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -429,7 +429,6 @@ public function print_script_module_translations(): void { * footer. * * @since 6.5.0 - * @since 7.2.0 The callbacks are procedural functions that resolve the current global instance when they run. */ public function add_hooks() { $is_block_theme = wp_is_block_theme(); From 42ca0c61b79a8f4b672cf90e5220b35f1e890980 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 13:41:34 +0400 Subject: [PATCH 3/3] Script Loader: Declare void return types on the script module print functions. Five of the seven new procedural print functions declared no return type while `wp_print_script_module_translations()` and `wp_print_script_module_data()` declared `: void`. Declare `: void` on the other five so the set is consistent. None of them returns a value. See #66100. --- src/wp-includes/script-modules.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index 8f75c5175a76d..b2469c1be7a5b 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -166,7 +166,7 @@ function wp_set_script_module_translations( string $id, string $domain = 'defaul * * @see WP_Script_Modules::print_import_map() */ -function wp_print_script_module_import_map() { +function wp_print_script_module_import_map(): void { wp_script_modules()->print_import_map(); } @@ -177,7 +177,7 @@ function wp_print_script_module_import_map() { * * @see WP_Script_Modules::print_head_enqueued_script_modules() */ -function wp_print_head_script_modules() { +function wp_print_head_script_modules(): void { wp_script_modules()->print_head_enqueued_script_modules(); } @@ -188,7 +188,7 @@ function wp_print_head_script_modules() { * * @see WP_Script_Modules::print_enqueued_script_modules() */ -function wp_print_script_modules() { +function wp_print_script_modules(): void { wp_script_modules()->print_enqueued_script_modules(); } @@ -199,7 +199,7 @@ function wp_print_script_modules() { * * @see WP_Script_Modules::print_script_module_preloads() */ -function wp_print_script_module_preloads() { +function wp_print_script_module_preloads(): void { wp_script_modules()->print_script_module_preloads(); } @@ -232,7 +232,7 @@ function wp_print_script_module_data(): void { * * @see WP_Script_Modules::print_a11y_script_module_html() */ -function wp_print_a11y_script_module_html() { +function wp_print_a11y_script_module_html(): void { wp_script_modules()->print_a11y_script_module_html(); }