Skip to content

Commit

Permalink
Interactivity API: Use modules instead of scripts in the frontend (#5…
Browse files Browse the repository at this point in the history
…6143)

* Bundle `@wordpress/interactivity` as an ES module

* Add the class with a basic API

* Make it work with the Navigation block

* Add versions

* Register `@wordpress/interactivity` module

* Add query and image blocks

* Add id with module identifier to the enqueued modules

* Add requried $usage for admin/frontend

* Avoid the use of enqueue to register modules

* Refactor versions

* Switch from `both` to `['admin', 'frontend']`

* Improve comments

* Add an optional static dependency graph

* Add static and dynamic dependencies in the server

* Add the file and search blocks

* Move $version out of $args to match wp_register_script

* Improve version logic

* Add polyfill

* Fix $version using its own arg in register calls

* Add unit tests

* Refactor tests, add test get import map

* Cleaning data

* Use gutenberg_url()

* Use wp_print_script_tag

* Fix DocBlock on get_import_map()

* Load navigation module only on Gutenberg plugin

* Load query module only on Gutenberg plugin

* Load search module only on Gutenberg plugin

* Load file module only on Gutenberg plugin

* Load image module only on Gutenberg plugin

* Make registration optional

* Improve navigation logic

* Remove unnecessary check

* Fix missing view file

* Don't print the import map if it's empty

* Load the importmap polyfill locally

* Move the es-modules-shims package to the top

* Use the public functions in the tests

* Update test to be more output oriented

* Remove we from comments.

* Start using modules in the interactivity e2e tests

* Update package-lock.json

---------

Co-authored-by: Carlos Bravo <carlos.bravo@automattic.com>
  • Loading branch information
luisherranz and cbravobernal authored Nov 29, 2023
1 parent ae430fe commit fe82fa0
Show file tree
Hide file tree
Showing 54 changed files with 1,052 additions and 681 deletions.
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre

- **Name:** core/image
- **Category:** media
- **Supports:** align (center, full, left, right, wide), anchor, color (~~background~~, ~~text~~), filter (duotone)
- **Supports:** align (center, full, left, right, wide), anchor, color (~~background~~, ~~text~~), filter (duotone), interactivity
- **Attributes:** alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width

## Latest Comments
Expand Down
32 changes: 20 additions & 12 deletions lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -543,20 +543,28 @@ private static function get_nav_element_directives( $should_load_view_script ) {
*/
private static function handle_view_script_loading( $attributes, $block, $inner_blocks ) {
$should_load_view_script = static::should_load_view_script( $attributes, $inner_blocks );
$is_gutenberg_plugin = defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN;
$view_js_file = 'wp-block-navigation-view';
$script_handles = $block->block_type->view_script_handles;

$view_js_file = 'wp-block-navigation-view';

// If the script already exists, there is no point in removing it from viewScript.
if ( ! wp_script_is( $view_js_file ) ) {
$script_handles = $block->block_type->view_script_handles;

// If the script is not needed, and it is still in the `view_script_handles`, remove it.
if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) {
$block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) );
if ( $is_gutenberg_plugin ) {
if ( $should_load_view_script ) {
gutenberg_enqueue_module( '@wordpress/block-library/navigation-block' );
}
// If the script is needed, but it was previously removed, add it again.
if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) {
$block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) );
// Remove the view script because we are using the module.
$block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) );
} else {
// If the script already exists, there is no point in removing it from viewScript.
if ( ! wp_script_is( $view_js_file ) ) {

// If the script is not needed, and it is still in the `view_script_handles`, remove it.
if ( ! $should_load_view_script && in_array( $view_js_file, $script_handles, true ) ) {
$block->block_type->view_script_handles = array_diff( $script_handles, array( $view_js_file ) );
}
// If the script is needed, but it was previously removed, add it again.
if ( $should_load_view_script && ! in_array( $view_js_file, $script_handles, true ) ) {
$block->block_type->view_script_handles = array_merge( $script_handles, array( $view_js_file ) );
}
}
}
}
Expand Down
33 changes: 33 additions & 0 deletions lib/experimental/interactivity-api/modules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php
/**
* Interactive modules.
*
* @package Gutenberg
* @subpackage Interactivity API
*/

