Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ Three related knobs:
Shell surfaces that render on demand but are **not** native windows — the AI assistant, the bug-report window — get the same deferral through a different pipe: `openstation_build_deferred_styles()` resolves their handles into `openStationConfig.deferredStyles` (handle → `{ url, inline }`), and the surface's open path calls `ensureDeferredStyle( handle )` (`src/deferred-styles.ts`) to inject the sheet once, in parallel with whatever bundle the open is already fetching. Internal plumbing rather than public API — a plugin's own on-demand surface should be a native window and use `styles`.
- **`wp.os.loadWindowScript( id )`** — load a window's bundle without opening the window, for the case where another bundle needs an API that one publishes. See [`javascript-reference.md`](./javascript-reference.md).

The payload builders harvest each registered handle's `extra['data']` (localize), `extra['before']` / `extra['after']` (inline), and `wp_set_script_translations()` snippet into a **handle-keyed map** — `nativeWindowScriptData[handle] = { url, before, after, l10n, translations }` — while the `nativeWindows[]` entries carry only handle NAMES (`scriptHandle`, `companionScripts` as an ordered handle list, `tabs[].scriptHandle`). The shell joins the two on receipt (`hydrateServerEntries()` in `src/native-windows.ts`, tolerant of the old inline-entry format for cross-version bridge payloads) and injects the data as inline `<script>` tags around the `<script src>` in `wp_print_scripts` order — translations → l10n → before → src → after. So `wp_localize_script` / `wp_add_inline_script` / `wp_set_script_translations` work transparently on both paths. This is why the enqueue hook runs at priority **5**: `openstation_enqueue_assets()` builds the payload at 10, and data attached after that would ship a bundle with no config.
The payload builders harvest each registered handle's `extra['data']` (localize), `extra['before']` / `extra['after']` (inline), and `wp_set_script_translations()` snippet into a **handle-keyed map** — `nativeWindowScriptData[handle] = { url, before, after, l10n, translations }` — while the `nativeWindows[]` entries carry only handle NAMES (`scriptHandle`, `companionScripts` as an ordered handle list, `tabs[].scriptHandle`). The shell joins the two on receipt (`hydrateServerEntries()` in `src/native-windows.ts`, tolerant of the old inline-entry format for cross-version bridge payloads) and injects the data as inline `<script>` tags around the `<script src>` in `wp_print_scripts` order — translations → l10n → before → src → after. Each loadable handle's map entry also names its dependency closure in `deps` — ordered handles, every one a key of the same map, so a package shared by several bundles is serialized once — and the loader brings those into the tab before the bundle (`ScriptExtras.deps` in `src/wallpapers/vendor-loader.ts`); a src-less alias handle lands with an empty `url` and its inline data replayed in print order. So `wp_localize_script` / `wp_add_inline_script` / `wp_set_script_translations` — and a declared dependency — work transparently on both paths. This is why the enqueue hook runs at priority **5**: `openstation_enqueue_assets()` builds the payload at 10, and data attached after that would ship a bundle with no config.

Two further pieces the boot config deliberately does NOT carry, because the boot page delivers them another way: `nativeWindows[].templateHtml` is stripped (`''`) — every registered window's template is server-printed as a real `<template>` tag at `admin_footer` @ 20, before footer scripts, and `ensureTemplate()` adopts it by id; the payload copy exists for mid-session activations, so bridge and probe payloads keep theirs. And `serverDesktopThemes[]` entries ship without `cssText` / `tokens` (`cssDeferred: true`) — see [desktop-themes.md](./desktop-themes.md) for the on-demand fetch that fills them. The command-palette manifest plays the same trick on Core's `initializeCommandPalette` inline: the embedded ~20 KB menu-command list is stripped and the call synthesized against `window.__openStationMenuCommands`, the copy the boot page already ships for the shell harvester's classification lookup.

