Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
b64aab8
Enhancement: Add 'in_footer' argument to script module functions for …
b1ink0 Sep 13, 2025
1caf38a
Split printing into head/footer passes with printed script module tra…
b1ink0 Sep 18, 2025
4115360
Skip printing script modules in head if dependencies are set to print…
b1ink0 Sep 18, 2025
05584fc
Sort script module identifiers by dependencies before printing
b1ink0 Sep 24, 2025
ac4973c
Fix failing tests caused by tracking of printed script modules
b1ink0 Sep 29, 2025
040c249
Fix PHPCS warnings
b1ink0 Sep 29, 2025
e55cf3e
Improve documentation and add type hints
b1ink0 Sep 30, 2025
c794fad
Consolidate multi-line assignment to single expression
b1ink0 Sep 30, 2025
acaeceb
Use Reflection API to access done property in tests
b1ink0 Sep 30, 2025
8bc8f69
Remove unnecessary checks for `done` array
b1ink0 Oct 1, 2025
43bb39b
Add tests for head/footer placement and dependency ordering
b1ink0 Oct 9, 2025
377989d
Fix formatting by removing trailing comma
b1ink0 Oct 9, 2025
09897f5
Merge branch 'trunk' into enhancement/63486-script-modules-footer-sup…
b1ink0 Oct 14, 2025
8dfc49a
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Oct 17, 2025
45a40a3
Simplify script module printing methods
b1ink0 Oct 17, 2025
c8ccf33
Merge branch 'trunk' into enhancement/63486-script-modules-footer-sup…
westonruter Oct 17, 2025
5aef0c7
Move done member up next to queue member
westonruter Oct 17, 2025
3e96498
Address majority of PHPStan level 10 issues in class-wp-script-module…
westonruter Oct 17, 2025
9915940
Remove unused get_marked_for_enqueue() method
westonruter Oct 17, 2025
858ef91
Further refine types
westonruter Oct 17, 2025
4203864
Allow empty src for script module for the sake of Tests_Blocks_Regist…
westonruter Oct 17, 2025
8c71e84
Use more specific types
westonruter Oct 17, 2025
e60e4fa
Re-work get_dependencies() to return an array of script module IDs
westonruter Oct 17, 2025
dbdd3a1
Account for empty src for script module
westonruter Oct 17, 2025
78c7e7a
Simplify get_recursive_dependents() to remove closure
westonruter Oct 17, 2025
40e1260
Refactor get_sorted_dependencies() to eliminate closure
westonruter Oct 18, 2025
aeb22f0
Add missing '>'
westonruter Oct 18, 2025
9ba920a
Use get_sorted_dependencies() when printing preloads and only for sta…
westonruter Oct 18, 2025
60fd75f
Reduce nesting in print_script_module_preloads()
westonruter Oct 18, 2025
2438a12
Disregard the done array when getting sorted item dependencies
westonruter Oct 18, 2025
6b62d90
Remove the need for set_printed_script_modules() in test
westonruter Oct 18, 2025
1bba6a3
Ensure core script modules and iAPI script modules are printed in foo…
westonruter Oct 18, 2025
2c35487
Merge branch 'trunk' into enhancement/63486-script-modules-footer-sup…
westonruter Oct 19, 2025
2a695c5
Add test case for static dependency on dynamic which depends on anoth…
westonruter Oct 19, 2025
47a0013
Merge branch 'trunk' into enhancement/63486-script-modules-footer-sup…
westonruter Oct 20, 2025
b652fdd
Extract repeated wp_is_block_theme() calls into variable
westonruter Oct 20, 2025
de83bd2
Restore $position variable containing action name
westonruter Oct 20, 2025
379cba0
Merge branch 'trunk' into enhancement/63486-script-modules-footer-sup…
westonruter Oct 20, 2025
7c2e9c5
Remove PHPStan annotations
westonruter Oct 20, 2025
b1b7043
Add tests for enqueue and register with empty string for ID
westonruter Oct 20, 2025
58044f1
Improve empty src handling in get_src() and add test
westonruter Oct 20, 2025
0c46399
Merge branch 'trunk' into enhancement/63486-script-modules-footer-sup…
westonruter Oct 20, 2025
49c3d5b
Add missing WP_Script_Modules::set_in_footer() method
westonruter Oct 20, 2025
2950c7d
Add ticket references
westonruter Oct 20, 2025
4459c6f
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Oct 20, 2025
cde9ccf
Replace non-empty-string with string to amend 7c2e9c5ff3
westonruter Oct 20, 2025
04ec8ac
Restore private get_marked_for_enqueue() method since used via Reflec…
westonruter Oct 21, 2025
4fc5e27
Fix phpcs
westonruter Oct 21, 2025
bf733b2
Make note that in_footer only applies to block themes
westonruter Oct 21, 2025
922dcf0
Remove redundant array_unique()
westonruter Oct 21, 2025
d91f8c3
Remove another redundant array_unique()
westonruter Oct 21, 2025
c0179e6
Use multi-line comment
westonruter Oct 21, 2025
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
148 changes: 127 additions & 21 deletions src/wp-includes/class-wp-script-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ class WP_Script_Modules {
*/
private $dependents_map = array();

/**
* Holds the script module identifiers that have been printed.
*
* @since 6.9.0
* @var string[]
*/
private $done = array();

/**
* Registers the script module if no script module with that script module
* identifier has already been registered.
Expand Down Expand Up @@ -84,6 +92,7 @@ class WP_Script_Modules {
* @param array $args {
* Optional. An array of additional args. Default empty array.
*
* @type bool $in_footer Whether to print the script module in the footer. Default 'false'. Optional.
* @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
* }
*/
Expand All @@ -110,6 +119,8 @@ public function register( string $id, string $src, array $deps = array(), $versi
}
}

