diff --git a/wp-includes/class-wp-scripts.php b/wp-includes/class-wp-scripts.php index ddaa270c6d..857f1e948b 100644 --- a/wp-includes/class-wp-scripts.php +++ b/wp-includes/class-wp-scripts.php @@ -133,6 +133,24 @@ class WP_Scripts extends WP_Dependencies { */ private $type_attr = ''; + /** + * Holds a mapping of dependents (as handles) for a given script handle. + * Used to optimize recursive dependency tree checks. + * + * @since 6.3.0 + * @var array + */ + private $dependents_map = array(); + + /** + * Holds a reference to the delayed (non-blocking) script loading strategies. + * Used by methods that validate loading strategies. + * + * @since 6.3.0 + * @var string[] + */ + private $delayed_strategies = array( 'defer', 'async' ); + /** * Constructor. * @@ -284,29 +302,27 @@ class WP_Scripts extends WP_Dependencies { $ver = $ver ? $ver . '&' . $this->args[ $handle ] : $this->args[ $handle ]; } - $src = $obj->src; - $cond_before = ''; - $cond_after = ''; - $conditional = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : ''; + $src = $obj->src; + $strategy = $this->get_eligible_loading_strategy( $handle ); + $intended_strategy = (string) $this->get_data( $handle, 'strategy' ); + $cond_before = ''; + $cond_after = ''; + $conditional = isset( $obj->extra['conditional'] ) ? $obj->extra['conditional'] : ''; + + if ( ! $this->is_delayed_strategy( $intended_strategy ) ) { + $intended_strategy = ''; + } if ( $conditional ) { $cond_before = "\n"; } - $before_handle = $this->print_inline_script( $handle, 'before', false ); - $after_handle = $this->print_inline_script( $handle, 'after', false ); + $before_script = $this->get_inline_script_tag( $handle, 'before' ); + $after_script = $this->get_inline_script_tag( $handle, 'after' ); - if ( $before_handle ) { - $before_handle = sprintf( "\n%s\n\n", $this->type_attr, esc_attr( $handle ), $before_handle ); - } - - if ( $after_handle ) { - $after_handle = sprintf( "\n%s\n\n", $this->type_attr, esc_attr( $handle ), $after_handle ); - } - - if ( $before_handle || $after_handle ) { - $inline_script_tag = $cond_before . $before_handle . $after_handle . $cond_after; + if ( $before_script || $after_script ) { + $inline_script_tag = $cond_before . $before_script . $after_script . $cond_after; } else { $inline_script_tag = ''; } @@ -333,7 +349,10 @@ class WP_Scripts extends WP_Dependencies { */ $srce = apply_filters( 'script_loader_src', $src, $handle ); - if ( $this->in_default_dir( $srce ) && ( $before_handle || $after_handle || $translations_stop_concat ) ) { + if ( + $this->in_default_dir( $srce ) + && ( $before_script || $after_script || $translations_stop_concat || $this->is_delayed_strategy( $strategy ) ) + ) { $this->do_concat = false; // Have to print the so-far concatenated scripts right away to maintain the right order. @@ -390,9 +409,16 @@ class WP_Scripts extends WP_Dependencies { return true; } - $tag = $translations . $cond_before . $before_handle; - $tag .= sprintf( "\n", $this->type_attr, $src, esc_attr( $handle ) ); - $tag .= $after_handle . $cond_after; + $tag = $translations . $cond_before . $before_script; + $tag .= sprintf( + "\n", + $this->type_attr, + $src, // Value is escaped above. + esc_attr( $handle ), + $strategy ? " {$strategy}" : '', + $intended_strategy ? " data-wp-strategy='{$intended_strategy}'" : '' + ); + $tag .= $after_script . $cond_after; /** * Filters the HTML script tag of an enqueued script. @@ -445,29 +471,97 @@ class WP_Scripts extends WP_Dependencies { * Prints inline scripts registered for a specific handle. * * @since 4.5.0 + * @deprecated 6.3.0 Use methods get_inline_script_tag() or get_inline_script_data() instead. * - * @param string $handle Name of the script to add the inline script to. + * @param string $handle Name of the script to print inline scripts for. * Must be lowercase. * @param string $position Optional. Whether to add the inline script * before the handle or after. Default 'after'. - * @param bool $display Optional. Whether to print the script - * instead of just returning it. Default true. - * @return string|false Script on success, false otherwise. + * @param bool $display Optional. Whether to print the script tag + * instead of just returning the script data. Default true. + * @return string|false Script data on success, false otherwise. */ public function print_inline_script( $handle, $position = 'after', $display = true ) { - $output = $this->get_data( $handle, $position ); + _deprecated_function( __METHOD__, '6.3.0', 'WP_Scripts::get_inline_script_data() or WP_Scripts::get_inline_script_tag()' ); + $output = $this->get_inline_script_data( $handle, $position ); if ( empty( $output ) ) { return false; } - $output = trim( implode( "\n", $output ), "\n" ); - if ( $display ) { - printf( "\n%s\n\n", $this->type_attr, esc_attr( $handle ), esc_attr( $position ), $output ); + echo $this->get_inline_script_tag( $handle, $position ); + } + return $output; + } + + /** + * 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. + * @param string $position Optional. Whether to add the inline script + * before the handle or after. Default 'after'. + * @return string Inline script, which may be empty string. + */ + public function get_inline_script_data( $handle, $position = 'after' ) { + $data = $this->get_data( $handle, $position ); + if ( empty( $data ) || ! is_array( $data ) ) { + return ''; } - return $output; + 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. + * + * @since 6.3.0 + * + * @param string $handle Name of the script to get associated inline script tag for. + * Must be lowercase. + * @param string $position Optional. Whether to get tag for inline + * scripts in the before or after position. Default 'after'. + * @return string Inline script, which may be empty string. + */ + public function get_inline_script_tag( $handle, $position = 'after' ) { + $js = $this->get_inline_script_data( $handle, $position ); + if ( empty( $js ) ) { + return ''; + } + + $id = "{$handle}-js-{$position}"; + + return wp_get_inline_script_tag( $js, compact( 'id' ) ); } /** @@ -714,6 +808,199 @@ JS; return false; } + /** + * This overrides the add_data method from WP_Dependencies, to support normalizing of $args. + * + * @since 6.3.0 + * + * @param string $handle Name of the item. Should be unique. + * @param string $key The data key. + * @param mixed $value The data value. + * @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( + __METHOD__, + sprintf( + /* translators: 1: $strategy, 2: $handle */ + __( 'Invalid strategy `%1$s` defined for `%2$s` during script registration.' ), + $value, + $handle + ), + '6.3.0' + ); + return false; + } 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 is an alias (it lacks a `src` value).' ), + $value, + $handle + ), + '6.3.0' + ); + return false; + } + } + return parent::add_data( $handle, $key, $value ); + } + + /** + * Gets all dependents of a script. + * + * @since 6.3.0 + * + * @param string $handle The script handle. + * @return string[] Script handles. + */ + private function get_dependents( $handle ) { + // Check if dependents map for the handle in question is present. If so, use it. + if ( isset( $this->dependents_map[ $handle ] ) ) { + return $this->dependents_map[ $handle ]; + } + + $dependents = array(); + + // Iterate over all registered scripts, finding dependents of the script passed to this method. + foreach ( $this->registered as $registered_handle => $args ) { + if ( in_array( $handle, $args->deps, true ) ) { + $dependents[] = $registered_handle; + } + } + + // Add the handles dependents to the map to ease future lookups. + $this->dependents_map[ $handle ] = $dependents; + + return $dependents; + } + + /** + * Checks if the strategy passed is a valid delayed (non-blocking) strategy. + * + * @since 6.3.0 + * + * @param string $strategy The strategy to check. + * @return bool True if $strategy is one of the delayed strategies, otherwise false. + */ + private function is_delayed_strategy( $strategy ) { + return in_array( + $strategy, + $this->delayed_strategies, + true + ); + } + + /** + * Gets the best eligible loading strategy for a script. + * + * @since 6.3.0 + * + * @param string $handle The script handle. + * @return string The best eligible loading strategy. + */ + private function get_eligible_loading_strategy( $handle ) { + $eligible = $this->filter_eligible_strategies( $handle ); + + // Bail early once we know the eligible strategy is blocking. + if ( empty( $eligible ) ) { + return ''; + } + + return in_array( 'async', $eligible, true ) ? 'async' : 'defer'; + } + + /** + * Filter the list of eligible loading strategies for a script. + * + * @since 6.3.0 + * + * @param string $handle The script handle. + * @param string[]|null $eligible Optional. The list of strategies to filter. Default null. + * @param array $checked Optional. An array of already checked script handles, used to avoid recursive loops. + * @return string[] A list of eligible loading strategies that could be used. + */ + private function filter_eligible_strategies( $handle, $eligible = null, $checked = array() ) { + // If no strategies are being passed, all strategies are eligible. + if ( null === $eligible ) { + $eligible = $this->delayed_strategies; + } + + // If this handle was already checked, return early. + if ( isset( $checked[ $handle ] ) ) { + return $eligible; + } + + // Mark this handle as checked. + $checked[ $handle ] = true; + + // If this handle isn't registered, don't filter anything and return. + if ( ! isset( $this->registered[ $handle ] ) ) { + return $eligible; + } + + // If the handle is not enqueued, don't filter anything and return. + if ( ! $this->query( $handle, 'enqueued' ) ) { + return $eligible; + } + + $is_alias = (bool) ! $this->registered[ $handle ]->src; + $intended_strategy = $this->get_data( $handle, 'strategy' ); + + // For non-alias handles, an empty intended strategy filters all strategies. + if ( ! $is_alias && empty( $intended_strategy ) ) { + return array(); + } + + // Handles with inline scripts attached in the 'after' position cannot be delayed. + if ( $this->has_inline_script( $handle, 'after' ) ) { + return array(); + } + + // If the intended strategy is 'defer', filter out 'async'. + if ( 'defer' === $intended_strategy ) { + $eligible = array( 'defer' ); + } + + $dependents = $this->get_dependents( $handle ); + + // Recursively filter eligible strategies for dependents. + foreach ( $dependents as $dependent ) { + // Bail early once we know the eligible strategy is blocking. + if ( empty( $eligible ) ) { + return array(); + } + + $eligible = $this->filter_eligible_strategies( $dependent, $eligible, $checked ); + } + + return $eligible; + } + + /** + * 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. + * @param string $position The position of the inline script. + * @return bool Whether the handle has an inline script (either before or after). + */ + private function has_inline_script( $handle, $position = null ) { + if ( $position && in_array( $position, array( 'before', 'after' ), true ) ) { + return (bool) $this->get_data( $handle, $position ); + } + + return (bool) ( $this->get_data( $handle, 'before' ) || $this->get_data( $handle, 'after' ) ); + } + /** * Resets class properties. * diff --git a/wp-includes/functions.wp-scripts.php b/wp-includes/functions.wp-scripts.php index 64b9368344..aab583d6e3 100644 --- a/wp-includes/functions.wp-scripts.php +++ b/wp-includes/functions.wp-scripts.php @@ -157,6 +157,7 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) { * * @since 2.1.0 * @since 4.3.0 A return value was added. + * @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array. * * @param string $handle Name of the script. Should be unique. * @param string|false $src Full URL of the script, or path of the script relative to the WordPress root directory. @@ -166,20 +167,32 @@ function wp_add_inline_script( $handle, $data, $position = 'after' ) { * 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 bool $in_footer Optional. Whether to enqueue the script before `` instead of in the ``. - * Default 'false'. + * @param array|bool $args { + * Optional. An array of additional script loading strategies. Default empty array. + * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. + * + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * } * @return bool Whether the script has been registered. True on success, false on failure. */ -function wp_register_script( $handle, $src, $deps = array(), $ver = false, $in_footer = false ) { +function wp_register_script( $handle, $src, $deps = array(), $ver = false, $args = array() ) { + if ( ! is_array( $args ) ) { + $args = array( + 'in_footer' => (bool) $args, + ); + } _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); $wp_scripts = wp_scripts(); $registered = $wp_scripts->add( $handle, $src, $deps, $ver ); - if ( $in_footer ) { + if ( ! empty( $args['in_footer'] ) ) { $wp_scripts->add_data( $handle, 'group', 1 ); } - + if ( ! empty( $args['strategy'] ) ) { + $wp_scripts->add_data( $handle, 'strategy', $args['strategy'] ); + } return $registered; } @@ -331,6 +344,7 @@ function wp_deregister_script( $handle ) { * @see WP_Dependencies::enqueue() * * @since 2.1.0 + * @since 6.3.0 The $in_footer parameter of type boolean was overloaded to be an $args parameter of type array. * * @param string $handle Name of the script. Should be unique. * @param string $src Full URL of the script, or path of the script relative to the WordPress root directory. @@ -340,24 +354,36 @@ function wp_deregister_script( $handle ) { * 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 bool $in_footer Optional. Whether to enqueue the script before `` instead of in the ``. - * Default 'false'. + * @param array|bool $args { + * Optional. An array of additional script loading strategies. Default empty array. + * Otherwise, it may be a boolean in which case it determines whether the script is printed in the footer. Default false. + * + * @type string $strategy Optional. If provided, may be either 'defer' or 'async'. + * @type bool $in_footer Optional. Whether to print the script in the footer. Default 'false'. + * } */ -function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $in_footer = false ) { +function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) { _wp_scripts_maybe_doing_it_wrong( __FUNCTION__, $handle ); $wp_scripts = wp_scripts(); - if ( $src || $in_footer ) { + if ( $src || ! empty( $args ) ) { $_handle = explode( '?', $handle ); + if ( ! is_array( $args ) ) { + $args = array( + 'in_footer' => (bool) $args, + ); + } if ( $src ) { $wp_scripts->add( $_handle[0], $src, $deps, $ver ); } - - if ( $in_footer ) { + if ( ! empty( $args['in_footer'] ) ) { $wp_scripts->add_data( $_handle[0], 'group', 1 ); } + if ( ! empty( $args['strategy'] ) ) { + $wp_scripts->add_data( $_handle[0], 'strategy', $args['strategy'] ); + } } $wp_scripts->enqueue( $handle ); diff --git a/wp-includes/version.php b/wp-includes/version.php index 4ecc1b38b7..38985acf6d 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.3-alpha-56032'; +$wp_version = '6.3-alpha-56033'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.