.js path long before * `script_loader_tag` (priority 20/30) runs, so the delay + exclusion * checks only ever see the hashed URL. A user targeting a script by * URL substring — the obvious thing to do, and what the UI invites — * would silently stop matching the moment minification was enabled. * Minifier::rewrite_script() records the original here so those * checks can test both. (FBS field report against 1.1.2) * * @var array */ private static $original_src = array(); /** * Record a script's URL as it was BEFORE minification rewrote it. * Called from Minifier::rewrite_script(). * * @param string $handle Script handle. * @param string $src Original (pre-minify) URL. */ public static function remember_original_src( string $handle, string $src ): void { if ( '' !== $handle && '' !== $src ) { self::$original_src[ $handle ] = $src; } } /** * The pre-minify URL for a handle, or '' when we never rewrote it * (external script, minification off, or a handle we didn't touch). * * @param string $handle Script handle. */ public static function original_src( string $handle ): string { return isset( self::$original_src[ $handle ] ) ? self::$original_src[ $handle ] : ''; } /** * Reset the remembered URLs. Test-only seam. */ public static function reset_original_src(): void { self::$original_src = array(); } /** * Does a user-supplied target match this script? * * A target is either a script handle (exact) or a URL substring. The * URL is checked against BOTH the current src and the pre-minify src, * so a target written against the real asset path keeps working once * minification starts rewriting URLs to hashed cache paths. * * @param string $needle Target from the user's list. * @param string $handle Script handle. * @param string $src Current (possibly rewritten) src. */ private static function target_matches( string $needle, string $handle, string $src ): bool { if ( '' === $needle ) { return false; } if ( $handle === $needle ) { return true; } if ( '' !== $src && false !== stripos( $src, $needle ) ) { return true; } $original = self::original_src( $handle ); return '' !== $original && false !== stripos( $original, $needle ); } /** * Filter: `script_loader_tag` — add defer="defer" to non-excluded * scripts. WordPress passes the full fallback so * users with JS disabled still get styles applied (via media="all"). * * @param string $tag * @param string $handle */ public static function async_style_tag( $tag, $handle ): string { if ( ! is_string( $tag ) || '' === $tag ) { return (string) $tag; } if ( self::skip_in_non_frontend_context() ) { return $tag; } // Only operate on with a media attribute // we can swap. Skip anything custom (preload, etc.) — we don't // want to fight with explicit author intent. if ( false === stripos( $tag, 'rel=\'stylesheet\'' ) && false === stripos( $tag, 'rel="stylesheet"' ) ) { return $tag; } // Avoid double-wrapping. if ( false !== stripos( $tag, 'data-xs-async' ) ) { return $tag; } $async = (string) preg_replace_callback( '#\bmedia\s*=\s*(["\'])([^"\']*)\1#i', static function ( $m ) { $orig = $m[2]; return 'media="print" onload="this.media=\'' . esc_attr( $orig ) . '\'" data-xs-async="' . esc_attr( $orig ) . '"'; }, $tag, 1 ); // If no media= was present (rare), inject one. if ( $async === $tag ) { $async = (string) preg_replace( '#. return $async . ''; } /** * Filter: `style_loader_src` + `script_loader_src` — strip the * ?ver=X.Y query string that WP appends for cache busting. Some * CDNs / reverse proxies cache better when the URL has no query. * * Skip URLs whose query carries non-ver params — those might be * intentional (e.g. a CDN providing per-image transforms). * * @param string $src */ public static function strip_version_query( $src ): string { if ( ! is_string( $src ) || '' === $src ) { return (string) $src; } if ( self::skip_in_non_frontend_context() ) { return $src; } $parts = wp_parse_url( $src ); if ( ! is_array( $parts ) || empty( $parts['query'] ) ) { return $src; } parse_str( $parts['query'], $query ); if ( ! is_array( $query ) ) { return $src; } // Only strip 'ver' — keep anything else the asset URL needs. unset( $query['ver'] ); $new_query = http_build_query( $query ); $new_url = ( $parts['scheme'] ?? 'http' ) . '://' . ( $parts['host'] ?? '' ); if ( isset( $parts['port'] ) ) { $new_url .= ':' . $parts['port']; } $new_url .= $parts['path'] ?? ''; if ( '' !== $new_query ) { $new_url .= '?' . $new_query; } if ( ! empty( $parts['fragment'] ) ) { $new_url .= '#' . $parts['fragment']; } return $new_url; } /** * Defensive context guard for filter callbacks. Mirrors the registration- * time bail in Minifier::__construct() so a late context flip (admin page * render kicked off mid-request, REST_REQUEST set after plugins_loaded, * etc.) doesn't let frontend tag rewrites leak into wp-admin / AJAX / * REST / cron responses. * * Specifically prevents the React admin bundle's