Skip to content

Commit 95ec71c

Browse files
committed
Script Loader: Walk deps of empty-scoped queued entry points.
A module registered with `scopes => array()` and enqueued is an entry point: it is emitted as a `<script type="module">` and its imports must resolve in the browser even though its own bare specifier resolves nowhere. The dependency walk in `get_import_map()` previously stopped at every empty-scoped node, including the starting node, leaving the entry's deps absent from the import map. Stop traversal at *transitive* empty-scoped nodes only. Starting nodes (queue items and classic-script `module_dependencies`) are now exempt. The leak prevention for empty-scoped modules reached only via dep edges is preserved.
1 parent 602daa6 commit 95ec71c

2 files changed

Lines changed: 75 additions & 7 deletions

File tree

src/wp-includes/class-wp-script-modules.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -812,20 +812,28 @@ private function get_import_map(): array {
812812
* but they appear via dep edges of other modules — matching prior behavior of
813813
* `get_dependencies()`.
814814
*
815-
* Modules registered with empty scopes (`scopes => array()`) are not traversed:
816-
* their transitive deps are unreachable via bare specifier from any importer, so
817-
* walking through them would leak otherwise-private deps into top-level `imports`.
815+
* Modules registered with empty scopes (`scopes => array()`) reached as a *transitive*
816+
* dep are not traversed: their deps are unreachable via bare specifier from any importer,
817+
* so walking through them would leak otherwise-private deps into top-level `imports`.
818+
*
819+
* The starting nodes (queue items + classic-script module dependencies) are exempt
820+
* from this stop rule: queued empty-scoped modules are still entry points whose deps
821+
* must resolve when their `<script>` tag executes in the browser.
818822
*/
819823
$collected_dependencies = array();
820-
$id_queue = array_merge( $this->queue, $classic_script_module_dependencies );
824+
$id_queue = array();
825+
foreach ( array_merge( $this->queue, $classic_script_module_dependencies ) as $start_id ) {
826+
$id_queue[] = array( $start_id, true );
827+
}
821828
while ( ! empty( $id_queue ) ) {
822-
$current_id = array_shift( $id_queue );
829+
list( $current_id, $is_starting_node ) = array_shift( $id_queue );
823830
if ( ! isset( $this->registered[ $current_id ] ) ) {
824831
continue;
825832
}
826833

827-
// Stop at empty-scoped modules.
834+
// Stop traversal at transitive empty-scoped modules; entry points are exempt.
828835
if (
836+
! $is_starting_node &&
829837
isset( $this->registered[ $current_id ]['scopes'] ) &&
830838
array() === $this->registered[ $current_id ]['scopes']
831839
) {
@@ -838,7 +846,7 @@ private function get_import_map(): array {
838846
isset( $this->registered[ $dependency['id'] ] )
839847
) {
840848
$collected_dependencies[ $dependency['id'] ] = true;
841-
$id_queue[] = $dependency['id'];
849+
$id_queue[] = array( $dependency['id'], false );
842850
}
843851
}
844852
}

tests/phpunit/tests/script-modules/wpScriptModules.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3418,6 +3418,66 @@ public function test_scopes_classic_script_module_dep_on_empty_scoped_warns() {
34183418
$this->assertArrayNotHasKey( 'scopes', $map );
34193419
}
34203420

3421+
/**
3422+
* A queued module with `scopes => array()` is treated as an entry point: its transitive
3423+
* deps are still walked and emitted in the import map so the entry's imports resolve.
3424+
*
3425+
* Regression: previously the walk stopped at *every* empty-scoped node, including the
3426+
* starting node, leaving the entry's deps absent from the import map.
3427+
*
3428+
* @covers WP_Script_Modules::get_import_map
3429+
*/
3430+
public function test_scopes_empty_queued_entry_still_emits_dep_imports() {
3431+
$this->script_modules->register(
3432+
'@plugin/utils',
3433+
'/wp-content/plugins/x/utils.js',
3434+
array(),
3435+
null,
3436+
array( 'scopes' => array( '/wp-content/plugins/x/' ) )
3437+
);
3438+
$this->script_modules->register(
3439+
'@plugin/lib',
3440+
'/wp-content/plugins/x/lib.js',
3441+
array(
3442+
array(
3443+
'id' => '@plugin/utils',
3444+
'import' => 'dynamic',
3445+
),
3446+
),
3447+
null,
3448+
array( 'scopes' => array( '/wp-content/plugins/x/' ) )
3449+
);
3450+
// Public dep simulating @wordpress/* etc.
3451+
$this->script_modules->register( '@public/dep', '/wp-content/plugins/x/dep.js' );
3452+
$this->script_modules->register(
3453+
'@plugin/main',
3454+
'/wp-content/plugins/x/main.js',
3455+
array(
3456+
'@public/dep',
3457+
'@plugin/lib',
3458+
array(
3459+
'id' => '@plugin/utils',
3460+
'import' => 'dynamic',
3461+
),
3462+
),
3463+
null,
3464+
array( 'scopes' => array() )
3465+
);
3466+
$this->script_modules->enqueue( '@plugin/main' );
3467+
3468+
$map = $this->get_full_import_map();
3469+
3470+
// The queued entry's static and dynamic deps must resolve.
3471+
$this->assertArrayHasKey( '@public/dep', $map['imports'] ?? array() );
3472+
$this->assertArrayHasKey( 'scopes', $map );
3473+
$this->assertArrayHasKey( '/wp-content/plugins/x/', $map['scopes'] );
3474+
$this->assertArrayHasKey( '@plugin/lib', $map['scopes']['/wp-content/plugins/x/'] );
3475+
$this->assertArrayHasKey( '@plugin/utils', $map['scopes']['/wp-content/plugins/x/'] );
3476+
// The empty-scoped entry itself is not advertised in either map.
3477+
$this->assertArrayNotHasKey( '@plugin/main', $map['imports'] ?? array() );
3478+
$this->assertArrayNotHasKey( '@plugin/main', $map['scopes']['/wp-content/plugins/x/'] );
3479+
}
3480+
34213481
/**
34223482
* `module_id` resolution to a URL with no slash drops with a warning.
34233483
*

0 commit comments

Comments
 (0)