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

Modules API: Refactor, tests, and final dependencies array structure #57231

Merged
merged 11 commits into from Dec 20, 2023
185 changes: 121 additions & 64 deletions lib/experimental/modules/class-gutenberg-modules.php
Expand Up @@ -20,64 +20,90 @@ class Gutenberg_Modules {
private static $registered = array();

/**
* An array of queued modules.
* An array of module identifiers that were enqueued before registered.
*
* @var string[]
* @var array
*/
private static $enqueued = array();
private static $enqueued_modules_before_register = array();

/**
* Registers the module if no module with that module identifier already
* exists.
* Registers the module if no module with that module identifier has already
* been registered.
*
* @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.
* @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 dependencies of this module. The dependencies can be strings or arrays. If they are arrays, they need an `id` key with the module identifier, and can contain a `type` key with either `static` or `dynamic`. By default, dependencies that don't contain a type are considered static.
* @param string|false|null $version Optional. String specifying module version number. Defaults to false. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, the version is the current timestamp. If $version is set to false, the version number is the currently installed WordPress version. If $version is 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(),
);
$deps = array();
foreach ( $dependencies as $dependency ) {
if ( isset( $dependency['id'] ) ) {
$deps[] = array(
'id' => $dependency['id'],
'type' => isset( $dependency['type'] ) && 'dynamic' === $dependency['type'] ? 'dynamic' : 'static',
);
} elseif ( is_string( $dependency ) ) {
$deps[] = array(
'id' => $dependency,
'type' => 'static',
);
}
}

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

/**
* Enqueues a module in the page.
* Marks the module to be enqueued 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;
if ( isset( self::$registered[ $module_identifier ] ) ) {
self::$registered[ $module_identifier ]['enqueued'] = true;
} elseif ( ! in_array( $module_identifier, self::$enqueued_modules_before_register, true ) ) {
self::$enqueued_modules_before_register[] = $module_identifier;
}
}

/**
* Unmarks the module so it is no longer enqueued in the page.
*
* @param string $module_identifier The identifier of the module.
*/
public static function dequeue( $module_identifier ) {
if ( isset( self::$registered[ $module_identifier ] ) ) {
self::$registered[ $module_identifier ]['enqueued'] = false;
}
$key = array_search( $module_identifier, self::$enqueued_modules_before_register, true );
if ( false !== $key ) {
array_splice( self::$enqueued_modules_before_register, $key, 1 );
}
}

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

/**
* Prints the import map.
* Prints the import map using a script tag with an type="importmap" attribute.
*/
public static function print_import_map() {
$import_map = self::get_import_map();
Expand All @@ -90,27 +116,30 @@ public static function print_import_map() {
* 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,
)
);
}
foreach ( self::get_enqueued() as $module_identifier => $module ) {
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.
* Prints the the static dependencies of the enqueued modules using link tags
* with rel="modulepreload" attributes.
*/
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 . '">';
foreach ( self::get_dependencies( array_keys( self::get_enqueued() ), array( 'static' ) ) as $module_identifier => $module ) {
if ( true !== $module['enqueued'] ) {
echo sprintf(
'<link rel="modulepreload" href="%s" id="%s">',
esc_attr( $module['src'] . self::get_version_query_string( $module['version'] ) ),
esc_attr( $module_identifier )
);
}
}
}

Expand All @@ -120,7 +149,7 @@ public static function print_module_preloads() {
*
* TODO: Replace the polyfill with a simpler version that only provides
* support for import maps and load it only when the browser doesn't support
* import maps (https://github.com/guybedford/es-module-shims/issues/371).
* import maps (https://github.com/guybedford/es-module-shims/issues/406).
*/
public static function print_import_map_polyfill() {
$import_map = self::get_import_map();
Expand All @@ -135,15 +164,17 @@ public static function print_import_map_polyfill() {
}

/**
* 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.
* Gets the version of a module.
*
* If SCRIPT_DEBUG is true, the version is the current timestamp. If $version
* is set to false, the version number is the currently installed WordPress
* version. If $version is set to null, no version is added.
*
* @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 ) {
if ( defined( 'SCRIPT_DEBUG ' ) && SCRIPT_DEBUG ) {
return '?ver=' . time();
} elseif ( false === $version ) {
return '?ver=' . get_bloginfo( 'version' );
Expand All @@ -154,57 +185,83 @@ private static function get_version_query_string( $version ) {
}

/**
* 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.
* Retrieves an array of enqueued modules.
*
* @return array Array of modules keyed by module identifier.
*/
private static function get_enqueued() {
$enqueued = array();
foreach ( self::$registered as $module_identifier => $module ) {
if ( true === $module['enqueued'] ) {
$enqueued[ $module_identifier ] = $module;
}
}
return $enqueued;
}

/**
* Retrieves all the dependencies for given modules depending on type.
*
* @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.
* This method is recursive to also retrieve dependencies of the dependencies.
* It will consolidate an array containing unique dependencies based on the
* requested types ('static' or 'dynamic').
*
* @param array $module_identifiers The identifiers of the modules for which to gather dependencies.
* @param array $types Optional. Types of dependencies to retrieve: 'static', 'dynamic', or both. Default is both.
* @return array Array of modules keyed by module identifier.
*/
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 ] );
foreach ( self::$registered[ $module_identifier ]['dependencies'] as $dependency ) {
if (
in_array( $dependency['type'], $types, true ) &&
isset( self::$registered[ $dependency['id'] ] ) &&
! isset( $dependency_modules[ $dependency['id'] ] )
) {
$dependencies[ $dependency['id'] ] = self::$registered[ $dependency['id'] ];
}
}
$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 ) );
return array_merge( $dependency_modules, $dependencies, self::get_dependencies( array_keys( $dependencies ), $types ) );
},
array()
);
}
}

/**
* Registers a JavaScript module. It will be added to the import map.
* Registers the module if no module with that module identifier has already
* been registered.
*
* @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.
* @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 dependencies of this module. The dependencies can be strings or arrays. If they are arrays, they need an `id` key with the module identifier, and can contain a `type` key with either `static` or `dynamic`. By default, dependencies that don't contain a type are considered static.
* @param string|false|null $version Optional. String specifying module version number. Defaults to false. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, the version is the current timestamp. If $version is set to false, the version number is the currently installed WordPress version. If $version is 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.
* Marks the module to be enqueued in the page.
*
* @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map.
* @param string $module_identifier The identifier of the module.
*/
function gutenberg_enqueue_module( $module_identifier ) {
Gutenberg_Modules::enqueue( $module_identifier );
}

/**
* Unmarks the module so it is not longer enqueued in the page.
luisherranz marked this conversation as resolved.
Show resolved Hide resolved
*
* @param string $module_identifier The identifier of the module.
*/
function gutenberg_dequeue_module( $module_identifier ) {
Gutenberg_Modules::dequeue( $module_identifier );
}

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

Expand Down