$in_footer = isset( $args['in_footer'] ) && (bool) $args['in_footer'];

$fetchpriority = 'auto';
if ( isset( $args['fetchpriority'] ) ) {
if ( $this->is_valid_fetchpriority( $args['fetchpriority'] ) ) {
Expand All @@ -132,6 +143,7 @@ public function register( string $id, string $src, array $deps = array(), $versi
'src' => $src,
'version' => $version,
'dependencies' => $dependencies,
'in_footer' => $in_footer,
'fetchpriority' => $fetchpriority,
);
}
Expand Down Expand Up @@ -217,6 +229,7 @@ public function set_fetchpriority( string $id, string $priority ): bool {
* @param array $args {
* Optional. An array of additional args. Default empty array.
*
* @type bool $in_footer Whether to print the script module in the footer. Default 'false'. Optional.
* @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
* }
*/
Expand Down Expand Up @@ -263,10 +276,17 @@ public function deregister( string $id ) {
* @since 6.5.0
*/
public function add_hooks() {
$position = wp_is_block_theme() ? 'wp_head' : 'wp_footer';
add_action( $position, array( $this, 'print_import_map' ) );
add_action( $position, array( $this, 'print_enqueued_script_modules' ) );
add_action( $position, array( $this, 'print_script_module_preloads' ) );
add_action( wp_is_block_theme() ? 'wp_head' : 'wp_footer', array( $this, 'print_import_map' ) );
if ( wp_is_block_theme() ) {
// Modules can only be printed in the head for block themes because only with
// block themes will import map be fully populated by modules discovered by
// rendering the block template. In classic themes, modules are enqueued during
// template rendering, thus the import map must be printed in the footer,
// followed by all enqueued modules.
add_action( 'wp_head', array( $this, 'print_head_enqueued_script_modules' ) );
}
add_action( 'wp_footer', array( $this, 'print_enqueued_script_modules' ) );
add_action( wp_is_block_theme() ? 'wp_head' : 'wp_footer', array( $this, 'print_script_module_preloads' ) );
Copy link

@mindctrl mindctrl Oct 20, 2025

Choose a reason for hiding this comment

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

Can we continue to store the result of wp_is_block_theme() in a variable and reuse it? It instantiates WP_Theme every time it's called.

I also think this pattern is harder to read, and search for, and I don't recall it being used in core anywhere else.

Copy link
Member

Choose a reason for hiding this comment

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

Can we continue to store the result of wp_is_block_theme() in a variable and reuse it? It instantiates WP_Theme every time it's called.

Good idea to store in a variable.

I also think this pattern is harder to read, and search for, and I don't recall it being used in core anywhere else.

@mindctrl What pattern?

Copy link

@mindctrl mindctrl Oct 20, 2025

Choose a reason for hiding this comment

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

@westonruter the pattern of putting a conditional inside the first arg passed to add_action like this:
add_action( wp_is_block_theme() ? 'wp_head' : 'wp_footer', ... )

Copy link
Member

Choose a reason for hiding this comment

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

Changes applied in b652fdd and de83bd2


add_action( 'admin_print_footer_scripts', array( $this, 'print_import_map' ) );
add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) );
Expand Down Expand Up @@ -311,31 +331,71 @@ private function get_highest_fetchpriority( array $ids ): string {
}

/**
* Prints the enqueued script modules using script tags with type="module"
* attributes.
* Prints the enqueued script modules in head.
*
* @since 6.9.0
*/
public function print_head_enqueued_script_modules() {
$script_modules = $this->get_marked_for_enqueue();
$sorted_script_modules_ids = $this->get_sorted_dependencies( array_keys( $script_modules ) );
foreach ( $sorted_script_modules_ids as $id ) {
if ( isset( $script_modules[ $id ] ) && ! $script_modules[ $id ]['in_footer'] ) {
// If any dependency is set to be printed in footer, skip printing this module in head.
$dependencies = $this->get_dependencies( array( $id ) );
foreach ( $dependencies as $dependency_id => $dependency ) {
if ( in_array( $dependency_id, $this->queue, true ) && $dependency['in_footer'] ) {
continue 2;
}
}
$this->done[] = $id;
$this->print_script_module( $id, $script_modules[ $id ] );
}
}
}

/**
* Prints the enqueued script modules in footer.
*
* @since 6.5.0
*/
public function print_enqueued_script_modules() {
foreach ( $this->get_marked_for_enqueue() as $id => $script_module ) {
$args = array(
'type' => 'module',
'src' => $this->get_src( $id ),
'id' => $id . '-js-module',
);

$dependents = $this->get_recursive_dependents( $id );
$fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $dependents ) );
if ( 'auto' !== $fetchpriority ) {
$args['fetchpriority'] = $fetchpriority;
}
if ( $fetchpriority !== $script_module['fetchpriority'] ) {
$args['data-wp-fetchpriority'] = $script_module['fetchpriority'];
$script_modules = $this->get_marked_for_enqueue();
$sorted_script_modules_ids = $this->get_sorted_dependencies( array_keys( $script_modules ) );
foreach ( $sorted_script_modules_ids as $id ) {
if ( isset( $script_modules[ $id ] ) ) {
$this->done[] = $id;
$this->print_script_module( $id, $script_modules[ $id ] );
}
wp_print_script_tag( $args );
}
}

/**
* Prints the enqueued script module using script tags with type="module"
* attributes.
*
* @since 6.9.0
*
* @param string $id The script module identifier.
* @param array $module The script module to print.
*/
private function print_script_module( string $id, array $script_module ) {
$args = array(
'type' => 'module',
'src' => $this->get_src( $id ),
'id' => $id . '-js-module',
);

$dependents = $this->get_recursive_dependents( $id );
$fetchpriority = $this->get_highest_fetchpriority( array_merge( array( $id ), $dependents ) );
if ( 'auto' !== $fetchpriority ) {
$args['fetchpriority'] = $fetchpriority;
}
if ( $fetchpriority !== $script_module['fetchpriority'] ) {
$args['data-wp-fetchpriority'] = $script_module['fetchpriority'];
}
wp_print_script_tag( $args );
}

/**
* Prints the static dependencies of the enqueued script modules using
* link tags with rel="modulepreload" attributes.
Expand Down Expand Up @@ -520,6 +580,52 @@ private function get_recursive_dependents( string $id ): array {
return array_unique( $get( $id ) );
}

/**
* Sorts the given script module identifiers based on their dependencies.
*
* It will return a list of script module identifiers sorted in the order
* they should be printed, so that dependencies are printed before the script
* modules that depend on them.
*
* @since 6.9.0
*
* @param string[] $ids The identifiers of the script modules to sort.
* @return string[] Sorted list of script module identifiers.
*/
private function get_sorted_dependencies( array $ids ): array {
$sorted = array();
$sorter = function ( array $ids, bool $recursion = false ) use ( &$sorter, &$sorted ) {
foreach ( $ids as $id ) {
if ( in_array( $id, $this->done, true ) || in_array( $id, $sorted, true ) ) { // Already done.
continue;
}

$keep_going = true;
if ( ! isset( $this->registered[ $id ] ) ) {
$keep_going = false; // Item doesn't exist.
} elseif ( array_diff( array_column( $this->registered[ $id ]['dependencies'], 'id' ), array_keys( $this->registered ) ) ) {
$keep_going = false; // Item requires dependencies that don't exist.
} elseif ( ! $sorter( array_column( $this->registered[ $id ]['dependencies'], 'id' ), true ) ) {
$keep_going = false; // Item requires dependencies that don't exist.
}

if ( ! $keep_going ) { // Either item or its dependencies don't exist.
if ( $recursion ) {
return false; // Abort this branch.
} else {
continue; // We're at the top level. Move on to the next one.
}
}

$sorted[] = $id;
}
return true;
};
$sorter( $ids );

return array_unique( $sorted );
}

/**
* Gets the versioned URL for a script module src.
*
Expand Down
2 changes: 2 additions & 0 deletions src/wp-includes/script-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function wp_script_modules(): WP_Script_Modules {
* @param array $args {
* Optional. An array of additional args. Default empty array.
*
* @type bool $in_footer Whether to print the script module in the footer. Default 'false'. Optional.
* @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
* }
*/
Expand Down Expand Up @@ -107,6 +108,7 @@ function wp_register_script_module( string $id, string $src, array $deps = array
* @param array $args {
* Optional. An array of additional args. Default empty array.
*
* @type bool $in_footer Whether to print the script module in the footer. Default 'false'. Optional.
* @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
* }
*/
Expand Down
Loading
Loading