diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index 3a5fa6b3170ce..eaede952cdd01 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -551,6 +551,33 @@ public function get_inline_script_data( $handle, $position = 'after' ) { return trim( implode( "\n", $data ), "\n" ); } + /** + * Gets unaliased dependencies. + * + * An alias is a dependency whose src is false. It is used as a way to bundle multiple dependencies in a single + * handle. This in effect flattens an alias dependency tree. + * + * @since 6.3.0 + * + * @param string[] $deps Dependency handles. + * @return string[] Unaliased handles. + */ + private function get_unaliased_deps( array $deps ) { + $flattened = array(); + foreach ( $deps as $dep ) { + if ( ! isset( $this->registered[ $dep ] ) ) { + continue; + } + + if ( $this->registered[ $dep ]->src ) { + $flattened[] = $dep; + } elseif ( $this->registered[ $dep ]->deps ) { + array_push( $flattened, ...$this->get_unaliased_deps( $this->registered[ $dep ]->deps ) ); + } + } + return $flattened; + } + /** * Gets tags for inline scripts registered for a specific handle. * @@ -568,14 +595,20 @@ public function get_inline_script_tag( $handle, $position = 'after' ) { return ''; } - $id = "{$handle}-js-{$position}"; - $deps = $this->registered[ $handle ]->deps; - + $id = "{$handle}-js-{$position}"; if ( $this->should_delay_inline_script( $handle, $position ) ) { $attributes = array( 'id' => $id, 'type' => 'text/plain', ); + + /* + * Note that any dependency aliases need to be flattened because an alias is a bundle of dependencies + * and their handles won't appear on any specific scripts. Since no script will appear in the DOM for + * an alias, there won't be any way to keep track of when it has loaded. Therefore, we only keep track of + * the aliased dependencies (the leaf nodes of the alias dependency tree as it were). + */ + $deps = $this->get_unaliased_deps( $this->registered[ $handle ]->deps ); if ( $deps ) { $attributes['data-wp-deps'] = implode( ',', $deps ); } @@ -648,7 +681,18 @@ private function should_delay_inline_script( $handle, $position ) { * dependency's after inline script. */ foreach ( $deps as $dep ) { - if ( $this->is_delayed_strategy( $this->get_eligible_loading_strategy( $dep ) ) ) { + if ( ! isset( $this->registered[ $dep ] ) ) { + continue; + } + + // If the dependency is an alias, look at its members. + if ( ! $this->registered[ $dep ]->src ) { + foreach ( $this->get_unaliased_deps( $this->registered[ $dep ]->deps ) as $alias_dep ) { + if ( $this->is_delayed_strategy( $this->get_eligible_loading_strategy( $alias_dep ) ) ) { + return true; + } + } + } elseif ( $this->is_delayed_strategy( $this->get_eligible_loading_strategy( $dep ) ) ) { return true; } } @@ -911,6 +955,10 @@ public function in_default_dir( $src ) { * @return bool True on success, false on failure. */ public function add_data( $handle, $key, $value ) { + if ( ! isset( $this->registered[ $handle ] ) ) { + return false; + } + if ( 'strategy' === $key ) { if ( ! empty( $value ) && ! $this->is_delayed_strategy( $value ) ) { _doing_it_wrong( @@ -924,12 +972,12 @@ public function add_data( $handle, $key, $value ) { '6.3.0' ); return false; - } elseif ( empty( $this->registered[ $handle ]->src ) && $this->is_delayed_strategy( $value ) ) { + } elseif ( ! $this->registered[ $handle ]->src && $this->is_delayed_strategy( $value ) ) { _doing_it_wrong( __METHOD__, sprintf( /* translators: 1: $strategy, 2: $handle */ - __( 'Cannot supply a strategy `%1$s` for script `%2$s` because it does not have a `src` value.' ), + __( 'Cannot supply a strategy `%1$s` for script `%2$s` because it is an alias (it lacks a `src` value).' ), $value, $handle ), @@ -974,7 +1022,7 @@ public function has_delayed_inline_script( array $handles ) { */ private function get_dependents( $handle ) { // Check if dependents map for the handle in question is present. If so, use it. - if ( array_key_exists( $handle, $this->dependents_map ) ) { + if ( isset( $this->dependents_map[ $handle ] ) ) { return $this->dependents_map[ $handle ]; } @@ -1021,38 +1069,44 @@ private function is_delayed_strategy( $strategy ) { */ private function has_only_delayed_dependents( $handle, $async_only = false, $checked = array() ) { // If this node was already checked, this script can be delayed and the branch ends. - if ( array_key_exists( $handle, $checked ) ) { + if ( isset( $checked[ $handle ] ) ) { return true; } $checked[ $handle ] = true; $dependents = $this->get_dependents( $handle ); - // If there are no dependents remaining to consider, the script can be deferred. + // If there are no dependents remaining to consider, the script can be delayed. if ( empty( $dependents ) ) { return true; } // Consider each dependent and check if it is delayed. foreach ( $dependents as $dependent ) { - // If the dependent script has no src (as it represents a script bundle), ignore it for consideration. - if ( empty( $this->registered[ $dependent ]->src ) ) { + if ( ! isset( $this->registered[ $dependent ] ) ) { continue; } - // If the dependency is not enqueued, ignore it for consideration. + // If the dependency is not enqueued, exclude it from consideration. if ( ! $this->query( $dependent, 'enqueued' ) ) { continue; } - // If the dependent script is not using the defer or async strategy, no script in the chain is delayed. - $strategy = $this->get_data( $dependent, 'strategy' ); - if ( $async_only ) { - if ( 'async' !== $strategy ) { + // Handle script alias case (where it has no src). Here, the strategy doesn't matter, but only whether there are inline scripts. + if ( ! $this->registered[ $dependent ]->src ) { + // A script alias cannot be delayed if it has inline scripts since there is no load event. + if ( $this->has_inline_script( $dependent ) ) { + return false; + } + } else { + // If the dependent script is not using the defer or async strategy, no script in the chain is delayed. + $strategy = $this->get_data( $dependent, 'strategy' ); + if ( $async_only ? + 'async' !== $strategy : + ! $this->is_delayed_strategy( $strategy ) + ) { return false; } - } elseif ( ! $this->is_delayed_strategy( $strategy ) ) { - return false; } // Recursively check all dependents. @@ -1082,10 +1136,15 @@ private function get_eligible_loading_strategy( $handle ) { /* * Handle known blocking strategy scenarios. * - An empty strategy is synonymous with blocking. - * - A script bundle (where $src is false) must always be blocking since the after inline script cannot be + * - A script alias (where $src is false) must always be blocking since the after inline script cannot be * delayed as there is no external script tag and thus no load event at which the inline script can be run. */ - if ( empty( $intended_strategy ) || empty( $this->registered[ $handle ]->src ) ) { + if ( empty( $intended_strategy ) ) { + return ''; + } + + // Aliases that have before/after inline scripts can never be delayed since there is no load event. + if ( ! $this->registered[ $handle ]->src && $this->has_inline_script( $handle ) ) { return ''; } @@ -1102,6 +1161,19 @@ private function get_eligible_loading_strategy( $handle ) { return ''; } + /** + * Gets data for inline scripts registered for a specific handle. + * + * @since 6.3.0 + * + * @param string $handle Name of the script to get data for. + * Must be lowercase. + * @return bool Whether the handle has an inline script (either before or after). + */ + private function has_inline_script( $handle ) { + return $this->get_data( $handle, 'before' ) || $this->get_data( $handle, 'after' ); + } + /** * Resets class properties. * diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index 68615fbf50b7c..d6e27c3c63c22 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -633,15 +633,15 @@ public function test_has_only_delayed_dependents( $set_up, $async_only, $expecte } /** - * Enqueue test script with before/after inline scripts. + * Register test script. * * @param string $handle Dependency handle to enqueue. * @param string $strategy Strategy to use for dependency. * @param string[] $deps Dependencies for the script. * @param bool $in_footer Whether to print the script in the footer. */ - protected function enqueue_test_script( $handle, $strategy, $deps = array(), $in_footer = false ) { - wp_enqueue_script( + protected function register_test_script( $handle, $strategy, $deps = array(), $in_footer = false ) { + wp_register_script( $handle, add_query_arg( array( @@ -657,6 +657,19 @@ protected function enqueue_test_script( $handle, $strategy, $deps = array(), $in } } + /** + * Enqueue test script. + * + * @param string $handle Dependency handle to enqueue. + * @param string $strategy Strategy to use for dependency. + * @param string[] $deps Dependencies for the script. + * @param bool $in_footer Whether to print the script in the footer. + */ + protected function enqueue_test_script( $handle, $strategy, $deps = array(), $in_footer = false ) { + $this->register_test_script( $handle, $strategy, $deps, $in_footer ); + wp_enqueue_script( $handle ); + } + /** * Adds test inline script. * @@ -810,7 +823,6 @@ public function data_provider_to_test_various_strategy_dependency_chains() { $handle1 = 'blocking-bundle-of-none'; $handle2 = 'defer-dependent-of-blocking-bundle-of-none'; - // Note that jQuery is registered like this. wp_register_script( $handle1, false, array(), null ); $this->add_test_inline_script( $handle1, 'before' ); $this->add_test_inline_script( $handle1, 'after' ); @@ -831,7 +843,7 @@ public function data_provider_to_test_various_strategy_dependency_chains() { scriptEventLog.push( "defer-dependent-of-blocking-bundle-of-none: before inline" ) - HTML @@ -844,9 +856,9 @@ public function data_provider_to_test_various_strategy_dependency_chains() { $handle3 = 'blocking-bundle-member-two'; $handle4 = 'defer-dependent-of-blocking-bundle-of-two'; - wp_register_script( $handle1, false, array(), null ); - $this->enqueue_test_script( $handle2, 'blocking', array( $handle1 ) ); - $this->enqueue_test_script( $handle3, 'blocking', array( $handle1 ) ); + wp_register_script( $handle1, false, array( $handle2, $handle3 ), null ); + $this->enqueue_test_script( $handle2, 'blocking' ); + $this->enqueue_test_script( $handle3, 'blocking' ); $this->enqueue_test_script( $handle4, 'defer', array( $handle1 ) ); foreach ( array( $handle2, $handle3, $handle4 ) as $handle ) { @@ -873,7 +885,7 @@ public function data_provider_to_test_various_strategy_dependency_chains() { scriptEventLog.push( "defer-dependent-of-blocking-bundle-of-two: before inline" ) - HTML @@ -906,7 +918,7 @@ public function data_provider_to_test_various_strategy_dependency_chains() { scriptEventLog.push( "defer-dependent-of-defer-bundle-of-none: before inline" ) - HTML @@ -1046,6 +1058,74 @@ public function data_provider_to_test_various_strategy_dependency_chains() { +HTML + , + ), + 'jquery-deferred' => array( + 'set_up' => function () { + $wp_scripts = wp_scripts(); + wp_default_scripts( $wp_scripts ); + foreach ( $wp_scripts->registered['jquery']->deps as $jquery_dep ) { + $wp_scripts->registered[ $jquery_dep ]->add_data( 'strategy', 'defer' ); + } + wp_enqueue_script( 'theme-functions', 'https://example.com/theme-functions.js', array( 'jquery' ), null, array( 'strategy' => 'defer' ) ); + }, + 'expected_markup' => << + + +HTML + , + ), + 'nested-aliases' => array( + 'set_up' => function () { + $outer_alias_handle = 'outer-bundle-of-two'; + $inner_alias_handle = 'inner-bundle-of-two'; + + // The outer alias contains a blocking member, as well as a nested alias that contains defer scripts. + wp_register_script( $outer_alias_handle, false, array( $inner_alias_handle, 'outer-bundle-leaf-member' ), null ); + $this->register_test_script( 'outer-bundle-leaf-member', 'blocking', array() ); + + // Inner alias only contains delay scripts. + wp_register_script( $inner_alias_handle, false, array( 'inner-bundle-member-one', 'inner-bundle-member-two' ), null ); + $this->register_test_script( 'inner-bundle-member-one', 'defer', array() ); + $this->register_test_script( 'inner-bundle-member-two', 'defer', array() ); + + $this->enqueue_test_script( 'defer-dependent-of-nested-aliases', 'defer', array( $outer_alias_handle ) ); + $this->add_test_inline_script( 'defer-dependent-of-nested-aliases', 'before' ); + $this->add_test_inline_script( 'defer-dependent-of-nested-aliases', 'after' ); + }, + 'expected_markup' => $this->get_delayed_inline_script_loader_script_tag() . << + + + + + +HTML + , + ), + + 'async-alias-members-with-defer-dependency' => array( + 'set_up' => function () { + $alias_handle = 'async-alias'; + $async_handle1 = 'async1'; + $async_handle2 = 'async2'; + + wp_register_script( $alias_handle, false, array( $async_handle1, $async_handle2 ), null ); + $this->register_test_script( $async_handle1, 'async', array() ); + $this->register_test_script( $async_handle2, 'async', array() ); + + $this->enqueue_test_script( 'defer-dependent-of-async-aliases', 'defer', array( $alias_handle ) ); + }, + 'expected_markup' => << + + HTML , ),