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

Allow classic scripts to depend on modules #8024

Open
wants to merge 17 commits into
base: trunk
Choose a base branch
from
Open
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
67 changes: 58 additions & 9 deletions src/wp-includes/class-wp-script-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,15 @@ public function add_hooks() {
*/
public function print_enqueued_script_modules() {
foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) {
$src = $this->get_src( $id );
if ( null === $src ) {
continue;
}

wp_print_script_tag(
array(
'type' => 'module',
'src' => $this->get_src( $id ),
'src' => $src,
'id' => $id . '-js-module',
)
);
Expand All @@ -228,11 +233,16 @@ public function print_enqueued_script_modules() {
*/
public function print_script_module_preloads() {
foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ), array( 'static' ) ) as $id => $script_module ) {
$src = $this->get_src( $id );
if ( null === $src ) {
continue;
}

// Don't preload if it's marked for enqueue.
if ( true !== $script_module['enqueue'] ) {
echo sprintf(
'<link rel="modulepreload" href="%s" id="%s">',
esc_url( $this->get_src( $id ) ),
esc_url( $src ),
esc_attr( $id . '-js-modulepreload' )
);
}
Expand Down Expand Up @@ -262,14 +272,53 @@ public function print_import_map() {
*
* @since 6.5.0
*
* @return array Array with an `imports` key mapping to an array of script module identifiers and their respective
* URLs, including the version query.
* @global WP_Dependencies $wp_scripts
*
* @return array Array with an `imports` key mapping to an array of script
* module identifiers and their respective URLs, including
* the version query.
*/
private function get_import_map(): array {
global $wp_scripts;

$imports = array();
foreach ( $this->get_dependencies( array_keys( $this->get_marked_for_enqueue() ) ) as $id => $script_module ) {
$imports[ $id ] = $this->get_src( $id );

$classic_script_dependencies = array();
if ( $wp_scripts instanceof WP_Scripts ) {
foreach ( $wp_scripts->registered as $dependency ) {
Comment on lines +287 to +288
Copy link
Member Author

Choose a reason for hiding this comment

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

This is similar to what wp_dependencies_unique_hosts does:

foreach ( array( $wp_scripts, $wp_styles ) as $dependencies ) {
if ( $dependencies instanceof WP_Dependencies && ! empty( $dependencies->queue ) ) {
foreach ( $dependencies->queue as $handle ) {

I'm not confident this is the most optimal way to get all classic scripts in the enqueued dependency graph so would appreciate scrutiny or domain knowledge on that.

$handle = $dependency->handle;

if (
! $wp_scripts->query( $handle, 'done' ) &&
! $wp_scripts->query( $handle, 'to_do' ) &&
! $wp_scripts->query( $handle, 'enqueued' )
) {
continue;
}
Comment on lines +291 to +297
Copy link
Member Author

Choose a reason for hiding this comment

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

Is it sufficient to just check enqueued?

Suggested change
if (
! $wp_scripts->query( $handle, 'done' ) &&
! $wp_scripts->query( $handle, 'to_do' ) &&
! $wp_scripts->query( $handle, 'enqueued' )
) {
continue;
}
if ( ! $wp_scripts->query( $handle, 'enqueued' ) ) {
continue;
}


$module_deps = $wp_scripts->get_data( $handle, 'module_deps' );
if ( ! $module_deps ) {
continue;
}
foreach ( $module_deps as $id ) {
$src = $this->get_src( $id );
if ( null === $src ) {
continue;
}
$imports[ $id ] = $src;
$classic_script_dependencies[] = $id;
}
}
}

foreach ( $this->get_dependencies( array_merge( $classic_script_dependencies, array_keys( $this->get_marked_for_enqueue() ) ) ) as $id => $script_module ) {
$src = $this->get_src( $id );
if ( null === $src ) {
continue;
}
$imports[ $id ] = $src;
}

return array( 'imports' => $imports );
}

Expand Down Expand Up @@ -335,11 +384,11 @@ function ( $dependency_script_modules, $id ) use ( $import_types ) {
* @since 6.5.0
*
* @param string $id The script module identifier.
* @return string The script module src with a version if relevant.
* @return string|null The script module src with a version if relevant.
*/
private function get_src( string $id ): string {
private function get_src( string $id ): ?string {
if ( ! isset( $this->registered[ $id ] ) ) {
return '';
return null;
}

$script_module = $this->registered[ $id ];
Expand Down
52 changes: 52 additions & 0 deletions src/wp-includes/class-wp-scripts.php
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,58 @@ public function __construct() {
add_action( 'init', array( $this, 'init' ), 0 );
}

/**
* Register an item.
*
* Registers the item if no item of that name already exists.
*
* This method is subclassed here in order to add special handling for script module
* dependencies.
*
* @since 6.8.0
*
* @see WP_Dependencies::add()
*
* @param string $handle Name of the item. Should be unique.
* @param string|false $src Full URL of the item, or path of the item relative
* to the WordPress root directory. If source is set to false,
* the item is an alias of other items it depends on.
* @param (string|array{type: string, id: string})[] $deps Optional. An array of registered item handles this item depends on.
* Default empty array.
* @param string|bool|null $ver Optional. String specifying item version number, if it has one,
* which 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 set to null, no version is added.
* @param mixed $args Optional. Custom property of the item. NOT the class property $args.
* Examples: $media, $in_footer.
* @return bool Whether the item has been registered. True on success, false on failure.
*/
public function add( $handle, $src, $deps = array(), $ver = false, $args = null ) {
$module_deps = array();
$script_deps = array();
if ( array() !== $deps ) {
foreach ( $deps as $dep ) {
if ( is_string( $dep ) ) {
$script_deps[] = $dep;
} elseif (
isset( $dep['type'], $dep['id'] ) &&
'module' === $dep['type'] &&
is_string( $dep['id'] )
) {
$module_deps[] = $dep['id'];
}
}
}
if ( ! parent::add( $handle, $src, $script_deps, $ver, $args ) ) {
return false;
}
if ( array() !== $module_deps ) {
$this->add_data( $handle, 'module_deps', $module_deps );
}
return true;
}

/**
* Initialize the class.
*
Expand Down
142 changes: 138 additions & 4 deletions tests/phpunit/tests/script-modules/wpScriptModules.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,44 @@ public function set_up() {
parent::set_up();
// Set up the WP_Script_Modules instance.
$this->script_modules = new WP_Script_Modules();
unset( $GLOBALS['wp_scripts'] );
}

private static $wp_scripts;
private static $wp_scripts_was_set = false;

public static function set_up_before_class() {
parent::set_up_before_class();

// If the global is set, store it for restoring when done testing.
static::$wp_scripts_was_set = array_key_exists( 'wp_scripts', $GLOBALS );
if ( static::$wp_scripts_was_set ) {
static::$wp_scripts = $GLOBALS['wp_scripts'];
unset( $GLOBALS['wp_scripts'] );
}
}

public static function tear_down_after_class() {
// Restore the global if it was set before running this set of tests.
if ( static::$wp_scripts_was_set ) {
$GLOBALS['wp_scripts'] = static::$wp_scripts;
}

parent::tear_down_after_class();
}

public function clean_up_global_scope() {
unset( $GLOBALS['wp_scripts'] );
parent::clean_up_global_scope();
}


/**
* Gets a list of the enqueued script modules.
*
* @return array Enqueued script module URLs, keyed by script module identifier.
*/
public function get_enqueued_script_modules() {
private function get_enqueued_script_modules() {
$script_modules_markup = get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) );
$p = new WP_HTML_Tag_Processor( $script_modules_markup );
$enqueued_script_modules = array();
Expand All @@ -54,18 +84,20 @@ public function get_enqueued_script_modules() {
*
* @return array Import map entry URLs, keyed by script module identifier.
*/
public function get_import_map() {
private function get_import_map() {
$import_map_markup = get_echo( array( $this->script_modules, 'print_import_map' ) );
preg_match( '/<script type="importmap" id="wp-importmap">.*?(\{.*\}).*?<\/script>/s', $import_map_markup, $import_map_string );
return json_decode( $import_map_string[1], true )['imports'];
return isset( $import_map_string[1] )
? json_decode( $import_map_string[1], true )['imports']
: array();
}

/**
* Gets a list of preloaded script modules.
*
* @return array Preloaded script module URLs, keyed by script module identifier.
*/
public function get_preloaded_script_modules() {
private function get_preloaded_script_modules() {
$preloaded_markup = get_echo( array( $this->script_modules, 'print_script_module_preloads' ) );
$p = new WP_HTML_Tag_Processor( $preloaded_markup );
$preloaded_script_modules = array();
Expand Down Expand Up @@ -906,4 +938,106 @@ public static function data_invalid_script_module_data(): array {
'string' => array( 'string' ),
);
}

/**
* @ticket 61500
*/
public function test_included_module_appears_in_importmap() {
$this->script_modules->register( 'dependency', '/dep.js' );
$this->script_modules->register( 'example', '/example.js', array( 'dependency' ) );

// Nothing printed now.
$this->assertSame( array(), $this->get_enqueued_script_modules(), 'Initial enqueued script modules was wrong.' );
$this->assertSame( array(), $this->get_preloaded_script_modules(), 'Initial module preloads was wrong.' );
$this->assertSame( array(), $this->get_import_map(), 'Initial import map was wrong.' );

// Enqueuing a script with a module dependency should add it to the import map.
wp_enqueue_script(
'classic',
'/classic.js',
array(
'classic-dependency',
array(
'type' => 'module',
'id' => 'example',
),
)
);

$this->assertSame( array(), $this->get_enqueued_script_modules(), 'Final enqueued script modules was wrong.' );
$this->assertSame( array(), $this->get_preloaded_script_modules(), 'Final module preloads was wrong.' );

$import_map = $this->get_import_map();
$this->assertCount( 2, $import_map, 'Final import map count was wrong.' );
$this->assertArrayHasKey( 'example', $import_map, 'Final missing "example" script module in import map.' );
$this->assertArrayHasKey( 'dependency', $import_map, 'Final missing "dependency" script module in import map.' );
}

/**
* @ticket 61500
*/
public function test_included_modules_concat_With_enqueued_dependencies() {
$this->script_modules->register( 'dependency-of-enqueued', '/dependency-of-enqueued.js' );
$this->script_modules->enqueue(
'enqueued',
'/enqueued.js',
array(
array(
'id' => 'dependency-of-enqueued',
'import' => 'dynamic',
),
)
);

$this->script_modules->register( 'classic-transitive-dependency', '/classic-transitive-dependency.js' );

$this->script_modules->register( 'dependency-of-not-enqueued', '/dependency-of-not-enqueued.js' );
$this->script_modules->register( 'not-enqueued', '/not-enqueued.js', array( 'dependency-of-not-enqueued' ) );

// Only dependency-enqueued should be printed.
$enqueued = $this->get_enqueued_script_modules();
$this->assertCount( 1, $enqueued, 'Initial enqueue count was wrong.' );
$this->assertArrayHasKey( 'enqueued', $enqueued, 'Initial missing "enqueued" script module enqueue.' );
$this->assertCount( 0, $this->get_preloaded_script_modules(), 'Initial module preload count was wrong.' );

$import_map = $this->get_import_map();
$this->assertCount( 1, $import_map, 'Initial import map count was wrong.' );
$this->assertArrayHasKey( 'dependency-of-enqueued', $import_map, 'Initial missing "dependency-of-enqueued" script module in import map.' );

// Enqueuing a script with a module dependency should add it to the import map.
wp_register_script(
'_test_classic-dependency_',
'/classic-transitive-dep.js',
array(
array(
'type' => 'module',
'id' => 'classic-transitive-dependency',
),
)
);
wp_enqueue_script(
'_test_classic_',
'/classic.js',
array(
'_test_classic-dependency_',
array(
'type' => 'module',
'id' => 'not-enqueued',
),
)
);

$enqueued = $this->get_enqueued_script_modules();
$this->assertCount( 1, $enqueued, 'Final enqueue count was wrong.' );
$this->assertArrayHasKey( 'enqueued', $enqueued, 'Final missing "enqueued" script module enqueue.' );

$this->assertCount( 0, $this->get_preloaded_script_modules(), 'Final module preload count was wrong.' );

$import_map = $this->get_import_map();
$this->assertCount( 4, $import_map, 'Final import map count was wrong.' );
$this->assertArrayHasKey( 'dependency-of-enqueued', $import_map, 'Final missing "dependency-of-enqueued" script module in import map.' );
$this->assertArrayHasKey( 'classic-transitive-dependency', $import_map, 'Final missing "classic-transitive-dependency" script module in import map.' );
$this->assertArrayHasKey( 'not-enqueued', $import_map, 'Final missing "not-enqueued" script module in import map.' );
$this->assertArrayHasKey( 'dependency-of-not-enqueued', $import_map, 'Final missing "dependency-of-not-enqueued" script module in import map.' );
}
}
Loading