diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index b498c1189700e..fc6cf86cfd2ba 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -492,6 +492,14 @@ 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_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(); + /* * There can be only one root interactive block at a time because the rendered HTML of that block contains * the rendered HTML of all its inner blocks, including any interactive block. @@ -661,6 +669,44 @@ public function render( $options = array() ) { $root_interactive_block = null; } + // 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. + if ( + ( $has_new_styles || $has_new_scripts || $has_new_script_modules ) && + ( + trim( $block_content ) !== '' || + /** + * Filters whether to enqueue assets for a block which has no rendered content. + * + * @since 6.9.0 + * + * @param bool $enqueue Whether to enqueue assets. + * @param string $block_name Block 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 ) ); + } + if ( $has_new_scripts ) { + wp_scripts()->queue = array_unique( array_merge( wp_scripts()->queue, $new_scripts_queue ) ); + } + if ( $has_new_script_modules ) { + wp_script_modules()->queue = array_unique( array_merge( wp_script_modules()->queue, $new_script_modules_queue ) ); + } + } + return $block_content; } } diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index 08d08a5d1a65e..52537b1a3412e 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -23,12 +23,12 @@ class WP_Script_Modules { private $registered = array(); /** - * Holds the script module identifiers that were enqueued before registered. + * An array of IDs for queued script modules. * - * @since 6.5.0 - * @var array + * @since 6.9.0 + * @var string[] */ - private $enqueued_before_registered = array(); + public $queue = array(); /** * Tracks whether the @wordpress/a11y script module is available. @@ -122,7 +122,6 @@ public function register( string $id, string $src, array $deps = array(), $versi $this->registered[ $id ] = array( 'src' => $src, 'version' => $version, - 'enqueue' => isset( $this->enqueued_before_registered[ $id ] ), 'dependencies' => $dependencies, 'fetchpriority' => $fetchpriority, ); @@ -213,13 +212,11 @@ public function set_fetchpriority( string $id, string $priority ): bool { * } */ public function enqueue( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) { - if ( isset( $this->registered[ $id ] ) ) { - $this->registered[ $id ]['enqueue'] = true; - } elseif ( $src ) { + if ( ! in_array( $id, $this->queue, true ) ) { + $this->queue[] = $id; + } + if ( ! isset( $this->registered[ $id ] ) && $src ) { $this->register( $id, $src, $deps, $version, $args ); - $this->registered[ $id ]['enqueue'] = true; - } else { - $this->enqueued_before_registered[ $id ] = true; } } @@ -231,10 +228,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 ) { - if ( isset( $this->registered[ $id ] ) ) { - $this->registered[ $id ]['enqueue'] = false; - } - unset( $this->enqueued_before_registered[ $id ] ); + $this->queue = array_diff( $this->queue, array( $id ) ); } /** @@ -245,8 +239,8 @@ public function dequeue( string $id ) { * @param string $id The identifier of the script module. */ public function deregister( string $id ) { + $this->dequeue( $id ); unset( $this->registered[ $id ] ); - unset( $this->enqueued_before_registered[ $id ] ); } /** @@ -304,9 +298,9 @@ public function print_enqueued_script_modules() { * @since 6.5.0 */ public function print_script_module_preloads() { - foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) { + foreach ( $this->get_dependencies( array_unique( $this->queue ), array( 'static' ) ) as $id => $script_module ) { // Don't preload if it's marked for enqueue. - if ( true !== $script_module['enqueue'] ) { + if ( ! in_array( $id, $this->queue, true ) ) { echo sprintf( '', esc_url( $this->get_src( $id ) ), @@ -345,7 +339,7 @@ public function print_import_map() { */ private function get_import_map(): array { $imports = array(); - foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ) ) as $id => $script_module ) { + foreach ( $this->get_dependencies( array_unique( $this->queue ) ) as $id => $script_module ) { $imports[ $id ] = $this->get_src( $id ); } return array( 'imports' => $imports ); @@ -359,13 +353,10 @@ private function get_import_map(): array { * @return array Script modules marked for enqueue, keyed by script module identifier. */ private function get_marked_for_enqueue(): array { - $enqueued = array(); - foreach ( $this->registered as $id => $script_module ) { - if ( true === $script_module['enqueue'] ) { - $enqueued[ $id ] = $script_module; - } - } - return $enqueued; + return wp_array_slice_assoc( + $this->registered, + $this->queue + ); } /** @@ -457,7 +448,7 @@ private function get_src( string $id ): string { */ public function print_script_module_data(): void { $modules = array(); - foreach ( array_keys( $this->get_marked_for_enqueue() ) as $id ) { + foreach ( array_unique( $this->queue ) as $id ) { if ( '@wordpress/a11y' === $id ) { $this->a11y_available = true; } diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 2ba757921038d..4241161388eb8 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -31,9 +31,21 @@ 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; + wp_scripts(); + wp_styles(); } public function tear_down() { + global $wp_scripts, $wp_styles; + $wp_scripts = $this->original_wp_scripts; + $wp_styles = $this->original_wp_styles; + /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $wp_rest_server = null; @@ -42,6 +54,16 @@ public function tear_down() { parent::tear_down(); } + /** + * @var WP_Scripts|null + */ + protected $original_wp_scripts; + + /** + * @var WP_Styles|null + */ + protected $original_wp_styles; + public function filter_set_block_categories_post( $block_categories, $post ) { if ( empty( $post ) ) { return $block_categories; diff --git a/tests/phpunit/tests/blocks/wpBlock.php b/tests/phpunit/tests/blocks/wpBlock.php index f28bbd6c83733..39ab3712b1842 100644 --- a/tests/phpunit/tests/blocks/wpBlock.php +++ b/tests/phpunit/tests/blocks/wpBlock.php @@ -13,7 +13,7 @@ class Tests_Blocks_wpBlock extends WP_UnitTestCase { /** * Fake block type registry. * - * @var WP_Block_Type_Registry + * @var WP_Block_Type_Registry|null */ private $registry = null; @@ -23,6 +23,11 @@ class Tests_Blocks_wpBlock extends WP_UnitTestCase { public function set_up() { parent::set_up(); + global $wp_styles, $wp_scripts, $wp_script_modules; + $wp_styles = null; + $wp_scripts = null; + $wp_script_modules = null; + $this->registry = new WP_Block_Type_Registry(); } @@ -32,6 +37,11 @@ public function set_up() { public function tear_down() { $this->registry = null; + global $wp_styles, $wp_scripts, $wp_script_modules; + $wp_styles = null; + $wp_scripts = null; + $wp_script_modules = null; + parent::tear_down(); } @@ -352,6 +362,292 @@ public function test_render_applies_dynamic_render_block_filter() { $this->assertSame( 'Original: "StaticOriginal: "Inner", from block "core/example"", from block "core/example"', $rendered_content ); } + /** + * Data provider for test_render_enqueues_scripts_and_styles. + * + * @return array + */ + public function data_provider_test_render_enqueues_scripts_and_styles(): array { + $block_markup = ' + +
+ +
First child
+ + + +
Last child
+ +
+ + '; + + // TODO: Add case where a dynamic block renders other blocks? + return array( + 'all_printed' => array( + 'set_up' => null, + 'block_markup' => $block_markup, + 'expected_rendered_block' => ' +
+
First child
+

Hello World!

+
Last child
+
+ ', + 'expected_styles' => array( 'static-view-style', 'static-child-view-style', 'dynamic-view-style' ), + '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' ), + ), + 'all_printed_with_extra_asset_via_filter' => array( + 'set_up' => static function () { + add_filter( + 'render_block_core/dynamic', + static function ( $content ) { + wp_enqueue_style( 'dynamic-extra', home_url( '/dynamic-extra.css' ), array(), null ); + $processor = new WP_HTML_Tag_Processor( $content ); + if ( $processor->next_tag() ) { + $processor->add_class( 'filtered' ); + $content = $processor->get_updated_html(); + } + return $content; + } + ); + }, + 'block_markup' => $block_markup, + 'expected_rendered_block' => ' +
+
First child
+

Hello World!

+
Last child
+
+ ', + 'expected_styles' => array( 'static-view-style', 'dynamic-extra', 'static-child-view-style', 'dynamic-view-style' ), + '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' ), + ), + 'dynamic_hidden_assets_omitted' => array( + 'set_up' => static function () { + add_filter( 'render_block_core/dynamic', '__return_empty_string' ); + }, + 'block_markup' => $block_markup, + 'expected_rendered_block' => ' +
+
First child
+
Last child
+
+ ', + 'expected_styles' => array( 'static-view-style', 'static-child-view-style' ), + 'expected_scripts' => array( 'static-view-script', 'static-child-view-script' ), + 'expected_script_modules' => array( 'static-view-script-module', 'static-child-view-script-module' ), + ), + 'dynamic_hidden_assets_included' => array( + 'set_up' => static function () { + add_filter( 'render_block_core/dynamic', '__return_empty_string' ); + add_filter( + 'enqueue_empty_block_content_assets', + static function ( $enqueue, $block_name ) { + if ( 'core/dynamic' === $block_name ) { + $enqueue = true; + } + return $enqueue; + }, + 10, + 2 + ); + }, + 'block_markup' => $block_markup, + 'expected_rendered_block' => ' +
+
First child
+
Last child
+
+ ', + 'expected_styles' => array( 'static-view-style', 'static-child-view-style', 'dynamic-view-style' ), + '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' ), + ), + 'static_hidden_assets_omitted' => array( + 'set_up' => static function () { + add_filter( 'render_block_core/static', '__return_empty_string' ); + add_filter( + 'render_block_core/dynamic', + static function ( $content ) { + wp_enqueue_style( 'dynamic-extra', home_url( '/dynamic-extra.css' ), array(), null ); + return $content; + } + ); + }, + 'block_markup' => $block_markup, + 'expected_rendered_block' => '', + 'expected_styles' => array(), + 'expected_scripts' => array(), + 'expected_script_modules' => array(), + ), + 'static_child_hidden_assets_omitted' => array( + 'set_up' => static function () { + add_filter( 'render_block_core/static-child', '__return_empty_string' ); + }, + 'block_markup' => $block_markup, + 'expected_rendered_block' => ' +
+

Hello World!

+
+ ', + 'expected_styles' => array( 'static-view-style', 'dynamic-view-style' ), + 'expected_scripts' => array( 'static-view-script', 'dynamic-view-script' ), + 'expected_script_modules' => array( 'static-view-script-module', 'dynamic-view-script-module' ), + ), + 'last_static_child_hidden_assets_omitted' => array( + 'set_up' => static function () { + add_filter( + 'render_block_core/static-child', + static function ( $content ) { + if ( str_contains( $content, 'Last child' ) ) { + $content = ''; + } + return $content; + }, + 10, + 3 + ); + }, + 'block_markup' => $block_markup, + 'expected_rendered_block' => ' +
+
First child
+

Hello World!

+
+ ', + 'expected_styles' => array( 'static-view-style', 'static-child-view-style', 'dynamic-view-style' ), + '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' ), + ), + 'all_hidden_assets_omitted' => array( + 'set_up' => static function () { + add_filter( 'render_block', '__return_empty_string' ); + }, + 'block_markup' => $block_markup, + 'expected_rendered_block' => '', + 'expected_styles' => array(), + 'expected_scripts' => array(), + 'expected_script_modules' => array(), + ), + 'all_hidden_assets_included' => array( + 'set_up' => static function () { + add_filter( 'render_block', '__return_empty_string' ); + add_filter( 'enqueue_empty_block_content_assets', '__return_true' ); + }, + 'block_markup' => $block_markup, + 'expected_rendered_block' => '', + 'expected_styles' => array( 'static-view-style', 'static-child-view-style', 'dynamic-view-style' ), + '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' ), + ), + ); + } + + /** + * @ticket 63676 + * @covers WP_Block::render() + * + * @dataProvider data_provider_test_render_enqueues_scripts_and_styles + * + * @param Closure|null $set_up + * @param string $block_markup + * @param string[] $expected_styles + * @param string[] $expected_scripts + * @param string[] $expected_script_modules + */ + public function test_render_enqueues_scripts_and_styles( ?Closure $set_up, string $block_markup, string $expected_rendered_block, array $expected_styles, array $expected_scripts, array $expected_script_modules ) { + if ( $set_up instanceof Closure ) { + $set_up(); + } + 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' ) ); + $this->registry->register( + 'core/static', + array( + 'view_style_handles' => array( 'static-view-style' ), + 'view_script_handles' => array( 'static-view-script' ), + 'view_script_module_ids' => array( 'static-view-script-module' ), + ) + ); + + wp_register_style( 'static-child-view-style', home_url( '/static-child-view-style.css' ) ); + wp_register_script( 'static-child-view-script', home_url( '/static-child-view-script.js' ) ); + wp_register_script_module( 'static-child-view-script-module', home_url( '/static-child-view-script-module.js' ) ); + $this->registry->register( + 'core/static-child', + array( + 'view_style_handles' => array( 'static-child-view-style' ), + 'view_script_handles' => array( 'static-child-view-script' ), + 'view_script_module_ids' => array( 'static-child-view-script-module' ), + ) + ); + + wp_register_style( 'dynamic-view-style', home_url( '/dynamic-view-style.css' ) ); + wp_register_script( 'dynamic-view-script', home_url( '/dynamic-view-script.js' ) ); + wp_register_script_module( 'dynamic-view-script-module', home_url( '/dynamic-view-script-module.js' ) ); + $this->registry->register( + 'core/dynamic', + array( + 'render_callback' => static function () { + return '

Hello World!

'; + }, + 'view_style_handles' => array( 'dynamic-view-style' ), + 'view_script_handles' => array( 'dynamic-view-script' ), + 'view_script_module_ids' => array( 'dynamic-view-script-module' ), + ) + ); + + // TODO: Why not use do_blocks() instead? + $parsed_blocks = parse_blocks( trim( $block_markup ) ); + $parsed_block = $parsed_blocks[0]; + $context = array(); + $block = new WP_Block( $parsed_block, $context, $this->registry ); + $rendered_block = $block->render(); + + $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' ); + } + /** * @ticket 49927 */ diff --git a/tests/phpunit/tests/script-modules/wpScriptModules.php b/tests/phpunit/tests/script-modules/wpScriptModules.php index 35b96cd3e4af4..a912a0e95b268 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -1343,6 +1343,31 @@ public function test_set_fetchpriority_with_invalid_value() { $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] ); } + /** + * Tests that directly manipulating the queue works as expected. + * + * @ticket 63676 + * + * @covers WP_Script_Modules::queue + * @covers WP_Script_Modules::dequeue + */ + 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->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->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.' ); + } + /** * Gets registered script modules. *