PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.6
1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.2.0 All 28 releases
xspeed / includes / modules / Minify / MinifyModule.php

MinifyModule.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.1.6, at includes/modules/Minify/MinifyModule.php

281 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Minify module.
4 *
5 * Owns the minify_html / minify_css / minify_js settings. Engine work
6 * still happens in XSpeed\Minifier (filters style_loader_src and
7 * script_loader_src), but the Module is now the storage and editing
8 * authority. Settings live in xspeed_module_minify; the legacy
9 * xspeed_options blob is drained on first boot and on POST.
10 *
11 * Tier: Free. See SETTINGS.md for the contract this module satisfies.
12 *
13 * @package XSpeed
14 */
15
16 declare(strict_types=1);
17
18 namespace XSpeed\Modules\Minify;
19
20 defined( 'ABSPATH' ) || exit;
21
22 use XSpeed\Minifier as LegacyMinifier;
23 use XSpeed\Module;
24 use XSpeed\Settings_Manager;
25
26 final class MinifyModule extends Module {
27
28 public const SLUG = 'minify';
29 public const TIER = self::TIER_FREE;
30 public const VERSION = '1.2.0';
31
32 public function ui_metadata(): array {
33 return array(
34 'label' => 'CSS & JavaScript',
35 'tab_label' => 'Minify', // its own tab on the CSS & JavaScript page
36 'icon' => 'Wand2',
37 'description' => 'Strip whitespace and rewrite enqueued CSS / JS.',
38 // Host page: Minify (this module) / Critical CSS (Pro) / Unused
39 // CSS (Pro) as tabs — the three CSS/JS optimizations live on one
40 // page instead of three sidebar rows (FBS-83633).
41 'custom_panel' => 'CssJsPanel',
42 );
43 }
44
45 public function settings_schema(): array {
46 return array(
47 'minify_html' => array(
48 'type' => 'bool',
49 'default' => false,
50 'label' => 'Minify HTML',
51 'description' => 'Strip whitespace and comments from HTML output. Safe on most themes.',
52 ),
53 'minify_css' => array(
54 'type' => 'bool',
55 'default' => false,
56 'label' => 'Minify CSS',
57 'description' => 'Compress and rewrite enqueued local stylesheets. External CSS is left untouched.',
58 ),
59 'minify_js' => array(
60 'type' => 'bool',
61 'default' => false,
62 'label' => 'Minify JavaScript',
63 'description' => 'Compress enqueued local scripts. Disable if you hit script-loading conflicts on the frontend.',
64 ),
65 'defer_js' => array(
66 'type' => 'bool',
67 'default' => false,
68 'label' => 'Defer JavaScript',
69 'description' => 'Add defer="defer" to enqueued script tags so they execute after HTML parsing. jQuery + its hard dependencies are skipped automatically.',
70 ),
71 'delay_js' => array(
72 'type' => 'bool',
73 'default' => false,
74 'label' => 'Delay JavaScript Until Interaction',
75 'description' => 'Postpone script loading until the visitor scrolls, moves the mouse, taps, or presses a key. Drastically improves first paint on script-heavy pages; can break above-the-fold scripted UI — test before leaving on.',
76 ),
77 'async_css' => array(
78 'type' => 'bool',
79 'default' => false,
80 'label' => 'Load CSS Asynchronously',
81 'description' => 'Rewrite stylesheet link tags to load non-blocking via the print-then-all pattern. Pairs well with critical-CSS workflows; can cause a flash of unstyled content if the theme has no critical CSS.',
82 ),
83 'remove_query_strings' => array(
84 'type' => 'bool',
85 'default' => false,
86 'label' => 'Remove Asset Query Strings',
87 'description' => 'Strip ?ver=X.Y from enqueued CSS / JS URLs. Some CDN caches and proxies cache better when query strings are absent.',
88 ),
89 'defer_js_excluded' => array(
90 'type' => 'list',
91 'default' => array( 'jquery-core', 'jquery-migrate' ),
92 'item_type' => 'string',
93 'label' => 'Defer / Delay Exclusions',
94 'description' => 'Script handles OR URL substrings that skip defer + delay. Defaults exclude jQuery (most themes depend on it being available synchronously). One per line.',
95 // Only relevant once defer OR delay is on — the exclusion list
96 // governs both. Uses the `any` (OR) dependency form. (FBS-82227)
97 'dependsOn' => array(
98 'any' => array(
99 array( 'field' => 'defer_js' ),
100 array( 'field' => 'delay_js' ),
101 ),
102 ),
103 ),
104 'delay_js_targets' => array(
105 'type' => 'list',
106 'default' => array(),
107 'item_type' => 'string',
108 'label' => 'Delay Only These Scripts',
109 'description' => 'Script handles OR URL substrings. When non-empty, ONLY matching scripts are delayed — everything else loads normally. Leave empty to delay all scripts (minus the exclusions above). Ideal for postponing one heavy third-party embed without touching the rest of the page. Handles are the more reliable selector — a URL substring has to match the script\'s original URL, and minification rewrites that to a hashed cache path. One per line.',
110 'dependsOn' => array( 'field' => 'delay_js' ),
111 ),
112 'delay_js_timeout' => array(
113 'type' => 'int',
114 'default' => 8000,
115 'min' => 0,
116 'max' => 60000,
117 'label' => 'Delay Failsafe Timeout (ms)',
118 'unit' => 'ms',
119 'description' => 'Load delayed scripts automatically after this many milliseconds when the visitor never interacts. Set to 0 for interaction-only, with no timer: a timer that fires inside a lab tool\'s measurement window loads the "delayed" scripts anyway and inflates the reported TTI. Keep a non-zero value if a delayed script must eventually run for visitors who never scroll, tap, or type.',
120 'dependsOn' => array( 'field' => 'delay_js' ),
121 ),
122 'combine_css' => array(
123 'type' => 'bool',
124 'default' => false,
125 'label' => 'Combine CSS Files',
126 'description' => 'Concatenate enqueued local stylesheets into a single file (with @import and url(…) paths resolved). External CSS is left alone. Pairs poorly with HTTP/2 push — only enable on HTTP/1.1 hosts.',
127 ),
128 'combine_js' => array(
129 'type' => 'bool',
130 'default' => false,
131 'label' => 'Combine JavaScript Files',
132 'description' => 'Concatenate enqueued local scripts into a single file. External scripts + scripts marked async / deferred are left alone. Disable if you hit dependency-order issues; the combiner respects WordPress enqueue order but inline scripts attached via wp_add_inline_script can shift behavior.',
133 ),
134 );
135 }
136
137 /**
138 * 1.1.0: drain minify_html / minify_css / minify_js from the legacy
139 * `xspeed_options` blob into this module's per-module option, then
140 * delete the keys from the legacy blob so duplicate sources can't
141 * re-appear. Idempotent — re-running is a no-op once the keys are
142 * gone from xspeed_options.
143 */
144 public function migrations(): array {
145 return array(
146 '1.1.0' => static function ( array $opts ): array {
147 $legacy = get_option( 'xspeed_options', array() );
148 if ( ! is_array( $legacy ) ) {
149 return $opts;
150 }
151 $dirty = false;
152 foreach ( array( 'minify_html', 'minify_css', 'minify_js' ) as $key ) {
153 if ( array_key_exists( $key, $legacy ) ) {
154 $opts[ $key ] = (bool) $legacy[ $key ];
155 unset( $legacy[ $key ] );
156 $dirty = true;
157 }
158 }
159 if ( $dirty ) {
160 update_option( 'xspeed_options', $legacy );
161 }
162 return $opts;
163 },
164 );
165 }
166
167 /**
168 * Conflict declarations. Detected automatically by Conflict_Registry,
169 * but listing them here keeps the module self-documenting.
170 */
171 public function conflicts(): array {
172 return array(
173 array(
174 'plugin' => 'autoptimize/autoptimize.php',
175 'feature' => 'minify.html',
176 'strategy' => \XSpeed\Conflict_Registry::STRATEGY_REFUSE,
177 'reason' => 'Autoptimize is active and handles minification.',
178 ),
179 array(
180 'plugin' => 'wp-rocket/wp-rocket.php',
181 'feature' => 'minify.html',
182 'strategy' => \XSpeed\Conflict_Registry::STRATEGY_REFUSE,
183 'reason' => 'WP Rocket already handles minification.',
184 ),
185 );
186 }
187
188 public function cli_commands(): array {
189 return array(
190 array(
191 'name' => 'xspeed minify',
192 'callback' => array( $this, 'cli_handler' ),
193 'shortdesc' => 'Inspect or purge xSpeed minify cache.',
194 'synopsis' => array(
195 array(
196 'type' => 'positional',
197 'name' => 'action',
198 'options' => array( 'status', 'purge' ),
199 'optional' => false,
200 ),
201 ),
202 ),
203 );
204 }
205
206 /**
207 * On boot:
208 * 1. Seed our per-module option from the legacy blob if neither
209 * our option nor the migration has run yet (covers the
210 * already-installed-before-this-module-shipped path).
211 * 2. Instantiate the v1 Minifier engine; it now reads from
212 * Settings_Manager::get('minify') via its updated read path.
213 */
214 public function boot(): void {
215 $this->seed_from_legacy_if_needed();
216 new LegacyMinifier();
217 }
218
219 public function activate(): void {
220 // Plugin activation hits all modules. Same seed logic — safe to
221 // run more than once.
222 $this->seed_from_legacy_if_needed();
223 }
224
225 private function seed_from_legacy_if_needed(): void {
226 $existing = get_option( 'xspeed_module_minify', null );
227 if ( null !== $existing ) {
228 return;
229 }
230 $legacy = get_option( 'xspeed_options', array() );
231 if ( ! is_array( $legacy ) ) {
232 return;
233 }
234 $seed = array( '_version' => self::VERSION );
235 $dirty = false;
236 foreach ( array( 'minify_html', 'minify_css', 'minify_js' ) as $key ) {
237 if ( array_key_exists( $key, $legacy ) ) {
238 $seed[ $key ] = (bool) $legacy[ $key ];
239 unset( $legacy[ $key ] );
240 $dirty = true;
241 }
242 }
243 if ( $dirty ) {
244 update_option( 'xspeed_module_minify', $seed );
245 update_option( 'xspeed_options', $legacy );
246 }
247 }
248
249 public function cli_handler( array $args, array $assoc ): void {
250 $action = $args[0] ?? 'status';
251
252 if ( 'status' === $action ) {
253 $opts = Settings_Manager::get( self::SLUG );
254 // A bare "on" is a lie when the skip guard is active: the
255 // setting is stored, but Minifier::minify_html() returns its
256 // input untouched and the delivered HTML is unchanged. Say so
257 // on the same line, so the contradiction can never be read as
258 // "minify is broken".
259 $skip = \XSpeed\Minifier::skip_reason();
260 $html_state = $opts['minify_html'] ? 'on' : 'off';
261 if ( $opts['minify_html'] && '' !== $skip ) {
262 $html_state .= ( 'wp_debug' === $skip )
263 ? ' (NOT APPLIED — WP_DEBUG is enabled; set WP_DEBUG to false to minify HTML)'
264 : ' (NOT APPLIED — suppressed by the xspeed_skip_minify filter)';
265 }
266 \WP_CLI::log( sprintf( 'minify_html: %s', $html_state ) );
267 \WP_CLI::log( sprintf( 'minify_css : %s', $opts['minify_css'] ? 'on' : 'off' ) );
268 \WP_CLI::log( sprintf( 'minify_js : %s', $opts['minify_js'] ? 'on' : 'off' ) );
269 return;
270 }
271
272 if ( 'purge' === $action ) {
273 LegacyMinifier::purge_minified();
274 \WP_CLI::success( 'Minify cache purged.' );
275 return;
276 }
277
278 \WP_CLI::error( "Unknown action: $action" );
279 }
280 }
281