/**
* Register the `@wordpress/interactivity` module.
*/
function gutenberg_register_interactivity_module() {
gutenberg_register_module(
'@wordpress/interactivity',
gutenberg_url( '/build/interactivity/index.min.js' ),
array(),
defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' )
);

// TODO: Replace with a simpler version that only provides support for import maps.
// TODO: Load only if the browser doesn't support import maps (https://github.com/guybedford/es-module-shims/issues/371).
wp_enqueue_script(
'es-module-shims',
gutenberg_url( '/build/importmap-polyfill.min.js' ),
array(),
null,
array(
'strategy' => 'defer',
)
);
}

add_action( 'wp_enqueue_scripts', 'gutenberg_register_interactivity_module' );
40 changes: 0 additions & 40 deletions lib/experimental/interactivity-api/scripts.php

This file was deleted.

195 changes: 195 additions & 0 deletions lib/experimental/modules/class-gutenberg-modules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
<?php
/**
* Modules API: Gutenberg_Modules class.
*
* Native support for ES Modules and Import Maps.
*
* @package Gutenberg
* @subpackage Modules
*/

/**
* Gutenberg_Modules class
*/
class Gutenberg_Modules {
/**
* An array of registered modules, keyed by module identifier.
*
* @var array
*/
private static $registered = array();

/**
* An array of queued modules.
*
* @var string[]
*/
private static $enqueued = array();

/**
* Registers the module if no module with that module identifier already
* exists.
*
* @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map.
* @param string $src Full URL of the module, or path of the script relative to the WordPress root directory.
* @param array $dependencies Optional. An array of module identifiers of the static and dynamic dependencies of this module. It can be an indexed array, in which case all the dependencies are static, or it can be an associative array, in which case it has to contain the keys `static` and `dynamic`.
* @param string|bool|null $version Optional. String specifying module version number. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, a timestamp is used. If it is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
*/
public static function register( $module_identifier, $src, $dependencies = array(), $version = false ) {
// Register the module if it's not already registered.
if ( ! isset( self::$registered[ $module_identifier ] ) ) {
$deps = array(
'static' => isset( $dependencies['static'] ) || isset( $dependencies['dynamic'] ) ? $dependencies['static'] ?? array() : $dependencies,
'dynamic' => isset( $dependencies['dynamic'] ) ? $dependencies['dynamic'] : array(),
);

self::$registered[ $module_identifier ] = array(
'src' => $src,
'version' => $version,
'dependencies' => $deps,
);
}
}

/**
* Enqueues a module in the page.
*
* @param string $module_identifier The identifier of the module.
*/
public static function enqueue( $module_identifier ) {
// Add the module to the queue if it's not already there.
if ( ! in_array( $module_identifier, self::$enqueued, true ) ) {
self::$enqueued[] = $module_identifier;
}
}

/**
* Returns the import map array.
*
* @return array Associative array with 'imports' key mapping to an array of module identifiers and their respective source strings.
*/
public static function get_import_map() {
$imports = array();
foreach ( self::get_dependencies( self::$enqueued, array( 'static', 'dynamic' ) ) as $module_identifier => $module ) {
$imports[ $module_identifier ] = $module['src'] . self::get_version_query_string( $module['version'] );
}
return array( 'imports' => $imports );
}

/**
* Prints the import map.
*/
public static function print_import_map() {
$import_map = self::get_import_map();
if ( ! empty( $import_map['imports'] ) ) {
echo '<script type="importmap">' . wp_json_encode( self::get_import_map(), JSON_HEX_TAG | JSON_HEX_AMP ) . '</script>';
}
}

/**
* Prints all the enqueued modules using <script type="module">.
*/
public static function print_enqueued_modules() {
foreach ( self::$enqueued as $module_identifier ) {
if ( isset( self::$registered[ $module_identifier ] ) ) {
$module = self::$registered[ $module_identifier ];
wp_print_script_tag(
array(
'type' => 'module',
'src' => $module['src'] . self::get_version_query_string( $module['version'] ),
'id' => $module_identifier,
)
);
}
}
}

/**
* Prints the link tag with rel="modulepreload" for all the static
* dependencies of the enqueued modules.
*/
public static function print_module_preloads() {
foreach ( self::get_dependencies( self::$enqueued, array( 'static' ) ) as $dependency_identifier => $module ) {
echo '<link rel="modulepreload" href="' . $module['src'] . self::get_version_query_string( $module['version'] ) . '" id="' . $dependency_identifier . '">';
}
}

/**
* Gets the module's version. It either returns a timestamp (if SCRIPT_DEBUG
* is true), the explicit version of the module if it is set and not false, or
* an empty string if none of the above conditions are met.
*
* @param array $version The version of the module.
* @return string A string presenting the version.
*/
private static function get_version_query_string( $version ) {
if ( SCRIPT_DEBUG ) {
return '?ver=' . time();
} elseif ( false === $version ) {
return '?ver=' . get_bloginfo( 'version' );
} elseif ( null !== $version ) {
return '?ver=' . $version;
}
return '';
}

/**
* Returns all unique static and/or dynamic dependencies for the received modules. It's
* recursive, so it will also get the static or dynamic dependencies of the dependencies.
*
* @param array $module_identifiers The identifiers of the modules to get dependencies for.
* @param array $types The type of dependencies to retrieve. It can be `static`, `dynamic` or both.
* @return array The array containing the unique dependencies of the modules.
*/
private static function get_dependencies( $module_identifiers, $types = array( 'static', 'dynamic' ) ) {
return array_reduce(
$module_identifiers,
function ( $dependency_modules, $module_identifier ) use ( $types ) {
if ( ! isset( self::$registered[ $module_identifier ] ) ) {
return $dependency_modules;
}

$dependencies = array();
foreach ( $types as $type ) {
$dependencies = array_merge( $dependencies, self::$registered[ $module_identifier ]['dependencies'][ $type ] );
}
$dependencies = array_unique( $dependencies );
$dependency_modules = array_intersect_key( self::$registered, array_flip( $dependencies ) );

return array_merge( $dependency_modules, $dependency_modules, self::get_dependencies( $dependencies, $types ) );
},
array()
);
}
}

