diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php
index a6a283f953..116e98f673 100644
--- a/src/wp-includes/class-wp-scripts.php
+++ b/src/wp-includes/class-wp-scripts.php
@@ -231,6 +231,25 @@ class WP_Scripts extends WP_Dependencies {
return true;
}
+ /**
+ * Checks whether all dependents of a given handle are in the footer.
+ *
+ * If there are no dependents, this is considered the same as if all dependents were in the footer.
+ *
+ * @since 6.4.0
+ *
+ * @param string $handle Script handle.
+ * @return bool Whether all dependents are in the footer.
+ */
+ private function are_all_dependents_in_footer( $handle ) {
+ foreach ( $this->get_dependents( $handle ) as $dep ) {
+ if ( isset( $this->groups[ $dep ] ) && 0 === $this->groups[ $dep ] ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Processes a script dependency.
*
@@ -281,6 +300,25 @@ class WP_Scripts extends WP_Dependencies {
$intended_strategy = '';
}
+ /*
+ * Move this script to the footer if:
+ * 1. The script is in the header group.
+ * 2. The current output is the header.
+ * 3. The intended strategy is delayed.
+ * 4. The actual strategy is not delayed.
+ * 5. All dependent scripts are in the footer.
+ */
+ if (
+ 0 === $group &&
+ 0 === $this->groups[ $handle ] &&
+ $intended_strategy &&
+ ! $this->is_delayed_strategy( $strategy ) &&
+ $this->are_all_dependents_in_footer( $handle )
+ ) {
+ $this->in_footer[] = $handle;
+ return false;
+ }
+
if ( $conditional ) {
$cond_before = "\n";
diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php
index 7f2b956127..95cf958d3e 100644
--- a/tests/phpunit/tests/dependencies/scripts.php
+++ b/tests/phpunit/tests/dependencies/scripts.php
@@ -3065,4 +3065,291 @@ HTML
protected function add_html5_script_theme_support() {
add_theme_support( 'html5', array( 'script' ) );
}
+
+ /**
+ * Test that a script is moved to the footer if it is made non-deferrable, was in the header and
+ * all scripts that depend on it are in the footer.
+ *
+ * @ticket 58599
+ *
+ * @dataProvider data_provider_script_move_to_footer
+ *
+ * @param callable $set_up Test setup.
+ * @param string $expected_header Expected output for header.
+ * @param string $expected_footer Expected output for footer.
+ * @param string[] $expected_in_footer Handles expected to be in the footer.
+ * @param array $expected_groups Expected groups.
+ */
+ public function test_wp_scripts_move_to_footer( $set_up, $expected_header, $expected_footer, $expected_in_footer, $expected_groups ) {
+ $set_up();
+
+ // Get the header output.
+ ob_start();
+ wp_scripts()->do_head_items();
+ $header = ob_get_clean();
+
+ // Print a script in the body just to make sure it doesn't cause problems.
+ ob_start();
+ wp_print_scripts( array( 'jquery' ) );
+ ob_end_clean();
+
+ // Get the footer output.
+ ob_start();
+ wp_scripts()->do_footer_items();
+ $footer = ob_get_clean();
+
+ $this->assertEqualMarkup( $expected_header, $header, 'Expected header script markup to match.' );
+ $this->assertEqualMarkup( $expected_footer, $footer, 'Expected footer script markup to match.' );
+ $this->assertEqualSets( $expected_in_footer, wp_scripts()->in_footer, 'Expected to have the same handles for in_footer.' );
+ $this->assertEquals( $expected_groups, wp_scripts()->groups, 'Expected groups to match.' );
+ }
+
+ /**
+ * Data provider for test_wp_scripts_move_to_footer.
+ *
+ * @return array[]
+ */
+ public function data_provider_script_move_to_footer() {
+ return array(
+ 'footer-blocking-dependent-of-defer-head-script' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'script-b', 'https://example.com/script-b.js', array( 'script-a' ), null, array( 'in_footer' => true ) );
+ },
+ 'expected_header' => '',
+ 'expected_footer' => '
+
+
+ ',
+ 'expected_in_footer' => array(
+ 'script-a',
+ 'script-b',
+ ),
+ 'expected_groups' => array(
+ 'script-a' => 0,
+ 'script-b' => 1,
+ 'jquery' => 0,
+ ),
+ ),
+
+ 'footer-blocking-dependent-of-async-head-script' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'async' ) );
+ wp_enqueue_script( 'script-b', 'https://example.com/script-b.js', array( 'script-a' ), null, array( 'in_footer' => true ) );
+ },
+ 'expected_header' => '',
+ 'expected_footer' => '
+
+
+ ',
+ 'expected_in_footer' => array(
+ 'script-a',
+ 'script-b',
+ ),
+ 'expected_groups' => array(
+ 'script-a' => 0,
+ 'script-b' => 1,
+ 'jquery' => 0,
+ ),
+ ),
+
+ 'head-blocking-dependent-of-delayed-head-script' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script( 'script-b', 'https://example.com/script-b.js', array( 'script-a' ), null, array( 'in_footer' => false ) );
+ },
+ 'expected_header' => '
+
+
+ ',
+ 'expected_footer' => '',
+ 'expected_in_footer' => array(),
+ 'expected_groups' => array(
+ 'script-a' => 0,
+ 'script-b' => 0,
+ 'jquery' => 0,
+ ),
+ ),
+
+ 'delayed-footer-dependent-of-delayed-head-script' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script(
+ 'script-b',
+ 'https://example.com/script-b.js',
+ array( 'script-a' ),
+ null,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+ },
+ 'expected_header' => '
+
+ ',
+ 'expected_footer' => '
+
+ ',
+ 'expected_in_footer' => array(
+ 'script-b',
+ ),
+ 'expected_groups' => array(
+ 'script-a' => 0,
+ 'script-b' => 1,
+ 'jquery' => 0,
+ ),
+ ),
+
+ 'delayed-dependent-in-header-and-delayed-dependents-in-footer' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script(
+ 'script-b',
+ 'https://example.com/script-b.js',
+ array( 'script-a' ),
+ null,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => false,
+ )
+ );
+ wp_enqueue_script(
+ 'script-c',
+ 'https://example.com/script-c.js',
+ array( 'script-a' ),
+ null,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+ wp_enqueue_script(
+ 'script-d',
+ 'https://example.com/script-d.js',
+ array( 'script-a' ),
+ null,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+ },
+ 'expected_header' => '
+
+
+ ',
+ 'expected_footer' => '
+
+
+ ',
+ 'expected_in_footer' => array(
+ 'script-c',
+ 'script-d',
+ ),
+ 'expected_groups' => array(
+ 'script-a' => 0,
+ 'script-b' => 0,
+ 'script-c' => 1,
+ 'script-d' => 1,
+ 'jquery' => 0,
+ ),
+ ),
+
+ 'all-dependents-in-footer-with-one-blocking' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script(
+ 'script-b',
+ 'https://example.com/script-b.js',
+ array( 'script-a' ),
+ null,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+ wp_enqueue_script( 'script-c', 'https://example.com/script-c.js', array( 'script-a' ), null, true );
+ wp_enqueue_script(
+ 'script-d',
+ 'https://example.com/script-d.js',
+ array( 'script-a' ),
+ null,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+ },
+ 'expected_header' => '',
+ 'expected_footer' => '
+
+
+
+
+ ',
+ 'expected_in_footer' => array(
+ 'script-a',
+ 'script-b',
+ 'script-c',
+ 'script-d',
+ ),
+ 'expected_groups' => array(
+ 'script-a' => 0,
+ 'script-b' => 1,
+ 'script-c' => 1,
+ 'script-d' => 1,
+ 'jquery' => 0,
+
+ ),
+ ),
+
+ 'blocking-dependents-in-head-and-footer' => array(
+ 'set_up' => static function () {
+ wp_enqueue_script( 'script-a', 'https://example.com/script-a.js', array(), null, array( 'strategy' => 'defer' ) );
+ wp_enqueue_script(
+ 'script-b',
+ 'https://example.com/script-b.js',
+ array( 'script-a' ),
+ null,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => false,
+ )
+ );
+ wp_enqueue_script( 'script-c', 'https://example.com/script-c.js', array( 'script-a' ), null, true );
+ wp_enqueue_script(
+ 'script-d',
+ 'https://example.com/script-d.js',
+ array( 'script-a' ),
+ null,
+ array(
+ 'strategy' => 'defer',
+ 'in_footer' => true,
+ )
+ );
+ },
+ 'expected_header' => '
+
+
+ ',
+ 'expected_footer' => '
+
+
+ ',
+ 'expected_in_footer' => array(
+ 'script-c',
+ 'script-d',
+ ),
+ 'expected_groups' => array(
+ 'script-a' => 0,
+ 'script-b' => 0,
+ 'script-c' => 1,
+ 'script-d' => 1,
+ 'jquery' => 0,
+ ),
+ ),
+
+ );
+ }
}