diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index fc6cf86cfd2ba..822e64a2b5674 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -492,13 +492,12 @@ public function replace_rich_text( $rich_text ) { public function render( $options = array() ) { global $post; - // Capture the current assets queues and then clear out to capture the diff of what was introduced by rendering. + $before_wp_enqueue_scripts_count = did_action( 'wp_enqueue_scripts' ); + + // Capture the current assets queues. $before_styles_queue = wp_styles()->queue; $before_scripts_queue = wp_scripts()->queue; - $before_script_modules_queue = wp_script_modules()->queue; - wp_styles()->queue = array(); - wp_scripts()->queue = array(); - wp_script_modules()->queue = array(); + $before_script_modules_queue = wp_script_modules()->get_queue(); /* * There can be only one root interactive block at a time because the rendered HTML of that block contains @@ -670,21 +669,27 @@ public function render( $options = array() ) { } // Capture the new assets enqueued during rendering, and restore the queues the state prior to rendering. - $new_styles_queue = wp_styles()->queue; - $new_scripts_queue = wp_scripts()->queue; - $new_script_modules_queue = wp_script_modules()->queue; - wp_styles()->queue = $before_styles_queue; - wp_scripts()->queue = $before_scripts_queue; - wp_script_modules()->queue = $before_script_modules_queue; - $has_new_styles = count( $new_styles_queue ) > 0; - $has_new_scripts = count( $new_scripts_queue ) > 0; - $has_new_script_modules = count( $new_script_modules_queue ) > 0; - - // Merge the newly enqueued assets with the existing assets if the rendered block is not empty. + $after_styles_queue = wp_styles()->queue; + $after_scripts_queue = wp_scripts()->queue; + $after_script_modules_queue = wp_script_modules()->get_queue(); + + /* + * As a very special case, a dynamic block may in fact include a call to wp_head() (and thus wp_enqueue_scripts()), + * in which all of its enqueued assets are targeting wp_footer. In this case, nothing would be printed, but this + * shouldn't indicate that the just-enqueued assets should be dequeued due to it being an empty block. + */ + $just_did_wp_enqueue_scripts = ( did_action( 'wp_enqueue_scripts' ) !== $before_wp_enqueue_scripts_count ); + + $has_new_styles = ( $before_styles_queue !== $after_styles_queue ); + $has_new_scripts = ( $before_scripts_queue !== $after_scripts_queue ); + $has_new_script_modules = ( $before_script_modules_queue !== $after_script_modules_queue ); + + // Dequeue the newly enqueued assets with the existing assets if the rendered block was empty & wp_enqueue_scripts did not fire. if ( + ! $just_did_wp_enqueue_scripts && ( $has_new_styles || $has_new_scripts || $has_new_script_modules ) && ( - trim( $block_content ) !== '' || + trim( $block_content ) === '' && /** * Filters whether to enqueue assets for a block which has no rendered content. * @@ -693,17 +698,17 @@ public function render( $options = array() ) { * @param bool $enqueue Whether to enqueue assets. * @param string $block_name Block name. */ - (bool) apply_filters( 'enqueue_empty_block_content_assets', false, $this->name ) + ! (bool) apply_filters( 'enqueue_empty_block_content_assets', false, $this->name ) ) ) { - if ( $has_new_styles ) { - wp_styles()->queue = array_unique( array_merge( wp_styles()->queue, $new_styles_queue ) ); + foreach ( array_diff( $after_styles_queue, $before_styles_queue ) as $handle ) { + wp_dequeue_style( $handle ); } - if ( $has_new_scripts ) { - wp_scripts()->queue = array_unique( array_merge( wp_scripts()->queue, $new_scripts_queue ) ); + foreach ( array_diff( $after_scripts_queue, $before_scripts_queue ) as $handle ) { + wp_dequeue_script( $handle ); } - if ( $has_new_script_modules ) { - wp_script_modules()->queue = array_unique( array_merge( wp_script_modules()->queue, $new_script_modules_queue ) ); + foreach ( array_diff( $after_script_modules_queue, $before_script_modules_queue ) as $handle ) { + wp_dequeue_script_module( $handle ); } } diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index 07402f66b3f20..932fa984db018 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -28,7 +28,7 @@ class WP_Script_Modules { * @since 6.9.0 * @var string[] */ - public $queue = array(); + private $queue = array(); /** * Tracks whether the @wordpress/a11y script module is available. @@ -137,6 +137,17 @@ public function register( string $id, string $src, array $deps = array(), $versi } } + /** + * Gets IDs for queued script modules. + * + * @since 6.9.0 + * + * @return string[] Script module IDs. + */ + public function get_queue(): array { + return $this->queue; + } + /** * Checks if the provided fetchpriority is valid. * @@ -237,7 +248,7 @@ public function enqueue( string $id, string $src = '', array $deps = array(), $v * @param string $id The identifier of the script module. */ public function dequeue( string $id ) { - $this->queue = array_diff( $this->queue, array( $id ) ); + $this->queue = array_values( array_diff( $this->queue, array( $id ) ) ); } /** diff --git a/tests/phpunit/tests/blocks/wpBlock.php b/tests/phpunit/tests/blocks/wpBlock.php index 39ab3712b1842..f7c4710c9c3fe 100644 --- a/tests/phpunit/tests/blocks/wpBlock.php +++ b/tests/phpunit/tests/blocks/wpBlock.php @@ -543,6 +543,62 @@ static function ( $content ) { 'expected_scripts' => array( 'static-view-script', 'static-child-view-script', 'dynamic-view-script' ), 'expected_script_modules' => array( 'static-view-script-module', 'static-child-view-script-module', 'dynamic-view-script-module' ), ), + 'admin_bar_assets_enqueued_in_block' => array( + 'set_up' => static function () { + wp_enqueue_script( 'admin-bar' ); + wp_enqueue_style( 'admin-bar' ); + + add_filter( + 'render_block_core/static', + static function ( $content ) { + $processor = new WP_HTML_Tag_Processor( $content ); + $processor->next_tag(); + $processor->add_class( wp_script_is( 'admin-bar', 'enqueued' ) ? 'yes-admin-bar-script-enqueued' : 'not-admin-bar-script-enqueued' ); + $processor->add_class( wp_style_is( 'admin-bar', 'enqueued' ) ? 'yes-admin-bar-style-enqueued' : 'not-admin-bar-style-enqueued' ); + return $processor->get_updated_html(); + }, + 10, + 3 + ); + }, + 'block_markup' => '
', + 'expected_rendered_block' => ' + + ', + 'expected_styles' => array( 'static-view-style', 'admin-bar' ), + 'expected_scripts' => array( 'static-view-script', 'admin-bar' ), + 'expected_script_modules' => array( 'static-view-script-module' ), + ), + 'enqueues_in_wp_head_block' => array( + 'set_up' => static function () { + remove_all_actions( 'wp_head' ); + remove_all_actions( 'wp_enqueue_scripts' ); + + add_action( 'wp_head', 'wp_enqueue_scripts', 1 ); + add_action( 'wp_head', 'wp_print_styles', 8 ); + add_action( 'wp_head', 'wp_print_head_scripts', 9 ); + remove_action( 'wp_print_styles', 'print_emoji_styles' ); + + add_action( + 'wp_enqueue_scripts', + static function () { + wp_enqueue_script( 'for-footer', '/footer.js', array(), null, array( 'in_footer' => true ) ); + } + ); + add_action( + 'wp_head', + static function () { + wp_enqueue_style( 'for-footer', '/footer.css', array(), null ); + }, + 10000 + ); + }, + 'block_markup' => '', + 'expected_rendered_block' => '', + 'expected_styles' => array( 'for-footer' ), + 'expected_scripts' => array( 'for-footer' ), + 'expected_script_modules' => array(), + ), ); } @@ -562,6 +618,16 @@ public function test_render_enqueues_scripts_and_styles( ?Closure $set_up, strin if ( $set_up instanceof Closure ) { $set_up(); } + + $this->registry->register( + 'core/wp-head', + array( + 'render_callback' => static function () { + return get_echo( 'wp_head' ); + }, + ) + ); + wp_register_style( 'static-view-style', home_url( '/static-view-style.css' ) ); wp_register_script( 'static-view-script', home_url( '/static-view-script.js' ) ); wp_register_script_module( 'static-view-script-module', home_url( '/static-view-script-module.js' ) ); @@ -608,44 +674,16 @@ public function test_render_enqueues_scripts_and_styles( ?Closure $set_up, strin $block = new WP_Block( $parsed_block, $context, $this->registry ); $rendered_block = $block->render(); + $this->assertSameSets( $expected_styles, wp_styles()->queue, 'Enqueued styles do not meet expectations' ); + $this->assertSameSets( $expected_scripts, wp_scripts()->queue, 'Enqueued scripts do not meet expectations' ); + $this->assertSameSets( $expected_script_modules, wp_script_modules()->get_queue(), 'Enqueued script modules do not meet expectations' ); + $this->assertEqualHTML( $expected_rendered_block, $rendered_block, '', "Rendered block does not contain expected HTML:\n$rendered_block" ); - - remove_action( 'wp_print_styles', 'print_emoji_styles' ); - - $actual_styles = array(); - $printed_styles = get_echo( 'wp_print_styles' ); - $processor = new WP_HTML_Tag_Processor( $printed_styles ); - while ( $processor->next_tag( array( 'tag_name' => 'LINK' ) ) ) { - if ( 1 === preg_match( '/^(.+)-css$/', $processor->get_attribute( 'id' ), $matches ) ) { - $actual_styles[] = $matches[1]; - } - } - $this->assertSameSets( $expected_styles, $actual_styles, 'Enqueued styles do not meet expectations' ); - - $actual_scripts = array(); - $printed_scripts = get_echo( 'wp_print_scripts' ); - $processor = new WP_HTML_Tag_Processor( $printed_scripts ); - while ( $processor->next_tag( array( 'tag_name' => 'SCRIPT' ) ) ) { - if ( 1 === preg_match( '/^(.+)-js$/', $processor->get_attribute( 'id' ), $matches ) ) { - $actual_scripts[] = $matches[1]; - } - } - $this->assertSameSets( $expected_scripts, $actual_scripts, 'Enqueued scripts do not meet expectations' ); - - $actual_script_modules = array(); - $printed_script_modules = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); - $processor = new WP_HTML_Tag_Processor( $printed_script_modules ); - while ( $processor->next_tag( array( 'tag_name' => 'SCRIPT' ) ) ) { - if ( 1 === preg_match( '/^(.+)-js-module$/', $processor->get_attribute( 'id' ), $matches ) ) { - $actual_script_modules[] = $matches[1]; - } - } - $this->assertSameSets( $expected_script_modules, $actual_script_modules, 'Enqueued script modules do not meet expectations' ); } /** diff --git a/tests/phpunit/tests/script-modules/wpScriptModules.php b/tests/phpunit/tests/script-modules/wpScriptModules.php index 97be85f071489..bec646b521bad 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -1676,10 +1676,11 @@ private function normalize_markup_for_snapshot( string $markup ): string { } /** - * Tests that directly manipulating the queue works as expected. + * Tests that manipulating the queue works as expected. * * @ticket 63676 * + * @covers WP_Script_Modules::get_queue * @covers WP_Script_Modules::queue * @covers WP_Script_Modules::dequeue */ @@ -1687,17 +1688,18 @@ public function test_direct_queue_manipulation() { $this->script_modules->register( 'foo', '/foo.js' ); $this->script_modules->register( 'bar', '/bar.js' ); $this->script_modules->register( 'baz', '/baz.js' ); - $this->assertSame( array(), $this->script_modules->queue, 'Expected queue to be empty.' ); + $this->assertSame( array(), $this->script_modules->get_queue(), 'Expected queue to be empty.' ); $this->script_modules->enqueue( 'foo' ); $this->script_modules->enqueue( 'foo' ); $this->script_modules->enqueue( 'bar' ); - $this->assertSame( array( 'foo', 'bar' ), $this->script_modules->queue, 'Expected two deduplicated queued items.' ); - $this->script_modules->queue = array( 'baz' ); - $this->script_modules->enqueue( 'bar' ); - $this->assertSame( array( 'baz', 'bar' ), $this->script_modules->queue, 'Expected queue updated via setter and enqueue method to have two items.' ); + $this->assertSame( array( 'foo', 'bar' ), $this->script_modules->get_queue(), 'Expected two deduplicated queued items.' ); + $this->script_modules->dequeue( 'foo' ); + $this->script_modules->dequeue( 'foo' ); + $this->script_modules->enqueue( 'baz' ); + $this->assertSame( array( 'bar', 'baz' ), $this->script_modules->get_queue(), 'Expected items tup be updated after dequeue and enqueue.' ); $this->script_modules->dequeue( 'baz' ); $this->script_modules->dequeue( 'bar' ); - $this->assertSame( array(), $this->script_modules->queue, 'Expected queue to be empty after dequeueing both items.' ); + $this->assertSame( array(), $this->script_modules->get_queue(), 'Expected queue to be empty after dequeueing both items.' ); } /**