diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index b7c2c2c433190..47620d7f808fc 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -36,6 +36,7 @@ function remove_block_asset_path_prefix( $asset_handle_or_path ) { * * @since 5.5.0 * @since 6.1.0 Added `$index` parameter. + * @since 6.5.0 Added support for `viewScriptModule` field. * * @param string $block_name Name of the block. * @param string $field_name Name of the metadata field. @@ -52,6 +53,9 @@ function generate_block_asset_handle( $block_name, $field_name, $index = 0 ) { if ( str_starts_with( $field_name, 'view' ) ) { $asset_handle .= '-view'; } + if ( str_ends_with( strtolower( $field_name ), 'scriptmodule' ) ) { + $asset_handle .= '-script-module'; + } if ( $index > 0 ) { $asset_handle .= '-' . ( $index + 1 ); } @@ -59,12 +63,13 @@ function generate_block_asset_handle( $block_name, $field_name, $index = 0 ) { } $field_mappings = array( - 'editorScript' => 'editor-script', - 'script' => 'script', - 'viewScript' => 'view-script', - 'editorStyle' => 'editor-style', - 'style' => 'style', - 'viewStyle' => 'view-style', + 'editorScript' => 'editor-script', + 'editorStyle' => 'editor-style', + 'script' => 'script', + 'style' => 'style', + 'viewScript' => 'view-script', + 'viewScriptModule' => 'view-script-module', + 'viewStyle' => 'view-style', ); $asset_handle = str_replace( '/', '-', $block_name ) . '-' . $field_mappings[ $field_name ]; @@ -122,6 +127,62 @@ function get_block_asset_url( $path ) { return plugins_url( basename( $path ), $path ); } +/** + * Finds a script module ID for the selected block metadata field. It detects + * when a path to file was provided and optionally finds a corresponding asset + * file with details necessary to register the script module under with an + * automatically generated module ID. It returns unprocessed script module + * ID otherwise. + * + * @since 6.5.0 + * + * @param array $metadata Block metadata. + * @param string $field_name Field name to pick from metadata. + * @param int $index Optional. Index of the script module ID to register when multiple + * items passed. Default 0. + * @return string|false Script module ID or false on failure. + */ +function register_block_script_module_id( $metadata, $field_name, $index = 0 ) { + if ( empty( $metadata[ $field_name ] ) ) { + return false; + } + + $module_id = $metadata[ $field_name ]; + if ( is_array( $module_id ) ) { + if ( empty( $module_id[ $index ] ) ) { + return false; + } + $module_id = $module_id[ $index ]; + } + + $module_path = remove_block_asset_path_prefix( $module_id ); + if ( $module_id === $module_path ) { + return $module_id; + } + + $path = dirname( $metadata['file'] ); + $module_asset_raw_path = $path . '/' . substr_replace( $module_path, '.asset.php', - strlen( '.js' ) ); + $module_id = generate_block_asset_handle( $metadata['name'], $field_name, $index ); + $module_asset_path = wp_normalize_path( + realpath( $module_asset_raw_path ) + ); + + $module_path_norm = wp_normalize_path( realpath( $path . '/' . $module_path ) ); + $module_uri = get_block_asset_url( $module_path_norm ); + + $module_asset = ! empty( $module_asset_path ) ? require $module_asset_path : array(); + $module_dependencies = isset( $module_asset['dependencies'] ) ? $module_asset['dependencies'] : array(); + + wp_register_script_module( + $module_id, + $module_uri, + $module_dependencies, + isset( $module_asset['version'] ) ? $module_asset['version'] : false + ); + + return $module_id; +} + /** * Finds a script handle for the selected block metadata field. It detects * when a path to file was provided and optionally finds a corresponding asset @@ -314,7 +375,7 @@ function get_block_metadata_i18n_schema() { * @since 6.1.0 Added support for `render` field. * @since 6.3.0 Added `selectors` field. * @since 6.4.0 Added support for `blockHooks` field. - * @since 6.5.0 Added support for `allowedBlocks` and `viewStyle` fields. + * @since 6.5.0 Added support for `allowedBlocks`, `viewScriptModule`, and `viewStyle` fields. * * @param string $file_or_folder Path to the JSON file with metadata definition for * the block or path to the folder where the `block.json` file is located. @@ -490,6 +551,40 @@ function register_block_type_from_metadata( $file_or_folder, $args = array() ) { } } + $module_fields = array( + 'viewScriptModule' => 'view_script_module_ids', + ); + foreach ( $module_fields as $metadata_field_name => $settings_field_name ) { + if ( ! empty( $settings[ $metadata_field_name ] ) ) { + $metadata[ $metadata_field_name ] = $settings[ $metadata_field_name ]; + } + if ( ! empty( $metadata[ $metadata_field_name ] ) ) { + $modules = $metadata[ $metadata_field_name ]; + $processed_modules = array(); + if ( is_array( $modules ) ) { + for ( $index = 0; $index < count( $modules ); $index++ ) { + $result = register_block_script_module_id( + $metadata, + $metadata_field_name, + $index + ); + if ( $result ) { + $processed_modules[] = $result; + } + } + } else { + $result = register_block_script_module_id( + $metadata, + $metadata_field_name + ); + if ( $result ) { + $processed_modules[] = $result; + } + } + $settings[ $settings_field_name ] = $processed_modules; + } + } + $style_fields = array( 'editorStyle' => 'editor_style_handles', 'style' => 'style_handles', diff --git a/src/wp-includes/class-wp-block-type.php b/src/wp-includes/class-wp-block-type.php index 33825a7888320..63496d41029e3 100644 --- a/src/wp-includes/class-wp-block-type.php +++ b/src/wp-includes/class-wp-block-type.php @@ -226,6 +226,14 @@ class WP_Block_Type { */ public $view_script_handles = array(); + /** + * Block type front end only script module IDs. + * + * @since 6.5.0 + * @var string[] + */ + public $view_script_module_ids = array(); + /** * Block type editor only style handles. * diff --git a/src/wp-includes/class-wp-block.php b/src/wp-includes/class-wp-block.php index a965ef13727e1..4369223f42959 100644 --- a/src/wp-includes/class-wp-block.php +++ b/src/wp-includes/class-wp-block.php @@ -470,6 +470,12 @@ public function render( $options = array() ) { } } + if ( ! empty( $this->block_type->view_script_module_ids ) ) { + foreach ( $this->block_type->view_script_module_ids as $view_script_module_id ) { + wp_enqueue_script_module( $view_script_module_id ); + } + } + if ( ( ! empty( $this->block_type->style_handles ) ) ) { foreach ( $this->block_type->style_handles as $style_handle ) { wp_enqueue_style( $style_handle ); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php index 7bf2f6af4af2e..9577ebe8008e7 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-block-types-controller.php @@ -240,6 +240,7 @@ public function get_item( $request ) { * @since 5.5.0 * @since 5.9.0 Renamed `$block_type` to `$item` to match parent class for PHP 8 named parameter support. * @since 6.3.0 Added `selectors` field. + * @since 6.5.0 Added `view_script_module_ids` field. * * @param WP_Block_Type $item Block type data. * @param WP_REST_Request $request Full details about the request. @@ -291,6 +292,7 @@ public function prepare_item_for_response( $item, $request ) { 'editor_script_handles', 'script_handles', 'view_script_handles', + 'view_script_module_ids', 'editor_style_handles', 'style_handles', 'view_style_handles', @@ -584,6 +586,16 @@ public function get_item_schema() { 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), + 'view_script_module_ids' => array( + 'description' => __( 'Public facing script module IDs.' ), + 'type' => array( 'array' ), + 'default' => array(), + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'embed', 'view', 'edit' ), + 'readonly' => true, + ), 'editor_style_handles' => array( 'description' => __( 'Editor style handles.' ), 'type' => array( 'array' ), diff --git a/tests/phpunit/data/blocks/notice/block.json b/tests/phpunit/data/blocks/notice/block.json index 3e198145bdb49..7ccaef2d1312d 100644 --- a/tests/phpunit/data/blocks/notice/block.json +++ b/tests/phpunit/data/blocks/notice/block.json @@ -68,6 +68,7 @@ "editorScript": "tests-notice-editor-script", "script": "tests-notice-script", "viewScript": [ "tests-notice-view-script", "tests-notice-view-script-2" ], + "viewScriptModule": [ "tests-notice-view-script-module", "tests-notice-view-script-module-2" ], "editorStyle": "tests-notice-editor-style", "style": [ "tests-notice-style", "tests-notice-style-2" ], "viewStyle": [ "tests-notice-view-style" ], diff --git a/tests/phpunit/tests/blocks/register.php b/tests/phpunit/tests/blocks/register.php index 3ee56bc9e5470..7ac046f147955 100644 --- a/tests/phpunit/tests/blocks/register.php +++ b/tests/phpunit/tests/blocks/register.php @@ -138,6 +138,7 @@ public function test_removes_block_asset_path_prefix_and_current_directory() { /** * @ticket 50263 + * @ticket 60233 */ public function test_generate_block_asset_handle() { $block_name = 'unit-tests/my-block'; @@ -154,6 +155,18 @@ public function test_generate_block_asset_handle() { 'unit-tests-my-block-view-script-100', generate_block_asset_handle( $block_name, 'viewScript', 99 ) ); + $this->assertSame( + 'unit-tests-my-block-view-script-module', + generate_block_asset_handle( $block_name, 'viewScriptModule' ) + ); + $this->assertSame( + 'unit-tests-my-block-view-script-module-2', + generate_block_asset_handle( $block_name, 'viewScriptModule', 1 ) + ); + $this->assertSame( + 'unit-tests-my-block-view-script-module-100', + generate_block_asset_handle( $block_name, 'viewScriptModule', 99 ) + ); $this->assertSame( 'unit-tests-my-block-editor-style-2', generate_block_asset_handle( $block_name, 'editorStyle', 1 ) @@ -198,6 +211,52 @@ public function test_generate_block_asset_handle_core_block() { ); } + /** + * @ticket 60233 + */ + public function test_generate_block_asset_handle_core_block_module() { + $block_name = 'core/paragraph'; + + $this->assertSame( + 'wp-block-paragraph-editor-script-module', + generate_block_asset_handle( $block_name, 'editorScriptModule' ) + ); + $this->assertSame( + 'wp-block-paragraph-editor-script-module-2', + generate_block_asset_handle( $block_name, 'editorScriptModule', 1 ) + ); + $this->assertSame( + 'wp-block-paragraph-editor-script-module-100', + generate_block_asset_handle( $block_name, 'editorScriptModule', 99 ) + ); + + $this->assertSame( + 'wp-block-paragraph-view-script-module', + generate_block_asset_handle( $block_name, 'viewScriptModule' ) + ); + $this->assertSame( + 'wp-block-paragraph-view-script-module-2', + generate_block_asset_handle( $block_name, 'viewScriptModule', 1 ) + ); + $this->assertSame( + 'wp-block-paragraph-view-script-module-100', + generate_block_asset_handle( $block_name, 'viewScriptModule', 99 ) + ); + + $this->assertSame( + 'wp-block-paragraph-script-module', + generate_block_asset_handle( $block_name, 'scriptModule' ) + ); + $this->assertSame( + 'wp-block-paragraph-script-module-2', + generate_block_asset_handle( $block_name, 'scriptModule', 1 ) + ); + $this->assertSame( + 'wp-block-paragraph-script-module-100', + generate_block_asset_handle( $block_name, 'scriptModule', 99 ) + ); + } + /** * @ticket 50263 */ @@ -231,6 +290,115 @@ public function test_wrong_array_index_do_not_register_block_script_handle() { $this->assertFalse( $result ); } + /** + * @ticket 60233 + */ + public function test_field_not_found_register_block_script_module_id() { + $result = register_block_script_module_id( array(), 'viewScriptModule' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 60233 + */ + public function test_empty_string_value_do_not_register_block_script_module_id() { + $metadata = array( 'viewScriptModule' => '' ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 60233 + */ + public function test_empty_array_value_do_not_register_block_script_module_id() { + $metadata = array( 'viewScriptModule' => array() ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 60233 + */ + public function test_wrong_array_index_do_not_register_block_script_module_id() { + $metadata = array( 'viewScriptModule' => array( 'test-module_id' ) ); + $result = register_block_script_module_id( $metadata, 'script', 1 ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 60233 + */ + public function test_missing_asset_file_register_block_script_module_id() { + $metadata = array( + 'file' => __FILE__, + 'name' => 'unit-tests/test-block', + 'viewScriptModule' => 'file:./blocks/notice/missing-asset.js', + ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + + $this->assertSame( 'unit-tests-test-block-view-script-module', $result ); + } + + /** + * @ticket 60233 + */ + public function test_handle_passed_register_block_script_module_id() { + $metadata = array( + 'viewScriptModule' => 'test-script-module-id', + ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + + $this->assertSame( 'test-script-module-id', $result ); + } + + /** + * @ticket 60233 + */ + public function test_handles_passed_register_block_script_module_ids() { + $metadata = array( + 'viewScriptModule' => array( 'test-id', 'test-id-other' ), + ); + + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + $this->assertSame( 'test-id', $result ); + + $result = register_block_script_module_id( $metadata, 'viewScriptModule', 1 ); + $this->assertSame( 'test-id-other', $result ); + } + + /** + * @ticket 60233 + */ + public function test_success_register_block_script_module_id() { + $metadata = array( + 'file' => DIR_TESTDATA . '/blocks/notice/block.json', + 'name' => 'unit-tests/test-block', + 'viewScriptModule' => 'file:./block.js', + ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + + $this->assertSame( 'unit-tests-test-block-view-script-module', $result ); + + // Test the behavior directly within the unit test + $this->assertFalse( + strpos( + wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $metadata['viewScriptModule'] ) ), + trailingslashit( wp_normalize_path( get_template_directory() ) ) + ) === 0 + ); + + $this->assertFalse( + strpos( + wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $metadata['viewScriptModule'] ) ), + trailingslashit( wp_normalize_path( get_stylesheet_directory() ) ) + ) === 0 + ); + } + /** * @ticket 50263 */ @@ -245,14 +413,14 @@ public function test_handle_passed_register_block_script_handle() { public function test_handles_passed_register_block_script_handles() { $metadata = array( - 'script' => array( 'test-script-handle', 'test-script-handle-2' ), + 'script' => array( 'test-script-handle', 'test-script-handle-other' ), ); $result = register_block_script_handle( $metadata, 'script' ); $this->assertSame( 'test-script-handle', $result ); $result = register_block_script_handle( $metadata, 'script', 1 ); - $this->assertSame( 'test-script-handle-2', $result, 1 ); + $this->assertSame( 'test-script-handle-other', $result ); } /** @@ -751,6 +919,7 @@ public function data_register_block_registers_with_args_override_returns_false_w * @ticket 50328 * @ticket 57585 * @ticket 59797 + * @ticket 60233 */ public function test_block_registers_with_metadata_fixture() { $result = register_block_type_from_metadata( @@ -853,6 +1022,10 @@ public function test_block_registers_with_metadata_fixture() { array( 'tests-notice-view-script', 'tests-notice-view-script-2' ), $result->view_script_handles ); + $this->assertSameSets( + array( 'tests-notice-view-script-module', 'tests-notice-view-script-module-2' ), + $result->view_script_module_ids + ); $this->assertSameSets( array( 'tests-notice-editor-style' ), $result->editor_style_handles diff --git a/tests/phpunit/tests/rest-api/rest-block-type-controller.php b/tests/phpunit/tests/rest-api/rest-block-type-controller.php index 6e314e6af4ab7..6ae8954abf1be 100644 --- a/tests/phpunit/tests/rest-api/rest-block-type-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-type-controller.php @@ -261,6 +261,7 @@ public function test_get_item_invalid() { $this->assertSameSets( array(), $data['editor_script_handles'] ); $this->assertSameSets( array(), $data['script_handles'] ); $this->assertSameSets( array(), $data['view_script_handles'] ); + $this->assertSameSets( array(), $data['view_script_module_ids'] ); $this->assertSameSets( array(), $data['editor_style_handles'] ); $this->assertSameSets( array(), $data['style_handles'] ); $this->assertFalse( $data['is_dynamic'] ); @@ -339,6 +340,7 @@ public function test_get_item_defaults() { $this->assertSameSets( array(), $data['editor_script_handles'] ); $this->assertSameSets( array(), $data['script_handles'] ); $this->assertSameSets( array(), $data['view_script_handles'] ); + $this->assertSameSets( array(), $data['view_script_module_ids'] ); $this->assertSameSets( array(), $data['editor_style_handles'] ); $this->assertSameSets( array(), $data['style_handles'] ); $this->assertFalse( $data['is_dynamic'] ); @@ -562,7 +564,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 32, $properties ); + $this->assertCount( 33, $properties ); $this->assertArrayHasKey( 'api_version', $properties ); $this->assertArrayHasKey( 'name', $properties ); $this->assertArrayHasKey( 'title', $properties ); @@ -586,6 +588,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'editor_script_handles', $properties ); $this->assertArrayHasKey( 'script_handles', $properties ); $this->assertArrayHasKey( 'view_script_handles', $properties ); + $this->assertArrayHasKey( 'view_script_module_ids', $properties ); $this->assertArrayHasKey( 'editor_style_handles', $properties ); $this->assertArrayHasKey( 'style_handles', $properties ); $this->assertArrayHasKey( 'view_style_handles', $properties, 'schema must contain view_style_handles' ); @@ -718,6 +721,7 @@ protected function check_block_type_object( $block_type, $data, $links ) { 'editor_script_handles', 'script_handles', 'view_script_handles', + 'view_script_module_ids', 'editor_style_handles', 'style_handles', // Deprecated fields.