Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment: A Modules API with optional static server dependency graph #56118

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:** anchor, color (~~background~~, ~~text~~), filter (duotone)
- **Supports:** anchor, color (~~background~~, ~~text~~), filter (duotone), interactivity
- **Attributes:** align, alt, aspectRatio, caption, height, href, id, lightbox, linkClass, linkDestination, linkTarget, rel, scale, sizeSlug, title, url, width

## Latest Comments
Expand Down
35 changes: 35 additions & 0 deletions lib/experimental/interactivity-api/modules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
/**
* Interactive modules.
*
* @package Gutenberg
* @subpackage Interactivity API
*/

/**
* Register the `@wordpress/interactivity` module.
*/
function gutenberg_register_interactivity_module() {
gutenberg_register_module(
'@wordpress/interactivity',
'/wp-content/plugins/gutenberg/build/interactivity/index.min.js',
'frontend',
array(
'version' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ),
)
);

// TODO: Move this to a local file and replace with a simpler version that
// only provides support for import maps.
wp_enqueue_script(
'es-module-shims',
'https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.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.

229 changes: 229 additions & 0 deletions lib/experimental/modules/class-gutenberg-modules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
<?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 string|array $usage Specifies where the module would be used. Can be 'admin', 'frontend', or an array of such strings.
* @param array $args {
* Optional array of arguments.
*
* @type string|bool $ver Optional. String specifying script version number, if it has one, it is added to the URL
* as a query string for cache busting purposes. If version is set to false, a version
* number is automatically added equal to current installed WordPress version. If SCRIPT_DEBUG
* is set to true, it uses the timestamp instead.
* @type array $dependencies Optional. An array of module identifiers of the static dependencies of this module.
* }
*/
public static function register( $module_identifier, $src, $usage, $args = array() ) {
// Normalize $usage to an array.
if ( ! is_array( $usage ) ) {
$usage = array( $usage );
}

// Register the module if it's not already registered.
if ( ! isset( self::$registered[ $module_identifier ] ) ) {
self::$registered[ $module_identifier ] = array(
'src' => $src,
'usage' => $usage,
'version' => isset( $args['version'] ) ? $args['version'] : '',
'dependencies' => isset( $args['dependencies'] ) ? $args['dependencies'] : array(),
);
}
}

/**
* 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 string The import map.
*/
public static function get_import_map() {
$import_map = array(
'imports' => array(),
);

foreach ( self::$registered as $module_identifier => $module ) {
if ( self::get_appropriate_usage( $module['usage'] ) ) {
$import_map['imports'][ $module_identifier ] = $module['src'] . self::get_module_version( $module );
}
}

return $import_map;
}

/**
* Prints the import map.
*/
public static function print_import_map() {
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 ] ) && self::get_appropriate_usage( self::$registered[ $module_identifier ]['usage'] ) ) {
$module = self::$registered[ $module_identifier ];
$version = self::get_module_version( $module );
echo '<script type="module" src="' . $module['src'] . $version . '" id="' . $module_identifier . '"></script>';
}
}
}

/**
* 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 ) as $dependency_identifier => $module ) {
$version = self::get_module_version( $module );
echo '<link rel="modulepreload" href="' . $module['src'] . $version . '" id="' . $dependency_identifier . '">';
}
}

/**
* Determines if the usage is appropriate for the current context.
*
* @param array $usage Specifies the usage of the module. Can contain 'admin' or 'frontend'.
* @return bool Returns true if it's appropriate to load the module in the current WP context.
*/
private static function get_appropriate_usage( $usage ) {
if ( in_array( 'admin', $usage, true ) && is_admin() ) {
return true;
}
if ( in_array( 'frontend', $usage, true ) && ! is_admin() ) {
return true;
}
return false;
}

/**
* 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 $module The data of the module.
* @return string A string presenting the version.
*/
private static function get_module_version( $module ) {
if ( SCRIPT_DEBUG ) {
return '?ver=' . time();
} elseif ( $module['version'] ) {
return '?ver=' . $module['version'];
}
return '';
}

/**
* Returns all unique static dependencies for the received modules. It's
* recursive, so it will also get the static dependencies of the dependencies.
*
* @param array $module_identifiers The identifiers of the modules to get dependencies for.
* @return array The array containing the unique dependencies of the modules.
*/
public static function get_dependencies( $module_identifiers ) {
return array_reduce(
$module_identifiers,
function ( $dependency_modules, $module_identifier ) {
if (
! isset( self::$registered[ $module_identifier ] ) ||
! self::get_appropriate_usage( self::$registered[ $module_identifier ]['usage'] ) ||
empty( self::$registered[ $module_identifier ]['dependencies'] )
) {
return $dependency_modules;
}

$filtered_dependencies = array_filter(
self::$registered[ $module_identifier ]['dependencies'],
function ( $dependency_identifier ) {
return isset( self::$registered[ $dependency_identifier ] ) && self::get_appropriate_usage( self::$registered[ $dependency_identifier ]['usage'] );
}
);

$dependency_data = array_intersect_key( self::$registered, array_flip( $filtered_dependencies ) );
return array_merge( $dependency_modules, $dependency_data, self::get_dependencies( $filtered_dependencies ) );
},
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 string $usage Specifies where the module would be used. Can be 'admin', 'frontend', or 'both'.
* @param array $args {
* Optional array of arguments.
*
* @type string|bool $ver Optional. String specifying script version number, if it has one, it is added to the URL
* as a query string for cache busting purposes. If version is set to false, a version
* number is automatically added equal to current installed WordPress version. If SCRIPT_DEBUG
* is set to true, it uses the timestamp instead.
* @type array $dependencies Optional. An array of module identifiers of the static dependencies of this module.
* }
*/
function gutenberg_register_module( $module_identifier, $src, $usage, $args = array() ) {
Gutenberg_Modules::register( $module_identifier, $src, $usage, $args );
}

/**
* 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 @@ -138,7 +138,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 @@ -148,6 +148,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
1 change: 0 additions & 1 deletion packages/block-library/src/file/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
},
"interactivity": true
},
"viewScript": "file:./view.min.js",
"editorStyle": "wp-block-file-editor",
"style": "wp-block-file"
}
25 changes: 12 additions & 13 deletions packages/block-library/src/file/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,8 @@
*/
function render_block_core_file( $attributes, $content, $block ) {
$should_load_view_script = ! empty( $attributes['displayPreview'] );
$view_js_file = 'wp-block-file-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 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 ) );
}
if ( $should_load_view_script ) {
gutenberg_enqueue_module( '@wordpress/block-library/file-block' );
}

// Update object's aria-label attribute if present in block HTML.
Expand Down Expand Up @@ -96,5 +85,15 @@ function register_block_core_file() {
'render_callback' => 'render_block_core_file',
)
);

gutenberg_register_module(
'@wordpress/block-library/file-block',
'/wp-content/plugins/gutenberg/build/interactivity/file.min.js',
'frontend',
array(
'version' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ),
'dependencies' => array( '@wordpress/interactivity' ),
)
);
}
add_action( 'init', 'register_block_core_file' );
4 changes: 2 additions & 2 deletions packages/block-library/src/image/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
}
},
"supports": {
"interactivity": true,
"anchor": true,
"color": {
"text": false,
Expand Down Expand Up @@ -130,6 +131,5 @@
{ "name": "rounded", "label": "Rounded" }
],
"editorStyle": "wp-block-image-editor",
"style": "wp-block-image",
"viewScript": "file:./view.min.js"
"style": "wp-block-image"
}
Loading
Loading