Skip to content

chore(sync): mirror plugin tree into a sibling wordpress-develop checkout - #74

Closed
epeicher wants to merge 14 commits into
trunkfrom
add/sync-to-wp-develop
Closed

chore(sync): mirror plugin tree into a sibling wordpress-develop checkout#74
epeicher wants to merge 14 commits into
trunkfrom
add/sync-to-wp-develop

Conversation

@epeicher

@epeicher epeicher commented May 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds bin/sync-to-wp-develop.sh + bin/sync-to-wp-develop.includes to mirror the working tree into ~/github/wordpress-develop/src/wp-content/plugins/desktop-mode (override with WPDM_SYNC_DEST).
  • One-shot mode: bin/sync-to-wp-develop.sh runs npm run build, builds each extension that has node_modules/, then rsyncs the include set.
  • Watch mode: bin/sync-to-wp-develop.sh --watch adds an fswatch loop over the source inputs (TS under src/, CSS, PHP, per-extension src/, etc.) — built bundles under assets/js/ and assets/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-extension

The includes manifest references extensions.json, which only exists on this branch (added by 8f162e5 feat(marketplace): add Extensions tab + release pipeline). Targeting trunk would land a broken manifest.

Notable behaviours

  • rsync of extensions/ excludes node_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.
  • fswatch excludes /node_modules/ and /extensions/<name>/assets/(js|vendor)/ so dependency churn and per-extension build output never trigger a rebuild.
  • Per-extension npm run build is dispatched from build_extensions() because the parent npm run build doesn't recurse into extensions/. Failures are tolerated so one broken extension can't take down the watcher.
  • The watch loop wraps build_and_sync in || echo … so a single failed build doesn't kill the long-running watcher.

Test plan

  • bin/sync-to-wp-develop.sh populates the destination with the include set and nothing else.
  • Editing a TS file under src/ triggers a rebuild + rsync in --watch mode.
  • Editing a TS file under extensions/desktop-mode-code-editor/src/ triggers the extension's own npm run build and ships the new extensions/desktop-mode-code-editor/assets/js/code-editor*.js to the destination.
  • Running npm install inside an extension does not trigger an infinite rebuild loop (node_modules excluded from fswatch).
  • Destination's extensions/desktop-mode-code-editor/ does not contain node_modules/, src/, vite.config.js, tsconfig.json, package.json, or package-lock.json after a sync.

epeicher added 14 commits May 1, 2026 10:56
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
@epeicher
epeicher force-pushed the add/code-editor-extension branch from 539cc73 to b25b328 Compare May 4, 2026 09:38
Base automatically changed from add/code-editor-extension to trunk May 4, 2026 09:52
@epeicher

epeicher commented May 4, 2026

Copy link
Copy Markdown
Collaborator Author

The sync scripts will be added as part of a fresh PR, closing this as not required.

@epeicher epeicher closed this May 4, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant