PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 All 37 releases
desktop-mode / includes / render / chromeless-trim.php

chromeless-trim.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.13, at includes/render/chromeless-trim.php

1,086 lines 41.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — chromeless asset trim.
4 *
5 * A window renders a real admin page with the admin bar suppressed:
6 * `show_admin_bar` returns false and `wp_admin_bar_render` is removed
7 * from `in_admin_header` (see `includes/helpers.php`), so `#wpadminbar`
8 * never reaches the DOM inside a chromeless iframe. WordPress and the
9 * host still enqueue the bar's scripts and styles, which then load,
10 * parse and execute against markup that does not exist — once per
11 * window, every window.
12 *
13 * Measured on a live WordPress.com install, one Settings window:
14 *
15 * admin-bar.min.css 20.9 KB
16 * os-admin-bar.js (ours) 17.7 KB
17 * admin-bar.min.js 3.4 KB
18 * wpcom-notes admin-bar-v2.js 3.1 KB (cross-origin)
19 * wpcom-notes admin-bar-v2.css 2.4 KB (cross-origin)
20 * wpcom-admin-bar.js 0.6 KB
21 * notes-common-lite.min.js 0.5 KB (cross-origin)
22 * a8c-faux-inline-help.js 0.2 KB
23 * -------
24 * 48.8 KB + three cross-origin round
25 * trips, for a bar the
26 * window does not draw.
27 *
28 * **Dequeue, never deregister.** A handle that stays registered can
29 * still be pulled in as another script's dependency, which is exactly
30 * the safety property we want: if some third-party script genuinely
31 * depends on `admin-bar`, WordPress resolves it and that script keeps
32 * working. Deregistering would strand the dependent instead. The cost
33 * of that choice is that a dependency-pulled handle survives the trim
34 * — correct behaviour, and the reason this list covers the whole
35 * family rather than core's handle alone.
36 *
37 * Third-party handles ship in the defaults on purpose: they are
38 * admin-bar-only by construction (a masterbar, a notifications panel),
39 * and leaving them queued would drag core's `admin-bar` back in as a
40 * dependency, undoing the trim. Sites that need one of them back can
41 * filter it out.
42 */
43
44 defined( 'ABSPATH' ) || exit;
45
46 /**
47 * Script handles dropped inside chromeless windows.
48 *
49 * @return string[]
50 */
51 function openstation_chromeless_trimmed_scripts() {
52 $handles = array(
53 // Core's admin bar behaviour (hover intent, search, shortcuts).
54 'admin-bar',
55 // OpenStation's own toggle bundle. Also guarded at its enqueue
56 // site in `includes/admin-bar.php`; listed here so the trim is
57 // complete even if a plugin re-enqueues it.
58 'os-admin-bar',
59 // WordPress.com / Jetpack masterbar family.
60 'wpcom-admin-bar',
61 'wpcom-notes-common',
62 'wpcom-notes-admin-bar',
63 'a8c-faux-inline-help',
64 );
65
66 /**
67 * Filters the script handles dequeued inside chromeless windows.
68 *
69 * Everything here is chrome the window never renders. Add a handle
70 * to reclaim its parse/execute cost per window; remove one if your
71 * site genuinely needs it inside a window.
72 *
73 * @param string[] $handles Script handles to dequeue.
74 */
75 return (array) apply_filters( 'openstation_chromeless_trimmed_scripts', $handles );
76 }
77
78 /**
79 * Style handles dropped inside chromeless windows.
80 *
81 * @return string[]
82 */
83 function openstation_chromeless_trimmed_styles() {
84 $handles = array(
85 // Core's admin-bar stylesheet. Dropping it also removes the
86 // source of the 32px `html.wp-toolbar` padding; the
87 // `!important` override in `chromeless.css` stays as the
88 // belt-and-braces half of that pair and must not be removed.
89 'admin-bar',
90 'wpcom-notes-admin-bar',
91 );
92
93 /**
94 * Filters the style handles dequeued inside chromeless windows.
95 *
96 * @param string[] $handles Style handles to dequeue.
97 */
98 return (array) apply_filters( 'openstation_chromeless_trimmed_styles', $handles );
99 }
100
101 /**
102 * Drops chrome-only assets inside chromeless windows.
103 *
104 * Runs at `PHP_INT_MAX` on `admin_enqueue_scripts` so it sees the queue
105 * after every plugin has had its say. No-op outside chromeless
106 * requests — the shell itself draws a real admin bar and must keep all
107 * of this.
108 */
109 function openstation_chromeless_trim_assets() {
110 if ( ! openstation_is_chromeless_request() ) {
111 return;
112 }
113
114 foreach ( openstation_chromeless_trimmed_scripts() as $handle ) {
115 wp_dequeue_script( $handle );
116 }
117 foreach ( openstation_chromeless_trimmed_styles() as $handle ) {
118 wp_dequeue_style( $handle );
119 }
120
121 /**
122 * Fires after OpenStation trims chrome-only assets in a window.
123 *
124 * The point to dequeue anything else that only exists to decorate
125 * admin chrome a window does not draw.
126 */
127 do_action( 'openstation_chromeless_trimmed_assets' );
128 }
129 add_action( 'admin_enqueue_scripts', 'openstation_chromeless_trim_assets', PHP_INT_MAX );
130
131 /**
132 * Drops WordPress's emoji polyfill inside chromeless windows.
133 *
134 * **What this actually is**, because it is easy to overstate: the
135 * inline detection script tests the browser against the newest Unicode
136 * emoji set, and when anything is missing it pulls in
137 * `wp-emoji-release.min.js` (Twemoji, 22 KB) to swap those characters
138 * for images. It is a compatibility polyfill, not dead code — a live
139 * measurement on current Chrome showed the 22 KB file genuinely
140 * loading inside a window, because browsers routinely lag the newest
141 * emoji.
142 *
143 * **Why dropping it in a window is still right.** Core sets the
144 * precedent itself: `wp-admin/edit-form-blocks.php` removes this exact
145 * action on the block-editor screen. The only thing lost inside a
146 * window is that a very new emoji in admin content — a post title, a
147 * comment — renders with the operating system's own glyph (or its
148 * fallback) instead of a Twemoji image. And the shell that hosts these
149 * windows already requires service workers, custom elements, ES2020
150 * and `:has()`; a browser that clears that bar is not one that needs
151 * help drawing emoji at all.
152 *
153 * Removal happens on `admin_init` because `wp_enqueue_emoji_styles`
154 * rides `admin_enqueue_scripts` at the default priority — by the time
155 * the handle trim above runs at `PHP_INT_MAX`, it has already fired.
156 * This is the same hook and the same reasoning as the admin-bar
157 * suppression in `includes/helpers.php`.
158 */
159 function openstation_chromeless_suppress_emoji() {
160 if ( ! openstation_is_chromeless_request() ) {
161 return;
162 }
163
164 /**
165 * Filters whether the emoji polyfill is dropped inside windows.
166 *
167 * Return `false` to keep Twemoji's image replacement for admin
168 * content shown in a window.
169 *
170 * @param bool $trim Defaults to `true`.
171 */
172 if ( ! apply_filters( 'openstation_chromeless_trim_emoji', true ) ) {
173 return;
174 }
175
176 remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
177 remove_action( 'admin_enqueue_scripts', 'wp_enqueue_emoji_styles' );
178 // Retained by Core for back-compat and normally unhooked by
179 // `wp_enqueue_emoji_styles()`; removed here because we just took
180 // that away, and it would otherwise print the styles instead.
181 remove_action( 'admin_print_styles', 'print_emoji_styles' );
182 }
183 add_action( 'admin_init', 'openstation_chromeless_suppress_emoji' );
184
185 /**
186 * The Core command-palette root handles.
187 *
188 * `wp-commands` is the `core/commands` store package; `wp-core-commands`
189 * registers WordPress's baseline command set on top of it. Everything
190 * else in the family reaches the palette *through* one of these two, so
191 * they are both the things to drop and the marker that identifies a
192 * dependent as a palette contributor.
193 *
194 * @return string[]
195 */
196 function openstation_command_palette_root_handles() {
197 /**
198 * Filters the handles treated as command-palette roots.
199 *
200 * A queued script whose dependency closure reaches one of these is
201 * considered a palette contributor and is trimmed inside windows.
202 *
203 * @param string[] $handles Root handles.
204 */
205 return (array) apply_filters(
206 'openstation_command_palette_root_handles',
207 array( 'wp-commands', 'wp-core-commands' )
208 );
209 }
210
211 /**
212 * Whether a handle is one of Core's own bundled packages.
213 *
214 * **This is the line between a palette contributor and a library, and
215 * getting it wrong breaks the block editor.** `wp-block-editor` declares
216 * `wp-commands` directly:
217 *
218 * wp-block-editor => …, wp-blocks, wp-commands, wp-components, …
219 * wp-editor => …, wp-block-editor, wp-commands, …
220 *
221 * It does so because the editor *registers* commands into the palette
222 * store — the dependency runs the opposite way from a palette extension,
223 * which *is* the palette. A closure walk cannot tell those apart, so
224 * without this exclusion the walk convicts the whole block-editor stack
225 * and drops it, taking every plugin's block-registration script with it.
226 * Measured on `customize.php` before this rule existed: the block-widgets
227 * panel lost `wp-block-editor`, and Contact Form 7's and MailPoet's block
228 * scripts went with it.
229 *
230 * Core packages are therefore never *dependents* to be trimmed. They are
231 * libraries, and `WP_Dependencies` already knows how to decide whether
232 * one is needed: if something still queued requires it, it resolves and
233 * stays; if the only thing that wanted it was the palette, it falls out
234 * on its own. The roots themselves are exempt from this rule — they are
235 * the palette, not a library it uses.
236 *
237 * @param WP_Dependencies $dependencies The scripts registry.
238 * @param string $handle Handle to test.
239 * @return bool
240 */
241 function openstation_is_core_package_handle( $dependencies, $handle ) {
242 if ( ! isset( $dependencies->registered[ $handle ] ) ) {
243 return false;
244 }
245
246 // Identify a package by its NAME, not by where it is served from.
247 // The `wp-` prefix is the `@wordpress/*` package convention and is
248 // the only stable signal: the Gutenberg plugin re-registers the
249 // entire family — `wp-block-editor`, `wp-commands`, `wp-core-data`,
250 // `wp-customize-widgets` — from `/wp-content/plugins/gutenberg/
251 // build/scripts/…` via its own `$scripts->add()`. A path test for
252 // `/wp-includes/js/dist/` therefore answers false for every package
253 // on a Gutenberg site, silently retiring this guard exactly where
254 // it matters most, and the walk goes on to convict the whole editor
255 // stack. That is what emptied the Customizer's Widgets panel.
256 //
257 // A plugin that registers a handle under this prefix is treated as
258 // a package too. That direction is safe — the only consequence is
259 // that it is never trimmed.
260 if ( 0 === strpos( $handle, 'wp-' ) ) {
261 return true;
262 }
263
264 $src = $dependencies->registered[ $handle ]->src;
265
266 return ( is_string( $src ) && false !== strpos( $src, '/wp-includes/js/dist/' ) );
267 }
268
269 /**
270 * Whether `$handle`'s dependency closure reaches any of `$roots`.
271 *
272 * `$memo` is passed by reference and shared across every candidate in a
273 * single {@see openstation_command_palette_family()} call, so each node
274 * is decided once per call rather than once per candidate that happens
275 * to sit above it. The Gutenberg graph is dense with shared nodes, and
276 * the walk runs twice per request (dequeue, then print list), so a
277 * per-candidate guard re-walks the same subgraph repeatedly.
278 *
279 * A handle currently being walked is memoized as `null`, which reads as
280 * "not yet known" and breaks a dependency cycle the same way a visited
281 * set would.
282 *
283 * @param WP_Dependencies $dependencies The scripts registry.
284 * @param string $handle Handle to test.
285 * @param string[] $roots Root handles to look for.
286 * @param array $memo Handle => verdict, by reference.
287 * @return bool
288 */
289 function openstation_handle_depends_on( $dependencies, $handle, $roots, &$memo ) {
290 if ( array_key_exists( $handle, $memo ) ) {
291 // `null` means "in progress" — a cycle, which reaches nothing new.
292 return ( true === $memo[ $handle ] );
293 }
294 if ( ! isset( $dependencies->registered[ $handle ] ) ) {
295 $memo[ $handle ] = false;
296 return false;
297 }
298
299 $memo[ $handle ] = null;
300 $result = false;
301
302 foreach ( $dependencies->registered[ $handle ]->deps as $dep ) {
303 if ( in_array( $dep, $roots, true ) ) {
304 $result = true;
305 break;
306 }
307
308 /*
309 * Never route through a Core package. Reaching the palette *via*
310 * `wp-block-editor` says something about the editor, not about
311 * this handle: Contact Form 7's block script declares
312 * `wp-block-editor`, and traversing into it would convict the
313 * block script of being a palette extension. A real palette
314 * extension names the palette in its own chain — Astra's and
315 * WooCommerce's both list `wp-commands` directly.
316 */
317 if ( openstation_is_core_package_handle( $dependencies, $dep ) ) {
318 continue;
319 }
320 if ( openstation_handle_depends_on( $dependencies, $dep, $roots, $memo ) ) {
321 $result = true;
322 break;
323 }
324 }
325
326 $memo[ $handle ] = $result;
327 return $result;
328 }
329
330 /**
331 * Whether a handle carries no file of its own.
332 *
333 * A src-less handle is an aggregator: it exists to group dependencies
334 * and to hang inline data on. **Trimming one can only lose.** There is
335 * no file to stop downloading — the entire saving this module exists to
336 * make is zero — while dropping it discards its inline payload, which
337 * may be the whole reason the handle exists.
338 *
339 * Gutenberg's Connectors screen is the case that taught this:
340 * `options-connectors-wp-admin-prerequisites` is registered with an
341 * empty `src` and the dependency list of its boot module, and carries
342 * the app's bootstrap as an inline script —
343 * `import("@wordpress/boot").then( mod => mod.initSinglePage( … ) )`.
344 * Convicting it printed no bootstrap and rendered a blank page, for a
345 * saving of nothing.
346 *
347 * This is a structural rule, not a guess about intent: whatever a
348 * src-less handle is for, trimming it has no upside.
349 *
350 * @param WP_Dependencies $dependencies The scripts registry.
351 * @param string $handle Handle to test.
352 * @return bool
353 */
354 function openstation_handle_has_no_src( $dependencies, $handle ) {
355 if ( ! isset( $dependencies->registered[ $handle ] ) ) {
356 return false;
357 }
358 $src = $dependencies->registered[ $handle ]->src;
359
360 return ( ! is_string( $src ) || '' === $src );
361 }
362
363 /**
364 * Whether the trim may also drop handles that merely *depend on* the
365 * palette. Defaults to **true**, guarded structurally.
366 *
367 * Dropping the roots alone reclaims nothing while one dependent
368 * survives — `WP_Dependencies::all_deps()` pulls the whole chain back
369 * in on its behalf — so the walk earns its place. What it must never do
370 * is guess.
371 *
372 * **The graph records "needs", not "is."** `wp-block-editor` declares
373 * `wp-commands`; so does Gutenberg's Connectors bootstrap; so does a
374 * genuine palette extension. All three for the same honest reason —
375 * their UI needs the commands *store*. Nothing about the dependency
376 * itself separates the palette from something merely using it, and two
377 * regressions came from pretending otherwise.
378 *
379 * So conviction is fenced by **structural** exclusions rather than by
380 * inference about intent, and each is a statement about what trimming
381 * could possibly save:
382 *
383 * - {@see openstation_is_core_package_handle()} — Core's own packages
384 * are libraries `WP_Dependencies` already knows how to include or
385 * omit, and the palette is a feature *of* the editor, so the
386 * dependency there runs backwards.
387 * - {@see openstation_handle_has_no_src()} — a handle with no file
388 * offers nothing to reclaim, so trimming it is all risk.
389 *
390 * Neither rule names a plugin, and neither reads a handle's name: a
391 * site with a completely different plugin set gets the same treatment,
392 * derived from the graph in front of it.
393 *
394 * A residue remains — a plugin bundle with a real `src` that depends on
395 * the palette and also paints part of its screen would still be
396 * convicted. That is what {@see openstation_command_palette_family()}
397 * is for: a site knows its own handles, and can name the exception the
398 * framework has no way to infer.
399 *
400 * @return bool
401 */
402 function openstation_command_palette_trims_dependents() {
403 /**
404 * Filters whether handles depending on the palette are dropped too.
405 *
406 * Default true. Return false to trim only the palette roots, which
407 * gives up the saving on sites carrying palette extensions and
408 * keeps the whole of it on a site where Core's palette is the only
409 * consumer — the safest setting, and rarely a necessary one.
410 *
411 * @param bool $trim Whether to trim palette dependents.
412 */
413 return (bool) apply_filters( 'openstation_command_palette_trim_dependents', true );
414 }
415
416 /**
417 * The command-palette family present in a given handle list.
418 *
419 * The roots, plus every handle in `$handles` that reaches one of them
420 * and survives the structural exclusions —
421 * {@see openstation_command_palette_trims_dependents()} sets out what
422 * those are and why they are the only fencing this walk is allowed.
423 *
424 * Nothing here reads a handle's name or knows any plugin: the same
425 * rules run against whatever graph the site happens to present.
426 *
427 * @param WP_Dependencies $dependencies The scripts registry.
428 * @param string[] $handles Handles to scan.
429 * @return string[] Handles to drop.
430 */
431 function openstation_command_palette_family( $dependencies, $handles ) {
432 $roots = openstation_command_palette_root_handles();
433 $family = $roots;
434
435 if ( openstation_command_palette_trims_dependents() ) {
436 $memo = array();
437 foreach ( $handles as $handle ) {
438 if ( in_array( $handle, $family, true )
439 || openstation_is_core_package_handle( $dependencies, $handle )
440 || openstation_handle_has_no_src( $dependencies, $handle ) ) {
441 continue;
442 }
443 if ( openstation_handle_depends_on( $dependencies, $handle, $roots, $memo ) ) {
444 $family[] = $handle;
445 }
446 }
447 }
448
449 /**
450 * Filters the command-palette handles dropped inside windows.
451 *
452 * Remove a handle here to keep it inside windows — the escape hatch
453 * for a script that registers commands *and* renders part of its own
454 * admin screen, which the dependency walk cannot tell apart.
455 *
456 * @param string[] $family Handles about to be dropped.
457 * @param string[] $handles The handles that were scanned.
458 */
459 return (array) apply_filters( 'openstation_command_palette_family', $family, $handles );
460 }
461
462 /**
463 * The palette contributors among `$handles` — the family, less the
464 * roots.
465 *
466 * A contributor is a script whose reason to exist is the command
467 * palette: Astra's `command-palette.js`, WooCommerce's
468 * `command-palette.js` / `command-palette-analytics.js`. The roots are
469 * the palette itself, not contributions to it, so they are excluded.
470 *
471 * @param WP_Dependencies $dependencies The scripts registry.
472 * @param string[] $handles Handles to scan.
473 * @return string[] Contributor handles.
474 */
475 function openstation_command_palette_contributors( $dependencies, $handles ) {
476 return array_values(
477 array_diff(
478 openstation_command_palette_family( $dependencies, $handles ),
479 openstation_command_palette_root_handles()
480 )
481 );
482 }
483
484 /**
485 * The plugin or theme directory a handle's `src` lives in.
486 *
487 * @param WP_Dependencies $dependencies The scripts registry.
488 * @param string $handle Handle to resolve.
489 * @return string Directory slug, or '' for core / unregistered handles.
490 */
491 function openstation_command_palette_handle_owner( $dependencies, $handle ) {
492 if ( ! isset( $dependencies->registered[ $handle ] ) ) {
493 return '';
494 }
495 $src = $dependencies->registered[ $handle ]->src;
496 if ( ! is_string( $src ) || '' === $src ) {
497 return '';
498 }
499 if ( preg_match( '#/wp-content/(?:plugins|mu-plugins|themes)/([^/]+)/#', $src, $matches ) ) {
500 return $matches[1];
501 }
502 return '';
503 }
504
505 /**
506 * Whether a contributor's own plugin owns the current admin screen.
507 *
508 * The exemption that lets a plugin keep its palette script *under its
509 * own route*: on its own screen a plugin registers screen-specific
510 * commands, rather than the site-wide ones the shell already carries
511 * once for everybody.
512 *
513 * The default is deliberately conservative, because a false positive
514 * costs that window the whole chain: it matches when the request is for
515 * a file inside the plugin's own directory, or when the `page` query
516 * var is prefixed by the plugin's directory slug — the two shapes a
517 * plugin-owned admin route actually takes. A plugin whose menu slug
518 * resembles nothing in its folder name should claim its route through
519 * the filter rather than by loosening this.
520 *
521 * @param WP_Dependencies $dependencies The scripts registry.
522 * @param string $handle Contributor handle.
523 * @return bool
524 */
525 function openstation_command_palette_owns_screen( $dependencies, $handle ) {
526 $owner = openstation_command_palette_handle_owner( $dependencies, $handle );
527
528 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen routing.
529 $page = isset( $_GET['page'] )
530 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen routing.
531 ? sanitize_text_field( wp_unslash( $_GET['page'] ) )
532 : '';
533 $uri = isset( $_SERVER['REQUEST_URI'] )
534 ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) )
535 : '';
536
537 $owns = false;
538 if ( '' !== $owner ) {
539 if ( false !== strpos( $uri, '/wp-content/plugins/' . $owner . '/' )
540 || false !== strpos( $uri, '/wp-content/themes/' . $owner . '/' ) ) {
541 $owns = true;
542 } elseif ( '' !== $page && 0 === strpos( $page, $owner ) ) {
543 $owns = true;
544 }
545 }
546
547 /**
548 * Filters whether a palette contributor may load in this window.
549 *
550 * Return true to keep the contributor — and, necessarily, the
551 * palette runtime it depends on — inside a window on this screen.
552 *
553 * @param bool $owns Whether the contributor owns this screen.
554 * @param string $handle Contributor handle.
555 * @param string $owner The handle's plugin/theme directory slug.
556 * @param string $page The `page` query var, if any.
557 */
558 return (bool) apply_filters( 'openstation_command_palette_contributor_owns_screen', $owns, $handle, $owner, $page );
559 }
560
561 /**
562 * The palette handles to drop from a window's queue.
563 *
564 * Empty when a contributor owns the current screen: keeping that
565 * contributor means keeping the roots it depends on, so there is
566 * nothing left to drop. That is the deliberate price of the exemption
567 * — the plugin's own admin screen pays for the runtime its commands
568 * need, and no other window does.
569 *
570 * @param WP_Dependencies $dependencies The scripts registry.
571 * @param string[] $handles Handles to scan.
572 * @return string[] Handles to drop; empty when a contributor owns the screen.
573 */
574 function openstation_chromeless_command_palette_drops( $dependencies, $handles ) {
575 foreach ( openstation_command_palette_contributors( $dependencies, $handles ) as $handle ) {
576 if ( openstation_command_palette_owns_screen( $dependencies, $handle ) ) {
577 return array();
578 }
579 }
580
581 return openstation_protect_survivor_dependencies(
582 $dependencies,
583 $handles,
584 openstation_command_palette_family( $dependencies, $handles )
585 );
586 }
587
588 /**
589 * Removes from `$drops` anything a surviving handle still depends on.
590 *
591 * **The safety property the rest of this module rests on**, and the one
592 * that turns a guess about a handle's purpose into a fact about the
593 * page: whatever we believe a handle is *for*, if something that is
594 * still going to print needs it, dropping it strands that thing.
595 *
596 * `WP_Dependencies` walks the graph downwards — it will happily print a
597 * script whose dependency we removed from the list underneath it, and
598 * the browser then throws on the missing global rather than on the
599 * handle we actually meant to drop. That is how a trim aimed at the
600 * command palette emptied the Customizer's Widgets panel:
601 * `customize-widgets` survived, `wp-block-editor` and `wp-commands`
602 * were taken out from under it, and the panel rendered nothing.
603 *
604 * The closure is resolved on a CLONE, so the request's real `$to_do` /
605 * `$done` bookkeeping is untouched — the same technique the palette
606 * manifest builder uses.
607 *
608 * @param WP_Dependencies $dependencies The scripts or styles registry.
609 * @param string[] $handles The full list under consideration.
610 * @param string[] $drops Handles the trim wants to remove.
611 * @return string[] `$drops`, less anything a survivor depends on.
612 */
613 function openstation_protect_survivor_dependencies( $dependencies, $handles, $drops ) {
614 if ( empty( $drops ) ) {
615 return $drops;
616 }
617 $survivors = array_values( array_diff( (array) $handles, (array) $drops ) );
618 if ( empty( $survivors ) ) {
619 return $drops;
620 }
621
622 /*
623 * An explicit closure walk rather than `all_deps()`. The three
624 * reasons — filter re-entry, silent truncation under
625 * `$recursion = true`, and duplicated `_doing_it_wrong()` noise —
626 * are recorded on {@see openstation_script_dependency_closure()},
627 * which is the shared implementation. The one that bites hardest
628 * here: `WP_Styles::all_deps()` / `WP_Scripts::all_deps()` apply
629 * `print_styles_array` / `print_scripts_array` to their result, and
630 * those are the very filters this helper is called from — an
631 * infinite loop that hangs the request.
632 */
633 $needed = openstation_script_dependency_closure( $dependencies, $survivors );
634
635 return array_values( array_diff( $drops, $needed ) );
636 }
637
638 /**
639 * Whether this request should drop the Core command-palette runtime.
640 *
641 * **Why a window never needs it.** ⌘K inside a window belongs to the
642 * shell: the desktop owns the palette, the keystroke is handled in the
643 * parent frame, and the parent only asks a window for its commands when
644 * the palette is actually opened (`os-commands-subscribe`, sent from
645 * `onPaletteOpened()` in `src/commands/iframe-bridge.ts`). The runtime
646 * that answers that request was nevertheless loaded eagerly in every
647 * window, on the chance the user might one day press ⌘K while that
648 * window happened to hold focus.
649 *
650 * What that cost, measured on a live install opening Settings in a
651 * window: 43 files, 10.66 MB raw / 1.94 MB gzipped — react, react-dom,
652 * `components.js` (3.7 MB), `block-editor.js` (3.7 MB), `core-data`,
653 * `blocks`, `sync` — **73.6% of everything the window downloaded**, then
654 * parsed and executed again in each window's own JavaScript realm,
655 * where an HTTP cache hit buys nothing.
656 *
657 * **Block-editor screens are exempt.** `post.php`, `post-new.php`, the
658 * site editor and the widgets screen load that same chain for their own
659 * reasons, so the palette rides along for the cost of `commands.js` +
660 * `core-commands.js` (~150 KB) — and those are exactly the screens whose
661 * stores hold commands worth harvesting ("Duplicate block", pattern
662 * commands). Trimming there would cost real functionality and save
663 * nothing. Everywhere else the store only ever holds the WordPress
664 * baseline, which the shell already publishes itself from its own
665 * lazily-loaded runtime (`src/commands/shell-harvester.ts`).
666 *
667 * @return bool
668 */
669 function openstation_chromeless_should_trim_command_palette() {
670 if ( ! openstation_is_chromeless_request() ) {
671 return false;
672 }
673
674 $trim = ! openstation_chromeless_screen_uses_block_editor();
675
676 /**
677 * Filters whether the Core command-palette runtime is dropped in a
678 * window.
679 *
680 * Return `false` to keep Core's palette — and its Gutenberg
681 * dependency chain — inside windows on this screen.
682 *
683 * @param bool $trim Whether to trim. Defaults to true off block-editor screens.
684 */
685 return (bool) apply_filters( 'openstation_chromeless_trim_command_palette', $trim );
686 }
687
688 /**
689 * Whether the current admin screen renders the block editor.
690 *
691 * `WP_Screen::is_block_editor()` covers `post.php` / `post-new.php`.
692 * The site editor and the block-based widgets screen load the same
693 * runtime without setting that flag, so they are named explicitly.
694 *
695 * **`customize.php` is deliberately not in this list.** It was, on the
696 * assumption that the block-widgets panel made it an editor screen.
697 * Measured, it is not one worth exempting: the Customizer keeps its own
698 * Gutenberg chain either way — that chain has real consumers there, and
699 * they hold it — so the trim removes only the palette and the palette
700 * extensions, and the exemption bought nothing. Verified on a live
701 * install that `wp-block-editor`, `wp-blocks`, `wp-components` and every
702 * plugin block script survive the trim on that screen.
703 *
704 * @return bool
705 */
706 function openstation_chromeless_screen_uses_block_editor() {
707 global $pagenow;
708
709 if ( in_array( $pagenow, array( 'site-editor.php', 'widgets.php' ), true ) ) {
710 return true;
711 }
712 if ( ! function_exists( 'get_current_screen' ) ) {
713 return false;
714 }
715 $screen = get_current_screen();
716
717 return ( $screen instanceof WP_Screen && $screen->is_block_editor() );
718 }
719
720 /**
721 * Keeps Core's boot-time palette enqueue off window pages.
722 *
723 * Removing the callback rather than dequeuing its handles afterwards
724 * also skips the work it does *before* enqueuing anything: a walk of
725 * `$menu` and `$submenu` running `current_user_can()` and
726 * `menu_page_url()` per entry, serialized into a 19.6 KB inline
727 * `wp.coreCommands.initializeCommandPalette( … )` blob — per window.
728 *
729 * Priority 0, ahead of Core's default 10. The shell's own deferral
730 * lives in {@see openstation_defer_core_command_palette()}; this is
731 * the window half of the same idea.
732 */
733 function openstation_chromeless_defer_command_palette() {
734 if ( ! openstation_chromeless_should_trim_command_palette() ) {
735 return;
736 }
737 remove_action( 'admin_enqueue_scripts', 'wp_enqueue_command_palette_assets' );
738 }
739 add_action( 'admin_enqueue_scripts', 'openstation_chromeless_defer_command_palette', 0 );
740
741 /**
742 * Drops the command-palette family inside windows.
743 *
744 * Unhooking Core's enqueue above is necessary but nowhere near
745 * sufficient, and a live measurement showed exactly why: with Core's
746 * palette deferred, a Settings window still pulled 14.28 MB of the
747 * original 14.49 MB, because Astra's `command-palette.js` and
748 * WooCommerce's `command-palette.js` / `command-palette-analytics.js`
749 * each declare `wp-commands` as a dependency and were still queued.
750 * `WP_Dependencies::all_deps()` then pulls the entire chain back in on
751 * their behalf. Dropping the roots alone saves nothing while a single
752 * dependent survives — which is the same lesson the admin-bar trim
753 * above records, and the reason both are family trims.
754 *
755 * Runs at `PHP_INT_MAX` so it sees the queue after every plugin has
756 * had its say. Dequeue, never deregister: a handle that stays
757 * registered can still be resolved as a dependency by something that
758 * genuinely needs it.
759 */
760 function openstation_chromeless_trim_command_palette() {
761 if ( ! openstation_chromeless_should_trim_command_palette() ) {
762 return;
763 }
764
765 $scripts = wp_scripts();
766 if ( ! $scripts ) {
767 return;
768 }
769
770 $drops = openstation_chromeless_command_palette_drops( $scripts, $scripts->queue );
771 foreach ( $drops as $handle ) {
772 wp_dequeue_script( $handle );
773 }
774 if ( ! empty( $drops ) ) {
775 foreach ( openstation_command_palette_root_handles() as $handle ) {
776 wp_dequeue_style( $handle );
777 }
778 }
779
780 openstation_chromeless_keep_components_style( $scripts );
781
782 /**
783 * Fires after OpenStation drops the command-palette family in a window.
784 */
785 do_action( 'openstation_chromeless_trimmed_command_palette' );
786 }
787 add_action( 'admin_enqueue_scripts', 'openstation_chromeless_trim_command_palette', PHP_INT_MAX );
788
789 /**
790 * Keeps the `wp-components` stylesheet for screens that use the components.
791 *
792 * Core's palette enqueues the `wp-commands` stylesheet, which depends on
793 * `wp-components`, on every admin page. Plugins that render with
794 * `@wordpress/components` routinely depend on the `wp-components`
795 * SCRIPT and never declare the matching style, because in the classic
796 * admin the palette always brought it along. Trimming the palette from a
797 * window took that stylesheet away from them.
798 *
799 * What breaks first is anything that floats. A window narrower than
800 * Core's 782px "medium" breakpoint puts every Popover into its full-screen
801 * `is-expanded` mode, which drops the inline positioning and relies on
802 * `.components-popover.is-expanded` from this stylesheet. Without it the
803 * AI plugin's DataViews "View options" popover rendered as a static
804 * block below the page, so the button looked dead. Dropdowns, modals and
805 * snackbars lose their styling the same way.
806 *
807 * So if a queued plugin or theme script reaches the `wp-components`
808 * script, its stylesheet is queued too, which is what the classic admin
809 * effectively guaranteed. Screens that don't render components pay
810 * nothing.
811 *
812 * @param WP_Scripts $scripts The scripts registry.
813 */
814 function openstation_chromeless_keep_components_style( $scripts ) {
815 if ( ! wp_style_is( 'wp-components', 'registered' ) || wp_style_is( 'wp-components', 'enqueued' ) ) {
816 return;
817 }
818
819 // Only screens that render components count. Core packages such as
820 // `wp-abilities` reach `wp-components` as a library on every admin
821 // page, so the walk skips them, the same structural rule the palette
822 // family walk uses ({@see openstation_handle_depends_on()}). Measured:
823 // with it, Settings and the Posts list stay lean while the AI plugin's
824 // logs and WooCommerce's admin app keep the stylesheet. Without it,
825 // every window got it.
826 $uses_components = in_array( 'wp-components', $scripts->queue, true );
827 $memo = array();
828 foreach ( $scripts->queue as $handle ) {
829 if ( $uses_components ) {
830 break;
831 }
832 if ( openstation_is_core_package_handle( $scripts, $handle ) ) {
833 continue;
834 }
835 $uses_components = openstation_handle_depends_on( $scripts, $handle, array( 'wp-components' ), $memo );
836 }
837
838 /**
839 * Filters whether a window keeps the `wp-components` stylesheet after
840 * the command palette is trimmed.
841 *
842 * @param bool $keep Whether to enqueue it. Defaults to whether a queued
843 * plugin or theme script depends on the `wp-components`
844 * script.
845 */
846 if ( apply_filters( 'openstation_chromeless_keep_components_style', $uses_components ) ) {
847 wp_enqueue_style( 'wp-components' );
848 }
849 }
850
851 /**
852 * Second pass: strip the palette family from the actual print list.
853 *
854 * Same two survivors as the named trim below — late enqueues and
855 * dependency pull-back — but the family has to be recomputed here
856 * rather than reused, because a handle enqueued after
857 * `admin_enqueue_scripts` was never in the queue the dequeue pass
858 * scanned. The walk runs against the to-print list, which is the last
859 * word before output.
860 *
861 * @param string[] $handles Script handles about to print.
862 * @return string[]
863 */
864 function openstation_chromeless_filter_palette_print_list( $handles ) {
865 if ( ! is_array( $handles ) || ! openstation_chromeless_should_trim_command_palette() ) {
866 return $handles;
867 }
868 $scripts = wp_scripts();
869 if ( ! $scripts ) {
870 return $handles;
871 }
872 $drops = openstation_chromeless_command_palette_drops( $scripts, $handles );
873
874 return array_values( array_diff( $handles, $drops ) );
875 }
876 add_filter( 'print_scripts_array', 'openstation_chromeless_filter_palette_print_list' );
877
878 /**
879 * Second pass: strip the palette style roots from the print list.
880 *
881 * @param string[] $handles Style handles about to print.
882 * @return string[]
883 */
884 function openstation_chromeless_filter_palette_style_print_list( $handles ) {
885 if ( ! is_array( $handles ) || ! openstation_chromeless_should_trim_command_palette() ) {
886 return $handles;
887 }
888 // A contributor that owns this screen keeps the runtime, and its
889 // stylesheet with it — otherwise the palette it is allowed to show
890 // would render unstyled.
891 $scripts = wp_scripts();
892 if ( $scripts
893 && empty( openstation_chromeless_command_palette_drops( $scripts, $scripts->queue ) ) ) {
894 return $handles;
895 }
896
897 // Styles have their own dependency graph, and a surviving sheet may
898 // sit on top of a palette one — `wp-commands`' stylesheet is a
899 // dependency of others on editor screens. Drop only what nothing
900 // still printing depends on.
901 return array_values(
902 array_diff(
903 $handles,
904 openstation_protect_survivor_dependencies(
905 wp_styles(),
906 $handles,
907 openstation_command_palette_root_handles()
908 )
909 )
910 );
911 }
912 add_filter( 'print_styles_array', 'openstation_chromeless_filter_palette_style_print_list' );
913
914 /**
915 * Second pass: strip trimmed handles from the actual print list.
916 *
917 * The dequeue above cannot be the whole story, and a live install
918 * showed exactly why. Two things survive it:
919 *
920 * 1. **Late enqueues.** A host mu-plugin that queues its masterbar
921 * assets after `admin_enqueue_scripts` has already run is simply
922 * not in the queue yet when we dequeue. WordPress.com's
923 * `wpcom-notes-*` handles behave this way.
924 * 2. **Dependency pull-back.** `WP_Dependencies::all_deps()` pulls a
925 * dequeued-but-registered handle back in when something still
926 * queued declares it as a dependency. Core's `admin-bar`
927 * stylesheet rode back in on the notes stylesheet exactly so.
928 *
929 * `print_scripts_array` / `print_styles_array` run inside `do_items()`
930 * after every enqueue, dequeue and dependency walk has finished, so
931 * they are the last word. Scope stays the same list — a handle nobody
932 * asked us to trim is never touched — and because the list covers the
933 * whole family, removing one member never strands another member that
934 * depended on it.
935 *
936 * @param string[] $handles Handles WordPress is about to print.
937 * @param string $kind 'scripts' or 'styles'.
938 * @return string[] Filtered handles.
939 */
940 function openstation_chromeless_filter_print_list( $handles, $kind ) {
941 if ( ! is_array( $handles ) || ! openstation_is_chromeless_request() ) {
942 return $handles;
943 }
944 $trim = 'scripts' === $kind
945 ? openstation_chromeless_trimmed_scripts()
946 : openstation_chromeless_trimmed_styles();
947
948 return array_values( array_diff( $handles, $trim ) );
949 }
950
951 add_filter(
952 'print_scripts_array',
953 static function ( $handles ) {
954 return openstation_chromeless_filter_print_list( $handles, 'scripts' );
955 }
956 );
957 add_filter(
958 'print_styles_array',
959 static function ( $handles ) {
960 return openstation_chromeless_filter_print_list( $handles, 'styles' );
961 }
962 );
963
964 /**
965 * Hoists the shell's palette contributors into the deferred manifest.
966 *
967 * The shell already defers Core's own palette runtime and replays it on
968 * the first ⌘K. Plugin contributors defeated that completely: they are
969 * enqueued normally, they declare `wp-commands`, and
970 * `WP_Dependencies::all_deps()` therefore pulled the palette chain back
971 * into the boot document on their behalf — the deferral was in place
972 * and paying for nothing.
973 *
974 * Worse, it did not even work. A contributor that ran at boot
975 * registered its commands against a `core/commands` store that does not
976 * exist yet, and lost them. Hoisting fixes the bug and the cost
977 * together: the contributor now executes as part of the replay, *after*
978 * the store exists, so a plugin's commands reach the palette **once, on
979 * the shell**, and cost no window anything.
980 *
981 * Runs at `PHP_INT_MAX` so every plugin has enqueued. Appends to
982 * `openStationConfig.commandPalette.scripts` through a `before` inline
983 * on our own bundle, which prints after the localized config object and
984 * before the bundle that reads it. `src/commands/palette-assets.ts`
985 * de-duplicates by handle, so a dependency the Core manifest already
986 * lists is never executed twice — and re-running `wp-data` would wipe
987 * every store registered against the first copy.
988 */
989 function openstation_shell_hoist_command_palette_contributors() {
990 if ( ! openstation_is_shell_request() ) {
991 return;
992 }
993 $scripts = wp_scripts();
994 if ( ! $scripts || ! wp_script_is( 'openstation', 'enqueued' ) ) {
995 return;
996 }
997
998 // No Core palette on this WordPress, no manifest to hoist into.
999 //
1000 // `openstation_build_command_palette_assets_payload()` returns null
1001 // when `wp_enqueue_command_palette_assets()` does not exist, so
1002 // `openStationConfig.commandPalette` is null and the replay has
1003 // nowhere to put anything. Hoisting anyway would dequeue the
1004 // contributors from the boot page and then drop them on the floor —
1005 // their commands would not merely be late, they would be gone, which
1006 // is strictly worse than the eager loading this replaces. Leave them
1007 // printing normally instead.
1008 if ( ! function_exists( 'wp_enqueue_command_palette_assets' ) ) {
1009 return;
1010 }
1011
1012 $contributors = openstation_command_palette_contributors( $scripts, $scripts->queue );
1013 if ( empty( $contributors ) ) {
1014 return;
1015 }
1016
1017 // Resolve each contributor's ordered chain on a clone, so the
1018 // request's real `$to_do` / `$done` state is untouched.
1019 $probe = clone $scripts;
1020 $probe->to_do = array();
1021 $probe->done = array();
1022 $probe->all_deps( $contributors );
1023
1024 // `all_deps()` bails out wholesale if any single dependency is
1025 // unregistered — and every contributor depends on `wp-commands`,
1026 // which a pre-6.9 site simply does not have. Falling back to the
1027 // contributors themselves keeps the hoist working there: their Core
1028 // dependencies are already carried by the Core manifest this list is
1029 // appended to, so the contributor script is the only part that has
1030 // to come from here.
1031 $chain = $probe->to_do;
1032 foreach ( $contributors as $handle ) {
1033 if ( ! in_array( $handle, $chain, true ) ) {
1034 $chain[] = $handle;
1035 }
1036 }
1037
1038 $entries = array();
1039 foreach ( $chain as $handle ) {
1040 $payload = openstation_resolve_script_payload( $handle );
1041 if ( '' === $payload['url']
1042 && empty( $payload['before'] )
1043 && empty( $payload['after'] )
1044 && empty( $payload['l10n'] ) ) {
1045 continue;
1046 }
1047 $entries[] = array(
1048 'handle' => (string) $handle,
1049 'url' => $payload['url'],
1050 'before' => $payload['before'],
1051 'after' => $payload['after'],
1052 'l10n' => $payload['l10n'],
1053 'translations' => $payload['translations'],
1054 );
1055 }
1056
1057 // Unwind: none of it prints at boot any more. Dequeue, never
1058 // deregister — anything that genuinely depends on one of these
1059 // still resolves it.
1060 foreach ( $contributors as $handle ) {
1061 wp_dequeue_script( $handle );
1062 }
1063
1064 if ( empty( $entries ) ) {
1065 return;
1066 }
1067
1068 wp_add_inline_script(
1069 'openstation',
1070 sprintf(
1071 '(function(c){if(!c||!c.commandPalette)return;var s=c.commandPalette.scripts;if(!s)return;Array.prototype.push.apply(s,%s);})(window.openStationConfig);',
1072 wp_json_encode( $entries )
1073 ),
1074 'before'
1075 );
1076
1077 /**
1078 * Fires after the shell hoists palette contributors into the
1079 * deferred manifest.
1080 *
1081 * @param string[] $contributors Handles moved off the boot document.
1082 */
1083 do_action( 'openstation_command_palette_contributors_hoisted', $contributors );
1084 }
1085 add_action( 'admin_enqueue_scripts', 'openstation_shell_hoist_command_palette_contributors', PHP_INT_MAX );
1086