chore(sync): mirror plugin tree into a sibling wordpress-develop checkout - #74
Closed
epeicher wants to merge 14 commits into
Closed
chore(sync): mirror plugin tree into a sibling wordpress-develop checkout#74epeicher wants to merge 14 commits into
epeicher wants to merge 14 commits into
Conversation
Adds an in-shell catalog of Desktop Mode extensions resolved from the GitHub release manifest, with one-click install / activate / update / delete via standard Plugin_Upgrader. CI: bin/package-extensions.sh and bin/build-manifest.sh now run on every release tag, uploading per-extension zips and extensions.resolved.json as release assets alongside desktop-mode.zip. WP plugin: includes/marketplace/ fetches the manifest (15-min transient, filterable URL via wp_desktop_marketplace_manifest_url), exposes REST under wp-desktop/v1/marketplace/*, and injects updates into the native site_transient_update_plugins so they surface in Dashboard -> Updates, the Plugins screen, and WP-CLI. Multisite subsite admins see a read-only catalog; only install_plugins-capable users can mutate. UI: new Extensions tab in OS Settings (admin-only, order=50) with state-driven action buttons and an inline "vX -> vY [Update]" banner per outdated card. Local-dev: WP_DEBUG + WP_DESKTOP_LOCAL_MARKETPLACE_DIR shells out to package-extensions.sh against a local checkout instead of downloading from the release, so extension authors can iterate without cutting a tag.
The local-dev escape hatch (WP_DEBUG + WP_DESKTOP_LOCAL_MARKETPLACE_DIR) previously only bypassed the binary download — the manifest still had to come from a release URL. That left the catalog unreachable until the first release that ships extensions.resolved.json exists, which is a chicken-and-egg for contributors testing the marketplace before merge. Now in local-dev mode the manifest is computed in PHP from extensions.json + each plugin's header at the checkout, mirroring what bin/build-manifest.sh produces in CI. No network call, no transient cache, no release dependency — the marketplace works end-to-end against a checkout.
The local-dev manifest synthesizer required an explicit WP_DESKTOP_LOCAL_MARKETPLACE_DIR constant. When the running plugin folder already contains extensions.json and an extensions/ directory (e.g. when symlinked from a source checkout, or when the dev sync mirrors them in), auto-detect from DESKTOP_MODE_DIR with no constant required. Both extensions/ and extensions.json are export-ignored from the production plugin zip, so a released install cannot trip the local path even if WP_DEBUG is on. The explicit constant remains as an override for setups where the source lives outside the plugin folder.
The local-dev manifest synthesizer learned to auto-detect the source checkout from DESKTOP_MODE_DIR, but the install path (desktop_mode_marketplace_local_zip) still required the explicit WP_DESKTOP_LOCAL_MARKETPLACE_DIR constant. Result: in auto-detect mode the catalog rendered fine but Install failed with "no download_url" — the local fast-path was never reached and the empty-by-design download_url tripped the SSRF guard. Extract a single desktop_mode_marketplace_local_checkout() resolver that both functions call, so the manifest and installer always agree on which checkout (if any) is in play.
Replace the shell-out to bin/package-extensions.sh with a ZipArchive- based packager. The script approach needed bin/, git, bash, tar, and zip available at the checkout — none of which can be assumed in a typical wp-env / Studio setup that mounts only the plugin folder, and exec() is often sandbox-disabled too. ZipArchive is bundled with virtually every WP-capable PHP build, so this works in any dev environment without extra setup. Vendored content (e.g. assets/vendor/phpmyadmin/) is still bundled verbatim when present — same observable behaviour CI's splice produces.
Hook into the existing wp.desktop.refreshMenu() public API after every install / activate / deactivate / update / delete so the new (or removed) extension's dock items, native windows, widgets, settings tabs, etc. appear without the user having to hard-reload the page. The /wp-desktop/v1/menu endpoint already polyfills admin context (WP_ADMIN constants + admin_menu hook), so just-activated plugins register their hooks before the payload is built and the live-refresh sees the new contributions correctly.
Vendored libraries commonly use dist/ for compiled assets — phpMyAdmin ships its left-navigation tree (`navigation.js`), home dashboard, SQL editor, etc. under `assets/vendor/phpmyadmin/js/dist/`. My skip-pattern was tossing all of them out, so phpMyAdmin's UI loaded without its core JS and the navigation tree was empty. Drop `dist` from the pattern. Keep `.git`, `.DS_Store`, `node_modules`, `.cache` — those are unambiguously dev junk.
Two fixes for the live-refresh path used after a marketplace install / activate / deactivate / update / delete: 1. /wp-desktop/v1/menu was throwing a fatal error when called from a non-admin REST request. wp-admin/menu.php and its includes don't declare `global $menu, $submenu, …` themselves — they assume they're loaded at the global scope. Required from inside our bootstrap function, those become local variables, $menu stays null, uksort() fatals at the bottom of wp-admin/includes/menu.php. The chromeless plugins.php fast-path always provides the payload inline so this REST fallback is rarely hit in normal flow, which is why it stayed broken silently. The marketplace forces the REST path on every mutation and surfaces it. Bind the relevant globals in the bootstrap function before the require_once so the included files mutate the real $GLOBALS. 2. Every settings-tab registry mutation snapped the user back to the Appearance tab. The renderPanel re-render reset the tab strip to its initial value, since wpd-tabs[value] is the static initial attribute, not the live property. Track the active tab via a wpd-tab-change listener on each render and pass it as the initial value of the new <wpd-tabs>.
delete_plugins() reaches into request_filesystem_credentials() which lives in wp-admin/includes/file.php — the delete handler was only requiring plugin.php and crashing with "Call to undefined function" when the user clicked Delete on an installed extension. Switch to desktop_mode_marketplace_require_upgrader() (already used by install / update) so all five mutation paths share the same load sequence: file.php + misc.php + plugin.php + plugin-install.php + class-wp-upgrader.php.
Two issues compounding on the very first openWindow call after a marketplace-driven activation: 1. The bundle's render() never ran. The framework mounts the window template synchronously; the bundle then loads asynchronously and registers its callback into wpDesktopNativeWindows[id]. By the time the callback exists, the mount has already happened, so the loading skeleton stayed forever. Have the bundle self-recover: after registering, scan for any already-mounted skeleton and render into it. Idempotent — renderPhpMyAdmin clears the root before injecting the iframe. 2. The iframe rendered at the user-agent default 300×150. The stylesheet (phpmyadmin.css) ships via admin_enqueue_scripts which doesn't fire for plugins activated mid-session — the page that's already open never picks up the new <link>. Without the CSS, .wpdc-phpmyadmin__frame is unstyled and the iframe collapses to its UA default size, hiding most of phpMyAdmin's content. Apply the critical sizing inline on root and iframe so the layout works regardless of stylesheet presence. Once the user reloads, the stylesheet's rules take over with no behavioural change. .min.js regenerated via terser to match.
…ension
Move the Code editor (`includes/code-editor/`, `src/code-editor/`,
`assets/{js,css}/code-editor*`, `assets/vendor/monaco-editor/`) into a
new sibling plugin under `extensions/desktop-mode-code-editor/` so it
can be installed and activated independently of desktop-mode.
PHP layer renamed: `wpdc_*` → `desktop_mode_code_editor_*`,
`WPDC_*` → `DESKTOP_MODE_CODE_EDITOR_*`, textdomain `desktop-mode-code-editor`,
REST namespace `desktop-mode-code-editor/v1`, hook prefix
`desktop_mode_code_editor_*`. Window id `wpdc-editor`, the
`wpDesktopCodeEditorConfig` global, the `data-wpdc-editor-*` selectors,
and the `wpdc-editor*` CSS classes are kept — they're public API per
`docs/examples/code-editor-open.md`, and renaming would break consumers
that deep-link via `wp.desktop.openWindow('wpdc-editor')`.
Config delivery switches from `wp_localize_script` to an admin-ajax-served
bundle that bakes the Monaco vendor URL + REST URLs + nonce into the JS
response body — the previous wiring relied on `wp_print_scripts` firing,
which silently fails on the lazy-load path the native-window sync takes.
Per-extension build pipeline added: `package.json` + `vite.config.js` +
`tsconfig.json` for the TS sources, plus `bin/fetch-monaco.sh` (auto-
discovered by `bin/package-extensions.sh`) that copies Monaco's AMD
distributable from `node_modules` into the gitignored
`assets/vendor/monaco-editor/`. The packaging script splices the vendor
back into the zip so end users get a self-contained 6.4 MB plugin.
In-tree leftovers removed:
- `includes/code-editor/` (6 PHP files), `src/code-editor/` (12 TS files)
- `assets/{js,css}/code-editor*`, `assets/vendor/monaco-editor/`
- `require_once` in `desktop-mode.php`, script/style/translation
registrations in `includes/assets.php`
- `code-editor` Vite target, `build:code-editor` + `vendor:monaco`
npm scripts, `monaco-editor` + `@monaco-editor/loader` deps
- obsolete `/assets/js/code-editor*` lines in root `.gitignore`
- `tests/phpunit/tests/wpdc*.php` (referenced renamed functions;
re-establishing PHPUnit coverage inside the extension is a follow-up
— neither cron-manager nor phpmyadmin ship tests today)
Docs updated: `extensions.json` (new entry), `docs/javascript-reference.md`
(extension-required note next to `wpdc-editor` example),
`docs/examples/code-editor-open.md` (header note + renamed function).
These 116 files moved into `extensions/desktop-mode-code-editor/assets/vendor/monaco-editor/` in the previous commit, but the deletions of the original `assets/vendor/monaco-editor/` paths were not staged. With this commit `assets/vendor/` no longer ships Monaco — the extension's `bin/fetch-monaco.sh` repopulates it inside the extension during packaging.
…kout Adds bin/sync-to-wp-develop.sh and a bin/sync-to-wp-develop.includes manifest so the working tree can be one-shot or watch-mode mirrored into ~/github/wordpress-develop/src/wp-content/plugins/desktop-mode (or any path via WPDM_SYNC_DEST). The destination is populated via rsync of real files — symlinks aren't followed reliably by the target environment. Watch mode runs `npm run build` on every change to source inputs (TS under src/, CSS, PHP, plus per-extension TS) and rsyncs the include set, never observing its own writes to assets/js/ or assets/vendor/ so it can't loop. Per-extension package.json builds are dispatched from the same loop because the parent build doesn't recurse into extensions/. Extension dev tooling (node_modules/, src/, vite/tsconfig/package configs) is excluded from the rsync so the mirror only carries runtime artefacts.
The marketplace integration as designed cannot ship through wp.org —
it triggers a `plugin_updater_detected` (severity 9) auto-reject for
the `site_transient_update_plugins` filter, and the underlying "in-
shell installer for third-party plugins" pattern conflicts with wp.org
review guidelines regardless. Extensions instead live in the repo
(packaged + published as separate zips via release.yml's
package-extensions.sh step) and users install them independently.
Removed:
- includes/marketplace/ (PHP backend)
- src/settings/sections/extensions/ (UI)
- docs/marketplace.md + index/hooks references (docs)
- assets/css/os-settings.css marketplace block (~150 LOC)
- marketplaceUrl plumbing across desktop.ts / render.php / types
Kept (still useful, doesn't ship in the wp.org zip — bin/ + extensions/
are export-ignored):
- bin/package-extensions.sh + bin/build-manifest.sh (CI tooling)
- extensions.json (curated index for the release pipeline)
- .github/workflows/release.yml extension-packaging steps
Also kept on the side benefits of the marketplace work:
- includes/menu.php $menu/$submenu globals fix (real bug, helps the
chromeless plugins.php live-refresh path too)
- OsSettings activeTabId preservation (keeps user on their picked
tab when any plugin live-mutates the settings-tab registry)
- phpMyAdmin bundle self-recovery + inline iframe sizing (renders
correctly on first open, regardless of CSS-load timing)
Plugin Check fixes:
- includes/devtools.php: sanitize_text_field() on $_SERVER input
- includes/presence.php: hoist defined(ABSPATH) guard above docblock
so the static analyzer sees it
- .DS_Store files purged from the working tree
8 tasks
epeicher
force-pushed
the
add/code-editor-extension
branch
from
May 4, 2026 09:38
539cc73 to
b25b328
Compare
Collaborator
Author
|
The sync scripts will be added as part of a fresh PR, closing this as not required. |
7 tasks
epeicher
added a commit
that referenced
this pull request
May 4, 2026
* fix(menu+settings): preserve admin globals + active settings tab on re-render Two fixes that surfaced together when third-party plugins live-mutate state on the desktop and force a re-render of windows or panels. 1. \`includes/menu.php\` — \`desktop_mode_bootstrap_admin_menu_for_rest()\` includes \`wp-admin/menu.php\` from inside a function, which means \`\$menu\` / \`\$submenu\` / \`\$_wp_real_parent_file\` etc. silently become local variables. At sort time, \`\$menu\` is null and \`uksort()\` fatals. Hoist the relevant globals so the included files mutate the real \`\$GLOBALS['menu']\` that downstream payload-builders read. 2. \`src/settings/index.ts\` — when the settings-tab registry mutates (e.g. a third-party plugin live-registers a tab via the chromeless plugins-changed bridge), \`refreshMenu()\` re-renders the panel via \`lit\`. The new \`<wpd-tabs>\` is constructed with \`value="appearance"\` as its initial attribute, snapping the user back to the Appearance tab mid-action. Track the active tab via a \`wpd-tab-change\` listener and pass it as the initial value of the freshly-rendered \`<wpd-tabs>\`. Salvaged from PR #74's preliminary work. * fix(phpmyadmin): self-recover bundle + inline-size iframe on first open When the extension is activated mid-session, the framework's synchronous mount path runs the window template before this lazy-loaded bundle has registered its render callback — the user sees the loading skeleton and nothing else until they reload. Two fixes that, together, make first-open render correctly regardless of CSS/JS load timing: 1. After defining \`registry["wpdc-phpmyadmin"]\`, scan for any already-mounted skeleton (\`[data-wpdc-phpmyadmin-loading]\`) and render into it. Idempotent — \`renderPhpMyAdmin\` clears the root before injecting the iframe. 2. Set the root + iframe sizing inline on the elements (\`flex: 1 1 auto; width: 100%; height: 100%; border: 0; display: block\`) so the layout works even when phpmyadmin.css hasn't been enqueued yet. Once the user reloads, the stylesheet's rules take over with no behavioural change. \`.min.js\` regenerated to match. Salvaged from PR #74. * chore(plugin-check): sanitize devtools input + reorder presence guard Two fixes the WordPress Plugin Check linter flags on the wp.org submission, both no-ops at runtime but required to clear automated review. 1. \`includes/devtools.php\` — wrap the \`HTTP_X_WP_DEBUG_SESSION\` read in \`sanitize_text_field( wp_unslash( … ) )\`. The downstream code already restricts the value to a whitelist of session-id characters, so this doesn't change behaviour — it just satisfies the linter, which doesn't see the downstream filter. 2. \`includes/presence.php\` — hoist \`defined( 'ABSPATH' ) || exit;\` immediately under the opening \`<?php\` tag, above the docblock. The static analyzer doesn't scan past long docblocks looking for the guard, and flags the file as missing it. Salvaged from PR #74.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
bin/sync-to-wp-develop.sh+bin/sync-to-wp-develop.includesto mirror the working tree into~/github/wordpress-develop/src/wp-content/plugins/desktop-mode(override withWPDM_SYNC_DEST).bin/sync-to-wp-develop.shrunsnpm run build, builds each extension that hasnode_modules/, then rsyncs the include set.bin/sync-to-wp-develop.sh --watchadds an fswatch loop over the source inputs (TS undersrc/, CSS, PHP, per-extensionsrc/, etc.) — built bundles underassets/js/andassets/vendor/are rsynced but not watched, so the build can't loop on its own output.Why this is on top of
add/code-editor-extensionThe includes manifest references
extensions.json, which only exists on this branch (added by8f162e5 feat(marketplace): add Extensions tab + release pipeline). Targetingtrunkwould land a broken manifest.Notable behaviours
extensions/excludesnode_modules/,src/,package.json,package-lock.json,tsconfig.json,vite.config.js,.gitignore— each extension owns its own build pipeline; only runtime artefacts ship to wordpress-develop./node_modules/and/extensions/<name>/assets/(js|vendor)/so dependency churn and per-extension build output never trigger a rebuild.npm run buildis dispatched frombuild_extensions()because the parentnpm run builddoesn't recurse intoextensions/. Failures are tolerated so one broken extension can't take down the watcher.build_and_syncin|| echo …so a single failed build doesn't kill the long-running watcher.Test plan
bin/sync-to-wp-develop.shpopulates the destination with the include set and nothing else.src/triggers a rebuild + rsync in--watchmode.extensions/desktop-mode-code-editor/src/triggers the extension's ownnpm run buildand ships the newextensions/desktop-mode-code-editor/assets/js/code-editor*.jsto the destination.npm installinside an extension does not trigger an infinite rebuild loop (node_modules excluded from fswatch).extensions/desktop-mode-code-editor/does not containnode_modules/,src/,vite.config.js,tsconfig.json,package.json, orpackage-lock.jsonafter a sync.