| 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 |
|