Expand Down
2 changes: 1 addition & 1 deletion docs/hooks-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2728,7 +2728,7 @@ if ( is_wp_error( $result ) ) {

> **`style`.** Optional `wp_register_style()` handle. The shell resolves it to a `styleUrl` (and any `wp_add_inline_style()` blobs) and lazy-injects a `<link rel="stylesheet">` when the window's plugin is activated mid-session. Without `style`, a peer plugin activated from inside an open shell renders its window with **no CSS** until the user reloads — the parent shell already finished `wp_print_styles` before the plugin existed. If the handle isn't registered, the field is silently dropped (no error, no link); plugins active at boot continue to print through the normal `wp_print_styles` pipeline as before.

> **`script` loads on first open.** The shell reads your render callback off `window.openStationNativeWindows[ <id> ]` *after* fetching the bundle, so nothing is required of you: register the handle, publish the callback, and the window works. What changes is *when* — a bundle is no longer printed on every admin page for a window the user may never open. `wp_localize_script` / `wp_add_inline_script` / `wp_set_script_translations` data is harvested off the registered handle into the payload and replayed around the injected `<script>` tag, so it arrives either way.
> **`script` loads on first open.** The shell reads your render callback off `window.openStationNativeWindows[ <id> ]` *after* fetching the bundle, so nothing is required of you: register the handle, publish the callback, and the window works. What changes is *when* — a bundle is no longer printed on every admin page for a window the user may never open. `wp_localize_script` / `wp_add_inline_script` / `wp_set_script_translations` data is harvested off the registered handle into the payload and replayed around the injected `<script>` tag, so it arrives either way. So are the handle's **declared dependencies**: the closure WordPress would have resolved had it printed the handle is shipped with the window and loaded, in order, before the bundle — `wp-*` packages and your own handles alike, skipping anything the document already ran. A src-less alias (`wp_register_script( 'acme-config', false )` carrying your config through `wp_add_inline_script()`, declared as the bundle's dependency) has its inline data replayed in print order with nothing fetched. Declare what you use and it works on a live activation exactly as it does after a reload; see [`docs/migration-wp-package-globals.md`](./migration-wp-package-globals.md).
>
> **`scripts`** *(optional, `string[]`)* — companion handles loaded in order immediately **before** `script`. For a bundle that extends the window from outside it — subscribing to actions the window's own bundle fires, contributing a section — and therefore has to be listening before that bundle is parsed. Declaring it here is what keeps it off the boot critical path: it travels with the window it extends. Handles that were never registered are dropped silently, the same way `style` is.
>
Expand Down
2 changes: 1 addition & 1 deletion docs/javascript-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -5708,7 +5708,7 @@ The built-in Snow wallpaper (`src/plugins/snow-wallpaper/`) is the canonical in-
| `registerWallpaper( def )` | Stable | Add a wallpaper to the registry + re-apply |
| `registerWidget( def )` | Stable | Add a widget to the registry |
| `registerSystemTile( item )` | Stable | Add a JS-owned launcher tile to the bottom dock rail, alongside plugin admin menus. Returns nothing; fires `os.dock.item-appended`. See "System tiles" below. |
| `loadVendorScript( url, extras? )` | Stable | Memoized `<script>` injector. Low-level; most plugins use `needs` instead. Never re-executes something the document already ran — pass `extras.handle` whenever you know the WP script handle, since a Core package delivered inside a `load-scripts.php` concat blob has no `<script src>` of its own to match on. |
| `loadVendorScript( url, extras? )` | Stable | Memoized `<script>` injector. Low-level; most plugins use `needs` instead. Never re-executes something the document already ran — pass `extras.handle` whenever you know the WP script handle, since a Core package delivered inside a `load-scripts.php` concat blob has no `<script src>` of its own to match on. `extras.deps` is an ordered dependency list loaded first; an entry with an empty `url` is a src-less alias whose inline data is replayed in print order, once per document. |
| `getWallpaperSurfaces()` | Stable | Live `WallpaperSurface[]` for collision-aware wallpapers. See "Wallpaper surfaces" below. |
| `registerModule( def )` | Stable | Register a shared vendor library under a stable id. |
| `loadModules( ids )` | Stable | Imperatively load registered modules. Usually unnecessary — canvas wallpapers declare `needs[]` and the shell resolves. |
Expand Down
40 changes: 33 additions & 7 deletions docs/migration-wp-package-globals.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ subscribed before that point goes deaf; re-running `wp-data` wipes
every registered store.

