PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.8
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.8
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 0.8.9 0.8.8 0.8.7 0.8.6 All 33 releases
desktop-mode / includes / render / chromeless-trim.php

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

1,022 lines 39.1 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 /**
781 * Fires after OpenStation drops the command-palette family in a window.
782 */
783 do_action( 'openstation_chromeless_trimmed_command_palette' );
784 }
785 add_action( 'admin_enqueue_scripts', 'openstation_chromeless_trim_command_palette', PHP_INT_MAX );
786
787 /**
788 * Second pass: strip the palette family from the actual print list.
789 *
790 * Same two survivors as the named trim below — late enqueues and
791 * dependency pull-back — but the family has to be recomputed here
792 * rather than reused, because a handle enqueued after
793 * `admin_enqueue_scripts` was never in the queue the dequeue pass
794 * scanned. The walk runs against the to-print list, which is the last
795 * word before output.
796 *
797 * @param string[] $handles Script handles about to print.
798 * @return string[]
799 */
800 function openstation_chromeless_filter_palette_print_list( $handles ) {
801 if ( ! is_array( $handles ) || ! openstation_chromeless_should_trim_command_palette() ) {
802 return $handles;
803 }
804 $scripts = wp_scripts();
805 if ( ! $scripts ) {
806 return $handles;
807 }
808 $drops = openstation_chromeless_command_palette_drops( $scripts, $handles );
809
810 return array_values( array_diff( $handles, $drops ) );
811 }
812 add_filter( 'print_scripts_array', 'openstation_chromeless_filter_palette_print_list' );
813
814 /**
815 * Second pass: strip the palette style roots from the print list.
816 *
817 * @param string[] $handles Style handles about to print.
818 * @return string[]
819 */
820 function openstation_chromeless_filter_palette_style_print_list( $handles ) {
821 if ( ! is_array( $handles ) || ! openstation_chromeless_should_trim_command_palette() ) {
822 return $handles;
823 }
824 // A contributor that owns this screen keeps the runtime, and its
825 // stylesheet with it — otherwise the palette it is allowed to show
826 // would render unstyled.
827 $scripts = wp_scripts();
828 if ( $scripts
829 && empty( openstation_chromeless_command_palette_drops( $scripts, $scripts->queue ) ) ) {
830 return $handles;
831 }
832
833 // Styles have their own dependency graph, and a surviving sheet may
834 // sit on top of a palette one — `wp-commands`' stylesheet is a
835 // dependency of others on editor screens. Drop only what nothing
836 // still printing depends on.
837 return array_values(
838 array_diff(
839 $handles,
840 openstation_protect_survivor_dependencies(
841 wp_styles(),
842 $handles,
843 openstation_command_palette_root_handles()
844 )
845 )
846 );
847 }
848 add_filter( 'print_styles_array', 'openstation_chromeless_filter_palette_style_print_list' );
849
850 /**
851 * Second pass: strip trimmed handles from the actual print list.
852 *
853 * The dequeue above cannot be the whole story, and a live install
854 * showed exactly why. Two things survive it:
855 *
856 * 1. **Late enqueues.** A host mu-plugin that queues its masterbar
857 * assets after `admin_enqueue_scripts` has already run is simply
858 * not in the queue yet when we dequeue. WordPress.com's
859 * `wpcom-notes-*` handles behave this way.
860 * 2. **Dependency pull-back.** `WP_Dependencies::all_deps()` pulls a
861 * dequeued-but-registered handle back in when something still
862 * queued declares it as a dependency. Core's `admin-bar`
863 * stylesheet rode back in on the notes stylesheet exactly so.
864 *
865 * `print_scripts_array` / `print_styles_array` run inside `do_items()`
866 * after every enqueue, dequeue and dependency walk has finished, so
867 * they are the last word. Scope stays the same list — a handle nobody
868 * asked us to trim is never touched — and because the list covers the
869 * whole family, removing one member never strands another member that
870 * depended on it.
871 *
872 * @param string[] $handles Handles WordPress is about to print.
873 * @param string $kind 'scripts' or 'styles'.
874 * @return string[] Filtered handles.
875 */
876 function openstation_chromeless_filter_print_list( $handles, $kind ) {
877 if ( ! is_array( $handles ) || ! openstation_is_chromeless_request() ) {
878 return $handles;
879 }
880 $trim = 'scripts' === $kind
881 ? openstation_chromeless_trimmed_scripts()
882 : openstation_chromeless_trimmed_styles();
883
884 return array_values( array_diff( $handles, $trim ) );
885 }
886
887 add_filter(
888 'print_scripts_array',
889 static function ( $handles ) {
890 return openstation_chromeless_filter_print_list( $handles, 'scripts' );
891 }
892 );
893 add_filter(
894 'print_styles_array',
895 static function ( $handles ) {
896 return openstation_chromeless_filter_print_list( $handles, 'styles' );
897 }
898 );
899
900 /**
901 * Hoists the shell's palette contributors into the deferred manifest.
902 *
903 * The shell already defers Core's own palette runtime and replays it on
904 * the first ⌘K. Plugin contributors defeated that completely: they are
905 * enqueued normally, they declare `wp-commands`, and
906 * `WP_Dependencies::all_deps()` therefore pulled the palette chain back
907 * into the boot document on their behalf — the deferral was in place
908 * and paying for nothing.
909 *
910 * Worse, it did not even work. A contributor that ran at boot
911 * registered its commands against a `core/commands` store that does not
912 * exist yet, and lost them. Hoisting fixes the bug and the cost
913 * together: the contributor now executes as part of the replay, *after*
914 * the store exists, so a plugin's commands reach the palette **once, on
915 * the shell**, and cost no window anything.
916 *
917 * Runs at `PHP_INT_MAX` so every plugin has enqueued. Appends to
918 * `openStationConfig.commandPalette.scripts` through a `before` inline
919 * on our own bundle, which prints after the localized config object and
920 * before the bundle that reads it. `src/commands/palette-assets.ts`
921 * de-duplicates by handle, so a dependency the Core manifest already
922 * lists is never executed twice — and re-running `wp-data` would wipe
923 * every store registered against the first copy.
924 */
925 function openstation_shell_hoist_command_palette_contributors() {
926 if ( ! openstation_is_shell_request() ) {
927 return;
928 }
929 $scripts = wp_scripts();
930 if ( ! $scripts || ! wp_script_is( 'openstation', 'enqueued' ) ) {
931 return;
932 }
933
934 // No Core palette on this WordPress, no manifest to hoist into.
935 //
936 // `openstation_build_command_palette_assets_payload()` returns null
937 // when `wp_enqueue_command_palette_assets()` does not exist, so
938 // `openStationConfig.commandPalette` is null and the replay has
939 // nowhere to put anything. Hoisting anyway would dequeue the
940 // contributors from the boot page and then drop them on the floor —
941 // their commands would not merely be late, they would be gone, which
942 // is strictly worse than the eager loading this replaces. Leave them
943 // printing normally instead.
944 if ( ! function_exists( 'wp_enqueue_command_palette_assets' ) ) {
945 return;
946 }
947
948 $contributors = openstation_command_palette_contributors( $scripts, $scripts->queue );
949 if ( empty( $contributors ) ) {
950 return;
951 }
952
953 // Resolve each contributor's ordered chain on a clone, so the
954 // request's real `$to_do` / `$done` state is untouched.
955 $probe = clone $scripts;
956 $probe->to_do = array();
957 $probe->done = array();
958 $probe->all_deps( $contributors );
959
960 // `all_deps()` bails out wholesale if any single dependency is
961 // unregistered — and every contributor depends on `wp-commands`,
962 // which a pre-6.9 site simply does not have. Falling back to the
963 // contributors themselves keeps the hoist working there: their Core
964 // dependencies are already carried by the Core manifest this list is
965 // appended to, so the contributor script is the only part that has
966 // to come from here.
967 $chain = $probe->to_do;
968 foreach ( $contributors as $handle ) {
969 if ( ! in_array( $handle, $chain, true ) ) {
970 $chain[] = $handle;
971 }
972 }
973
974 $entries = array();
975 foreach ( $chain as $handle ) {
976 $payload = openstation_resolve_script_payload( $handle );
977 if ( '' === $payload['url']
978 && empty( $payload['before'] )
979 && empty( $payload['after'] )
980 && empty( $payload['l10n'] ) ) {
981 continue;
982 }
983 $entries[] = array(
984 'handle' => (string) $handle,
985 'url' => $payload['url'],
986 'before' => $payload['before'],
987 'after' => $payload['after'],
988 'l10n' => $payload['l10n'],
989 'translations' => $payload['translations'],
990 );
991 }
992
993 // Unwind: none of it prints at boot any more. Dequeue, never
994 // deregister — anything that genuinely depends on one of these
995 // still resolves it.
996 foreach ( $contributors as $handle ) {
997 wp_dequeue_script( $handle );
998 }
999
1000 if ( empty( $entries ) ) {
1001 return;
1002 }
1003
1004 wp_add_inline_script(
1005 'openstation',
1006 sprintf(
1007 '(function(c){if(!c||!c.commandPalette)return;var s=c.commandPalette.scripts;if(!s)return;Array.prototype.push.apply(s,%s);})(window.openStationConfig);',
1008 wp_json_encode( $entries )
1009 ),
1010 'before'
1011 );
1012
1013 /**
1014 * Fires after the shell hoists palette contributors into the
1015 * deferred manifest.
1016 *
1017 * @param string[] $contributors Handles moved off the boot document.
1018 */
1019 do_action( 'openstation_command_palette_contributors_hoisted', $contributors );
1020 }
1021 add_action( 'admin_enqueue_scripts', 'openstation_shell_hoist_command_palette_contributors', PHP_INT_MAX );
1022