From c42ebcd009b93c211cb284535a29f1d3697907e0 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 10:56:29 +0400 Subject: [PATCH 01/10] 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 02/10] 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 7931499dbbd3d2810dc3e06ff8057510f6d26392 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 11:32:02 +0400 Subject: [PATCH 03/10] Editor: Discard script modules enqueued during REST API preloading. `block_editor_rest_api_preload()` runs REST requests in-process, including the post with `context=edit`. Rendering the post content enqueues each block's view assets. The function already restores `$wp_scripts` and `$wp_styles` after preloading, so classic view scripts and view styles are discarded. It did not restore `$wp_script_modules`, so a block's `viewScriptModule` stayed enqueued and was printed in the block editor. Back up and restore `$wp_script_modules` the same way. Depends on the script module print hooks reading the global instance (#66100). See #64484, #55151. --- src/wp-includes/block-editor.php | 29 +++++---- tests/phpunit/tests/blocks/editor.php | 87 ++++++++++++++++++++++++--- 2 files changed, 95 insertions(+), 21 deletions(-) diff --git a/src/wp-includes/block-editor.php b/src/wp-includes/block-editor.php index 1e6d60f637592..c27735b436244 100644 --- a/src/wp-includes/block-editor.php +++ b/src/wp-includes/block-editor.php @@ -668,15 +668,16 @@ function get_block_editor_settings( array $custom_settings, $block_editor_contex * * @since 5.8.0 * - * @global WP_Post $post Global post object. - * @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts. - * @global WP_Styles $wp_styles The WP_Styles object for printing styles. + * @global WP_Post $post Global post object. + * @global WP_Scripts $wp_scripts The WP_Scripts object for printing scripts. + * @global WP_Styles $wp_styles The WP_Styles object for printing styles. + * @global WP_Script_Modules $wp_script_modules The WP_Script_Modules object for printing script modules. * * @param (string|string[])[] $preload_paths List of paths to preload. * @param WP_Block_Editor_Context $block_editor_context The current block editor context. */ function block_editor_rest_api_preload( array $preload_paths, $block_editor_context ) { - global $post, $wp_scripts, $wp_styles; + global $post, $wp_scripts, $wp_styles, $wp_script_modules; /** * Filters the array of REST API paths that will be used to preloaded common data for the block editor. @@ -710,15 +711,16 @@ function block_editor_rest_api_preload( array $preload_paths, $block_editor_cont } /* - * Ensure the global $post, $wp_scripts, and $wp_styles remain the same after - * API data is preloaded. + * Ensure the globals $post, $wp_scripts, $wp_styles, and $wp_script_modules + * remain the same after API data is preloaded. * Because API preloading can call the_content and other filters, plugins * can unexpectedly modify the global $post or enqueue assets which are not * intended for the block editor. */ - $backup_global_post = ! empty( $post ) ? clone $post : $post; - $backup_wp_scripts = ! empty( $wp_scripts ) ? clone $wp_scripts : $wp_scripts; - $backup_wp_styles = ! empty( $wp_styles ) ? clone $wp_styles : $wp_styles; + $backup_global_post = ! empty( $post ) ? clone $post : $post; + $backup_wp_scripts = ! empty( $wp_scripts ) ? clone $wp_scripts : $wp_scripts; + $backup_wp_styles = ! empty( $wp_styles ) ? clone $wp_styles : $wp_styles; + $backup_wp_script_modules = ! empty( $wp_script_modules ) ? clone $wp_script_modules : $wp_script_modules; foreach ( $preload_paths as &$path ) { if ( is_string( $path ) && ! str_starts_with( $path, '/' ) ) { @@ -739,10 +741,11 @@ function block_editor_rest_api_preload( array $preload_paths, $block_editor_cont array() ); - // Restore the global $post, $wp_scripts, and $wp_styles as they were before API preloading. - $post = $backup_global_post; - $wp_scripts = $backup_wp_scripts; - $wp_styles = $backup_wp_styles; + // Restore the globals $post, $wp_scripts, $wp_styles, and $wp_script_modules as they were before API preloading. + $post = $backup_global_post; + $wp_scripts = $backup_wp_scripts; + $wp_styles = $backup_wp_styles; + $wp_script_modules = $backup_wp_script_modules; wp_add_inline_script( 'wp-api-fetch', diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index ed740a35bbcbc..b9a5e8fd033f6 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -33,19 +33,28 @@ public function set_up() { global $post_ID; $post_ID = 1; - global $wp_scripts, $wp_styles; - $this->original_wp_scripts = $wp_scripts; - $this->original_wp_styles = $wp_styles; - $wp_scripts = null; - $wp_styles = null; + global $wp_scripts, $wp_styles, $wp_script_modules; + $this->original_wp_scripts = $wp_scripts; + $this->original_wp_styles = $wp_styles; + $this->original_wp_script_modules = $wp_script_modules; + $wp_scripts = null; + $wp_styles = null; + $wp_script_modules = null; wp_scripts(); wp_styles(); + wp_script_modules(); } public function tear_down() { - global $wp_scripts, $wp_styles; - $wp_scripts = $this->original_wp_scripts; - $wp_styles = $this->original_wp_styles; + global $wp_scripts, $wp_styles, $wp_script_modules; + $wp_scripts = $this->original_wp_scripts; + $wp_styles = $this->original_wp_styles; + $wp_script_modules = $this->original_wp_script_modules; + + $registry = WP_Block_Type_Registry::get_instance(); + if ( $registry->is_registered( 'tests/preload-view-assets' ) ) { + $registry->unregister( 'tests/preload-view-assets' ); + } /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; @@ -70,6 +79,11 @@ public function tear_down() { */ protected $original_wp_styles; + /** + * @var WP_Script_Modules|null + */ + protected $original_wp_script_modules; + /** * Original stylesheet. * @@ -685,6 +699,63 @@ public function test_block_editor_rest_api_preload_adds_missing_leading_slash( a $this->assertStringContainsString( $expected, $haystack ); } + /** + * Tests that scripts, styles, and script modules enqueued while rendering blocks + * during REST API preloading are not enqueued in the block editor. + * + * @ticket 64484 + * @ticket 55151 + * + * @covers ::block_editor_rest_api_preload + */ + public function test_block_editor_rest_api_preload_discards_assets_enqueued_during_preload() { + wp_register_script( 'test-view-script', '/test-view-script.js', array(), null ); + wp_register_style( 'test-view-style', '/test-view-style.css', array(), null ); + wp_register_script_module( 'test-view-script-module', '/test-view-script-module.js' ); + wp_register_script_module( 'test-editor-script-module', '/test-editor-script-module.js' ); + + $rendered = false; + register_block_type( + 'tests/preload-view-assets', + array( + 'render_callback' => static function () use ( &$rendered ) { + $rendered = true; + return '

Block content.

'; + }, + 'view_script_handles' => array( 'test-view-script' ), + 'view_style_handles' => array( 'test-view-style' ), + 'view_script_module_ids' => array( 'test-view-script-module' ), + ) + ); + + $post_id = self::factory()->post->create( + array( + 'post_content' => '', + ) + ); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + + // Enqueued before preloading, so it must still be enqueued afterwards. + wp_enqueue_script_module( 'test-editor-script-module' ); + + block_editor_rest_api_preload( + array( "/wp/v2/posts/{$post_id}?context=edit" ), + new WP_Block_Editor_Context( array( 'post' => get_post( $post_id ) ) ) + ); + + $admin_footer_scripts = get_echo( + static function () { + do_action( 'admin_print_footer_scripts' ); + } + ); + + $this->assertTrue( $rendered, 'The block should render during preloading.' ); + $this->assertNotContains( 'test-view-script', wp_scripts()->queue, 'The block view script should not be enqueued.' ); + $this->assertNotContains( 'test-view-style', wp_styles()->queue, 'The block view style should not be enqueued.' ); + $this->assertStringNotContainsString( 'test-view-script-module', $admin_footer_scripts, 'The block view script module should not be printed.' ); + $this->assertStringContainsString( 'test-editor-script-module', $admin_footer_scripts, 'The script module enqueued before preloading should be printed.' ); + } + /** * @ticket 57547 * From cf46e84c8eacf3b526e8cc77ec2922b1519ebd30 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 13:30:23 +0400 Subject: [PATCH 04/10] Revert "Remove redundant since tag" This reverts commit 89a0dbf1af51f14c29c8d8e3972e133694f8bc66. --- src/wp-includes/class-wp-script-modules.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index ce2cbba16daad..a41117d21c386 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -429,6 +429,7 @@ 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 15c13cd9b9c9dcd06414e3203157bcc6caa16466 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 13:30:23 +0400 Subject: [PATCH 05/10] Revert "Script Loader: Resolve the global script modules instance in hook callbacks." This reverts commit c42ebcd009b93c211cb284535a29f1d3697907e0. --- 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, 13 insertions(+), 122 deletions(-) diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index a41117d21c386..9c9f8a762af5e 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -429,12 +429,11 @@ 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, 'wp_print_script_module_import_map' ); + add_action( $position, array( $this, 'print_import_map' ) ); if ( $is_block_theme ) { /* * Modules can only be printed in the head for block themes because only with @@ -443,14 +442,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', 'wp_print_head_script_modules' ); + add_action( 'wp_head', array( $this, 'print_head_enqueued_script_modules' ) ); } - add_action( 'wp_footer', 'wp_print_script_modules' ); - add_action( $position, 'wp_print_script_module_preloads' ); + add_action( 'wp_footer', array( $this, 'print_enqueued_script_modules' ) ); + add_action( $position, 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' ); + 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' ) ); /* * Print translations after classic scripts like wp-i18n are loaded (at @@ -458,13 +457,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', '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_translations' ), 21 ); + add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_translations' ), 11 ); - 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 ); + 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 ); } /** diff --git a/src/wp-includes/script-modules.php b/src/wp-includes/script-modules.php index 8f75c5175a76d..23dc8a68641ad 100644 --- a/src/wp-includes/script-modules.php +++ b/src/wp-includes/script-modules.php @@ -159,83 +159,6 @@ 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 7bf39bb23b8a9..01c58898dcb19 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -2922,35 +2922,4 @@ 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 f933b99e4ee80dc7ab21078834f0e66ed915ed42 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Mon, 14 Sep 2026 13:32:34 +0400 Subject: [PATCH 06/10] Editor: Restore the original globals after REST API preloading. `block_editor_rest_api_preload()` cloned each global into a backup variable, ran the preload against the original object, then assigned the clone to the global. The global ended up pointing at a different object than before the call, so anything bound to the original instance kept reading an object nothing enqueues into any more. Swap copies in for the duration of the preload and restore the originals afterwards, which is what `_wp_get_iframed_editor_assets()` already does. Hooks and other references bound to the original instances stay valid, so the script module print callbacks registered by `WP_Script_Modules::add_hooks()` read the restored instance. This removes the dependency on the script module print hooks resolving the global instance (#66100). Follow-up to [52733]. See #64484, #55151. --- src/wp-includes/block-editor.php | 27 ++++++++++++++++++--------- tests/phpunit/tests/blocks/editor.php | 7 ++++++- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/block-editor.php b/src/wp-includes/block-editor.php index c27735b436244..45aa966cc0edb 100644 --- a/src/wp-includes/block-editor.php +++ b/src/wp-includes/block-editor.php @@ -716,11 +716,20 @@ function block_editor_rest_api_preload( array $preload_paths, $block_editor_cont * Because API preloading can call the_content and other filters, plugins * can unexpectedly modify the global $post or enqueue assets which are not * intended for the block editor. + * + * Copies are swapped in for the duration of the preload and the original + * instances are restored afterwards, so that hooks and other references + * bound to those instances remain valid. */ - $backup_global_post = ! empty( $post ) ? clone $post : $post; - $backup_wp_scripts = ! empty( $wp_scripts ) ? clone $wp_scripts : $wp_scripts; - $backup_wp_styles = ! empty( $wp_styles ) ? clone $wp_styles : $wp_styles; - $backup_wp_script_modules = ! empty( $wp_script_modules ) ? clone $wp_script_modules : $wp_script_modules; + $original_post = $post; + $original_wp_scripts = $wp_scripts; + $original_wp_styles = $wp_styles; + $original_wp_script_modules = $wp_script_modules; + + $post = ! empty( $post ) ? clone $post : $post; + $wp_scripts = ! empty( $wp_scripts ) ? clone $wp_scripts : $wp_scripts; + $wp_styles = ! empty( $wp_styles ) ? clone $wp_styles : $wp_styles; + $wp_script_modules = ! empty( $wp_script_modules ) ? clone $wp_script_modules : $wp_script_modules; foreach ( $preload_paths as &$path ) { if ( is_string( $path ) && ! str_starts_with( $path, '/' ) ) { @@ -741,11 +750,11 @@ function block_editor_rest_api_preload( array $preload_paths, $block_editor_cont array() ); - // Restore the globals $post, $wp_scripts, $wp_styles, and $wp_script_modules as they were before API preloading. - $post = $backup_global_post; - $wp_scripts = $backup_wp_scripts; - $wp_styles = $backup_wp_styles; - $wp_script_modules = $backup_wp_script_modules; + // Restore the original $post, $wp_scripts, $wp_styles, and $wp_script_modules instances. + $post = $original_post; + $wp_scripts = $original_wp_scripts; + $wp_styles = $original_wp_styles; + $wp_script_modules = $original_wp_script_modules; wp_add_inline_script( 'wp-api-fetch', diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index b9a5e8fd033f6..0dbde4cd1c873 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -42,7 +42,12 @@ public function set_up() { $wp_script_modules = null; wp_scripts(); wp_styles(); - wp_script_modules(); + /* + * The script module print callbacks are bound to the instance that + * registers them, so the fresh instance must register its own hooks. + * WP_UnitTestCase restores $wp_filter between tests. + */ + wp_script_modules()->add_hooks(); } public function tear_down() { From 7acec63c4e25eb0c2a84a5c18ffeaa740a83ada5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Sep 2026 17:46:14 -0700 Subject: [PATCH 07/10] Fix static analysis issues in test --- tests/phpunit/tests/blocks/editor.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 0dbde4cd1c873..814dc31f852a2 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -11,6 +11,10 @@ class Tests_Blocks_Editor extends WP_UnitTestCase { /** * Sets up each test method. + * + * @global WP_Scripts $wp_scripts + * @global WP_Styles $wp_styles + * @global WP_Script_Modules $wp_script_modules */ public function set_up() { global $post; @@ -750,6 +754,7 @@ public function test_block_editor_rest_api_preload_discards_assets_enqueued_duri $admin_footer_scripts = get_echo( static function () { + /** This action is documented in wp-admin/admin-footer.php */ do_action( 'admin_print_footer_scripts' ); } ); From d8815b3b65fba58056246497f59d306e4b4f313c Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 16 Sep 2026 08:08:46 +0200 Subject: [PATCH 08/10] Use PHP property types instead of phpdoc types Co-authored-by: Weston Ruter --- tests/phpunit/tests/blocks/editor.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 814dc31f852a2..02c51306691ab 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -91,7 +91,11 @@ public function tear_down() { /** * @var WP_Script_Modules|null */ - protected $original_wp_script_modules; + protected ?WP_Scripts $original_wp_scripts; + + protected ?WP_Styles $original_wp_styles; + + protected ?WP_Script_Modules $original_wp_script_modules; /** * Original stylesheet. From 22093fe32b577a644dfc20f2810b2876537d9f2f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 17 Sep 2026 10:12:28 +0400 Subject: [PATCH 09/10] Setup post data after restore Additional post globals are susceptible to modification in the same way during preloading. Restore them along with the original post. Co-authored-by: Weston Ruter --- src/wp-includes/block-editor.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/block-editor.php b/src/wp-includes/block-editor.php index 45aa966cc0edb..830e67cfce013 100644 --- a/src/wp-includes/block-editor.php +++ b/src/wp-includes/block-editor.php @@ -751,7 +751,10 @@ function block_editor_rest_api_preload( array $preload_paths, $block_editor_cont ); // Restore the original $post, $wp_scripts, $wp_styles, and $wp_script_modules instances. - $post = $original_post; + $post = $original_post; + if ( ! empty( $post ) ) { + setup_postdata( $post ); + } $wp_scripts = $original_wp_scripts; $wp_styles = $original_wp_styles; $wp_script_modules = $original_wp_script_modules; From 753c7ff067ab8fe4fc3e4892ff6fef7c1de763c1 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Thu, 17 Sep 2026 12:23:39 +0400 Subject: [PATCH 10/10] fixup! Use PHP property types instead of phpdoc types --- tests/phpunit/tests/blocks/editor.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 02c51306691ab..8f0b65fc5669d 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -78,19 +78,6 @@ public function tear_down() { parent::tear_down(); } - /** - * @var WP_Scripts|null - */ - protected $original_wp_scripts; - - /** - * @var WP_Styles|null - */ - protected $original_wp_styles; - - /** - * @var WP_Script_Modules|null - */ protected ?WP_Scripts $original_wp_scripts; protected ?WP_Styles $original_wp_styles;