/**
* Registers a JavaScript module. It will be added to the import map.
*
* @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map.
* @param string $src Full URL of the module, or path of the script relative to the WordPress root directory.
* @param array $dependencies Optional. An array of module identifiers of the static and dynamic dependencies of this module. It can be an indexed array, in which case all the dependencies are static, or it can be an associative array, in which case it has to contain the keys `static` and `dynamic`.
* @param string|bool|null $version Optional. String specifying module version number. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, a timestamp is used. If it is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
*/
function gutenberg_register_module( $module_identifier, $src, $dependencies = array(), $version = false ) {
Gutenberg_Modules::register( $module_identifier, $src, $dependencies, $version );
}

/**
* Enqueues a JavaScript module. It will be added to both the import map and a
* script tag with the "module" type.
*
* @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map.
*/
function gutenberg_enqueue_module( $module_identifier ) {
Gutenberg_Modules::enqueue( $module_identifier );
}

// Prints the import map in the head tag.
add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_import_map' ) );

// Prints the enqueued modules in the head tag.
add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_enqueued_modules' ) );

// Prints the preloaded modules in the head tag.
add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_module_preloads' ) );
4 changes: 3 additions & 1 deletion lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ function gutenberg_is_experiment_enabled( $name ) {

require __DIR__ . '/experimental/interactivity-api/class-wp-interactivity-store.php';
require __DIR__ . '/experimental/interactivity-api/store.php';
require __DIR__ . '/experimental/interactivity-api/scripts.php';
require __DIR__ . '/experimental/interactivity-api/modules.php';
require __DIR__ . '/experimental/interactivity-api/class-wp-directive-processor.php';
require __DIR__ . '/experimental/interactivity-api/class-wp-directive-context.php';
require __DIR__ . '/experimental/interactivity-api/directive-processing.php';
Expand All @@ -125,6 +125,8 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/experimental/interactivity-api/directives/wp-style.php';
require __DIR__ . '/experimental/interactivity-api/directives/wp-text.php';

require __DIR__ . '/experimental/modules/class-gutenberg-modules.php';

// Fonts API / Font Face.
remove_action( 'plugins_loaded', '_wp_theme_json_webfonts_handler' ); // Turns off WordPress 6.0's stopgap handler.

Expand Down
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"@wordpress/warning": "file:packages/warning",
"@wordpress/widgets": "file:packages/widgets",
"@wordpress/wordcount": "file:packages/wordcount",
"es-module-shims": "^1.8.2",
"wicg-inert": "3.1.2"
},
"devDependencies": {
Expand Down
Loading

1 comment on commit fe82fa0

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in fe82fa0.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/7035060015
📝 Reported issues:

Please sign in to comment.