Skip to content

Commit

Permalink
Block API: Extend register_block_type_from_metadata to handle assets (
Browse files Browse the repository at this point in the history
#22519)

* Blocks: Extend `register_block_type_from_metadata` to hanle assets
The proposed approach follows the solution proposed in Block Registration RFC.

* Update lib/compat.php

Co-authored-by: Andrew Duthie <andrew@andrewduthie.com>

* Map explicitly from metadata to settings to filter out unwanted fields

* Improve error handling by following register from the WP_Block_Type_Registry

* Update unit tests for register_block_from_metadata

* Add handling for scripts/style handles and paths

* Remove styleVariations alias in favor of the supported styles property

* Improve readability of the metada file path handling

* Introduce helper functions for working with asset files

* Docs: Reflect proposed changes for block.json handling of asset files'

* Correct the way asset paths are handled

* Add missing example field in block metadata processing

* Add basic unit tests to cover functions that register handles

* Add more tests covering automatic block asset registration

Co-authored-by: Andrew Duthie <andrew@andrewduthie.com>
  • Loading branch information
gziolo and aduth authored Jun 12, 2020
1 parent 8dca7e9 commit f5efc30
Show file tree
Hide file tree
Showing 8 changed files with 423 additions and 33 deletions.
39 changes: 22 additions & 17 deletions docs/rfc/block-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This RFC is intended to serve both as a specification and as documentation for t

Behind any block type registration is some abstract concept of a unit of content. This content type can be described without consideration of any particular technology. In much the same way, we should be able to describe the core constructs of a block type in a way which can be interpreted in any runtime.

In more practical terms, an implementation should fulfill requirements that...
In more practical terms, an implementation should fulfill requirements that:

* A block type registration should be declarative and context-agnostic. Any runtime (PHP, JS, or other) should be able to interpret the basics of a block type (see "Block API" in the sections below) and should be able to fetch or retrieve the definitions of the context-specific implementation details. The following things should be made possible:
* Fetching the available block types through REST APIs.
Expand All @@ -16,7 +16,7 @@ In more practical terms, an implementation should fulfill requirements that...

It can statically analyze the files of any plugin to retrieve blocks and their properties.
* It should not require a build tool compilation step (e.g. Babel, Webpack) to author code which would be referenced in a block type definition.
* There should allow the potential to dynamically load ("lazy-load") block types, or parts of block type definitions. It practical terms, it means that the editor should be able to be loaded without enqueuing all the assets (scripts and styles) of all block types. What it needs is the basic metadata (`title`, `description`, `category`, `icon`, etc...) to start with. It should be fine to defer loading all other code (`edit`, `save`, `transforms`, and other JavaScript implementations) until it is explicitly used (inserted into the post content).
* There should allow the potential to dynamically load ("lazy-load") block types, or parts of block type definitions. It practical terms, it means that the editor should be able to be loaded without enqueuing all the assets (scripts and styles) of all block types. What it needs is the basic metadata (`title`, `description`, `category`, `icon`, etc) to start with. It should be fine to defer loading all other code (`edit`, `save`, `transforms`, and other JavaScript implementations) until it is explicitly used (inserted into the post content).

## References

Expand Down Expand Up @@ -67,7 +67,7 @@ To register a new block type, start by creating a `block.json` file. This file:
"category": "text",
"parent": [ "core/group" ],
"icon": "star",
"description": "Shows warning, error or success notices ...",
"description": "Shows warning, error or success notices",
"keywords": [ "alert", "message" ],
"textdomain": "my-plugin",
"attributes": {
Expand All @@ -90,10 +90,10 @@ To register a new block type, start by creating a `block.json` file. This file:
"message": "This is a notice!"
},
},
"editorScript": "build/editor.js",
"script": "build/main.js",
"editorStyle": "build/editor.css",
"style": "build/style.css"
"editorScript": "file:./build/index.js",
"script": "file:./build/script.js",
"editorStyle": "file:./build/index.css",
"style": "file:./build/style.css"
}
```

Expand Down Expand Up @@ -295,7 +295,7 @@ Plugins and Themes can also register [custom block style](/docs/designers-develo
"example": {
"attributes": {
"message": "This is a notice!"
},
}
}
}
```
Expand All @@ -312,7 +312,7 @@ Plugins and Themes can also register [custom block style](/docs/designers-develo
* Property: `editorScript`

```json
{ "editorScript": "build/editor.js" }
{ "editorScript": "file:./build/index.js" }
```

Block type editor script definition. It will only be enqueued in the context of the editor.
Expand All @@ -325,7 +325,7 @@ Block type editor script definition. It will only be enqueued in the context of
* Property: `script`

```json
{ "script": "build/main.js" }
{ "script": "file:./build/script.js" }
```

Block type frontend script definition. It will be enqueued both in the editor and when viewing the content on the front of the site.
Expand All @@ -338,7 +338,7 @@ Block type frontend script definition. It will be enqueued both in the editor an
* Property: `editorStyle`

```json
{ "editorStyle": "build/editor.css" }
{ "editorStyle": "file:./build/index.css" }
```

Block type editor style definition. It will only be enqueued in the context of the editor.
Expand All @@ -351,7 +351,7 @@ Block type editor style definition. It will only be enqueued in the context of t
* Property: `style`

```json
{ "style": "build/style.css" }
{ "style": "file:./build/style.css" }
```

Block type frontend style definition. It will be enqueued both in the editor and when viewing the content on the front of the site.
Expand Down Expand Up @@ -387,18 +387,23 @@ In the case of [dynamic blocks](/docs/designers-developers/developers/tutorials/

### `WPDefinedAsset`

The `WPDefinedAsset` type is a subtype of string, where the value must represent an absolute or relative path to a JavaScript or CSS file.
The `WPDefinedAsset` type is a subtype of string, where the value represents a path to a JavaScript or CSS file relative to where `block.json` file is located. The path provided must be prefixed with `file:`. This approach is based on how npm handles [local paths](https://docs.npmjs.com/files/package.json#local-paths) for packages.

An alternative would be a script or style handle name referencing a registered asset using WordPress helpers.

**Example:**

In `block.json`:
```json
{ "editorScript": "build/editor.js" }
{
"editorScript": "file:./build/index.js",
"editorStyle": "my-editor-style-handle"
}
```

#### WordPress context

In the context of WordPress, when a block is registered with PHP, it will automatically register all scripts and styles that are found in the `block.json` file.
In the context of WordPress, when a block is registered with PHP, it will automatically register all scripts and styles that are found in the `block.json` file and use file paths rather than asset handles.

That's why, the `WPDefinedAsset` type has to offer a way to mirror also the shape of params necessary to register scripts and styles using [`wp_register_script`](https://developer.wordpress.org/reference/functions/wp_register_script/) and [`wp_register_style`](https://developer.wordpress.org/reference/functions/wp_register_style/), and then assign these as handles associated with your block using the `script`, `style`, `editor_script`, and `editor_style` block type registration settings.

Expand All @@ -419,7 +424,7 @@ build/

In `block.json`:
```json
{ "editorScript": "build/index.js" }
{ "editorScript": "file:./build/index.js" }
```

In `build/index.asset.php`:
Expand Down Expand Up @@ -480,7 +485,7 @@ Implementation should follow the existing [get_plugin_data](https://codex.wordpr
There is also a new API method proposed `register_block_type_from_metadata` that aims to simplify the block type registration on the server from metadata stored in the `block.json` file. This function is going to handle also all necessary work to make internationalization work seamlessly for metadata defined.

This function takes two params:
- `$path` (`string`) – path to the folder where the `block.json` file is located.
- `$path` (`string`) – path to the folder where the `block.json` file is located or full path to the metadata file if named differently.
- `$args` (`array`) – an optional array of block type arguments. Default value: `[]`. Any arguments may be defined. However, the one described below is supported by default:
- `$render_callback` (`callable`) – callback used to render blocks of this block type.

Expand Down
188 changes: 182 additions & 6 deletions lib/compat.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,132 @@
*/

if ( ! function_exists( 'register_block_type_from_metadata' ) ) {
/**
* Removes the block asset's path prefix if provided.
*
* @since 5.5.0
*
* @param string $asset_handle_or_path Asset handle or prefixed path.
*
* @return string Path without the prefix or the original value.
*/
function remove_block_asset_path_prefix( $asset_handle_or_path ) {
$path_prefix = 'file:';
if ( strpos( $asset_handle_or_path, $path_prefix ) !== 0 ) {
return $asset_handle_or_path;
}
return substr(
$asset_handle_or_path,
strlen( $path_prefix )
);
}

/**
* Generates the name for an asset based on the name of the block
* and the field name provided.
*
* @since 5.5.0
*
* @param string $block_name Name of the block.
* @param string $field_name Name of the metadata field.
*
* @return string Generated asset name for the block's field.
*/
function generate_block_asset_handle( $block_name, $field_name ) {
$field_mappings = array(
'editorScript' => 'editor-script',
'script' => 'script',
'editorStyle' => 'editor-style',
'style' => 'style',
);
return str_replace( '/', '-', $block_name ) .
'-' . $field_mappings[ $field_name ];
}

/**
* Finds a script handle for the selected block metadata field. It detects
* when a path to file was provided and finds a corresponding
* asset file with details necessary to register the script under
* automatically generated handle name. It returns unprocessed script handle
* otherwise.
*
* @since 5.5.0
*
* @param array $metadata Block metadata.
* @param string $field_name Field name to pick from metadata.
*
* @return string|boolean Script handle provided directly or created through
* script's registration, or false on failure.
*/
function register_block_script_handle( $metadata, $field_name ) {
if ( empty( $metadata[ $field_name ] ) ) {
return false;
}
$script_handle = $metadata[ $field_name ];
$script_path = remove_block_asset_path_prefix( $metadata[ $field_name ] );
if ( $script_handle === $script_path ) {
return $script_handle;
}

$script_handle = generate_block_asset_handle( $metadata['name'], $field_name );
$script_asset_path = realpath(
dirname( $metadata['file'] ) . '/' .
substr_replace( $script_path, '.asset.php', - strlen( '.js' ) )
);
if ( ! file_exists( $script_asset_path ) ) {
$message = sprintf(
/* translators: %1: field name. %2: block name */
__( 'The asset file for the "%1$s" defined in "%2$s" block definition is missing.', 'default' ),
$field_name,
$metadata['name']
);
_doing_it_wrong( __FUNCTION__, $message, '5.5.0' );
return false;
}
$script_asset = require( $script_asset_path );
$result = wp_register_script(
$script_handle,
plugins_url( $script_path, $metadata['file'] ),
$script_asset['dependencies'],
$script_asset['version']
);
return $result ? $script_handle : false;
}

/**
* Finds a style handle for the block metadata field. It detects when a path
* to file was provided and registers the style under automatically
* generated handle name. It returns unprocessed style handle otherwise.
*
* @since 5.5.0
*
* @param array $metadata Block metadata.
* @param string $field_name Field name to pick from metadata.
*
* @return string|boolean Style handle provided directly or created through
* style's registration, or false on failure.
*/
function register_block_style_handle( $metadata, $field_name ) {
if ( empty( $metadata[ $field_name ] ) ) {
return false;
}
$style_handle = $metadata[ $field_name ];
$style_path = remove_block_asset_path_prefix( $metadata[ $field_name ] );
if ( $style_handle === $style_path ) {
return $style_handle;
}

$style_handle = generate_block_asset_handle( $metadata['name'], $field_name );
$block_dir = dirname( $metadata['file'] );
$result = wp_register_style(
$style_handle,
plugins_url( $style_path, $metadata['file'] ),
array(),
filemtime( realpath( "$block_dir/$style_path" ) )
);
return $result ? $style_handle : false;
}

/**
* Registers a block type from metadata stored in the `block.json` file.
*
Expand All @@ -25,22 +151,72 @@
* @return WP_Block_Type|false The registered block type on success, or false on failure.
*/
function register_block_type_from_metadata( $file_or_folder, $args = array() ) {
$file = ( substr( $file_or_folder, -10 ) !== 'block.json' ) ?
trailingslashit( $file_or_folder ) . 'block.json' :
$filename = 'block.json';
$metadata_file = ( substr( $file_or_folder, -strlen( $filename ) ) !== $filename ) ?
trailingslashit( $file_or_folder ) . $filename :
$file_or_folder;
if ( ! file_exists( $file ) ) {
if ( ! file_exists( $metadata_file ) ) {
return false;
}

$metadata = json_decode( file_get_contents( $file ), true );
if ( ! is_array( $metadata ) ) {
$metadata = json_decode( file_get_contents( $metadata_file ), true );
if ( ! is_array( $metadata ) || empty( $metadata['name'] ) ) {
return false;
}
$metadata['file'] = $metadata_file;

$settings = array();
$property_mappings = array(
'title' => 'title',
'category' => 'category',
'parent' => 'parent',
'icon' => 'icon',
'description' => 'description',
'keywords' => 'keywords',
'attributes' => 'attributes',
'supports' => 'supports',
'styles' => 'styles',
'example' => 'example',
);

foreach ( $property_mappings as $key => $mapped_key ) {
if ( isset( $metadata[ $key ] ) ) {
$settings[ $mapped_key ] = $metadata[ $key ];
}
}

if ( ! empty( $metadata['editorScript'] ) ) {
$settings['editor_script'] = register_block_script_handle(
$metadata,
'editorScript'
);
}

if ( ! empty( $metadata['script'] ) ) {
$settings['script'] = register_block_script_handle(
$metadata,
'script'
);
}

if ( ! empty( $metadata['editorStyle'] ) ) {
$settings['editor_style'] = register_block_style_handle(
$metadata,
'editorStyle'
);
}

if ( ! empty( $metadata['style'] ) ) {
$settings['style'] = register_block_style_handle(
$metadata,
'style'
);
}

return register_block_type(
$metadata['name'],
array_merge(
$metadata,
$settings,
$args
)
);
Expand Down
Loading

0 comments on commit f5efc30

Please sign in to comment.