"Already in the document" is answered by handle as well as by URL,
because on a stock wp-admin the URL alone cannot answer it. Core
because on a stock wp-admin the URL alone cannot answer it. A tag Core
printed under the handle's own id (`<handle>-js`, or
`<handle>-js-before` and its siblings for inline data) counts. Core
concatenates every script below `wp-includes/js/` and `wp-admin/js/`
into a single `load-scripts.php` response — the wp-admin default, off
under `SCRIPT_DEBUG` or `CONCATENATE_SCRIPTS = false` — so those
Expand All @@ -96,12 +98,36 @@ the shell reads them back (`src/script-presence.ts`). Nothing is
asked of you: the handles ride along in the payload OpenStation
builds from your registration.

**No other lazy path does this yet.** Native-window scripts, command
scripts, settings-tab scripts, wallpapers, games and desktop-file
openers all travel the same loader, but their payload builders do not
resolve a closure. If one of those needs a `@wordpress/*` package,
either enqueue the handle normally so WordPress resolves it, or load
the package yourself before use — do not rely on load order.
**Native-window bundles close it the same way.** The handle a window
registers as its `script` — and each companion in `scripts`, and each
tab script — ships its closure in the window payload, and the loader
replays it before the bundle on first open. A window activated
mid-session therefore boots with the same packages it would have had
from a cold reload.

**A src-less alias handle is replayed too.** `wp_register_script( $h,
false )` plus `wp_add_inline_script()` is WordPress's supported way to
ship inline-only JavaScript, and a common home for a plugin's config
blob — declared as a dependency of every bundle so the config always
runs first, whatever the enqueue order. The alias has nothing to fetch,
so its inline data is replayed in print order (localized data, then
`before`, then `after`) with no `<script src>` in between, once per
document, and not at all when Core already printed it — the
`<script id="<handle>-js-before">` Core leaves behind is the evidence.
That is the case that surfaced this: a window whose builder read its
config off exactly such an alias opened, after a live activation, with
the config undefined.

**Every other lazy path ships its closure the same way** — command
scripts, settings-tab scripts, dock-rail renderers, title-bar buttons,
window actions, unfocus effects, window-link renderers, window themes,
controls, slots and chromes, wallpapers, games and desktop-file
openers. That uniformity is load-bearing rather than tidy: the loader
memoizes by URL, so whichever path fetches a bundle *first* decides
what ran before it. A plugin that registers one bundle as both a
window's `script` and a command script had the command sync win that
race on a live activation, and the bundle ran without the config alias
it declared. Declare what you use, on every handle, and it works.

The loader side of the mechanism is generic, so extending the
remaining builders is a payload change rather than a new mechanism.
6 changes: 6 additions & 0 deletions includes/commands.php
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ function openstation_build_desktop_command_scripts_payload() {
'scriptAfter' => $payload['after'],
'scriptL10n' => $payload['l10n'],
'scriptTranslations' => $payload['translations'],
// The handle's dependency closure, replayed before the bundle
// on its lazy load — see `openstation_resolve_script_dependencies()`.
'scriptDeps' => openstation_resolve_script_dependencies( $handle ),
);
$seen[ $handle ] = true;
}
Expand Down Expand Up @@ -305,6 +308,9 @@ function openstation_build_desktop_commands_payload() {
'scriptAfter' => $payload['after'],
'scriptL10n' => $payload['l10n'],
'scriptTranslations' => $payload['translations'],
// The handle's dependency closure, replayed before the bundle
// on its lazy load — see `openstation_resolve_script_dependencies()`.
'scriptDeps' => openstation_resolve_script_dependencies( $handle ),
);
}
return $out;
Expand Down
Loading
Loading