Skip to content

[wasm-posix-kernel] WordPress Playground Web under wasm-posix-kernel - Proof of concept#3633

Closed
mho22 wants to merge 8 commits into
WordPress:playground-cli-experimental-posix-kernelfrom
mho22:playground-web-experimental-posix-kernel
Closed

[wasm-posix-kernel] WordPress Playground Web under wasm-posix-kernel - Proof of concept#3633
mho22 wants to merge 8 commits into
WordPress:playground-cli-experimental-posix-kernelfrom
mho22:playground-web-experimental-posix-kernel

Conversation

@mho22
Copy link
Copy Markdown
Collaborator

@mho22 mho22 commented May 13, 2026

Summary

Adds an experimental dev:experimental-posix-kernel mode to the
Playground website that boots WordPress in an iframe under
nginx + PHP-FPM running on
wasm-posix-kernel,
instead of the existing PHP.wasm Asyncify/JSPI runtime.

This PR is stacked on top of #3604 (the CLI-side proof of
concept). The first 15 commits in this PR belong to that PR; the
last 7 are the new web work — see "Commits" below. Like #3604,
this is for visibility / posterity, not ready to merge: it
depends on the same five upstream-pending kernel fixes tracked
in mho22/wasm-posix-kernel#50.

The constraint while implementing was additive only — every
change is a new file under packages/playground/remote/ or
packages/playground/website/, plus three minimal sibling edits
(package.json script, two project.json targets, one new vite
config per package). The classic npm run dev path is untouched
and continues to boot Playground exactly as before.

What this adds

A new npm run dev:experimental-posix-kernel script that:

  1. Boots the website at http://127.0.0.1:5400/website-server/
    with COEP credentialless + COOP same-origin overlaid on
    the parent document, so an embedded iframe can be
    cross-origin isolated and use SharedArrayBuffer.
  2. Serves the remote iframe at :4400 with COEP require-corp,
    COOP same-origin, DIP isolate-and-require-corp,
    Service-Worker-Allowed: /, and aliases /remote.html to a
    sibling /remote-posix-kernel.html entry.
  3. From inside the iframe, spawns a Comlink worker that:
    • downloads WordPress + the SQLite integration through the
      existing CORS proxy (allowlist broadened to
      wordpress.org / downloads.w.org / GitHub),
    • builds a SAB-backed VFS image (binaries, /etc files, nginx
      • php-fpm configs, fpm-router, wp-config.php,
        wasm-optimizations mu-plugin, dinit service tree, streaming
        zip extraction),
    • boots BrowserKernel + HttpBridgeHost, polls
      waitForNginx until any HTTP status comes back,
    • runs the WordPress install over HTTP POST against the
      in-kernel server,
    • exposes LimitedPHPApi via a KernelLimitedPHPApi shim so
      existing v1 blueprint steps work against the
      kernel-resident WordPress.
  4. Forwards every iframe HTTP request to the in-kernel nginx via
    requestStreamed (scope strip → origin-form → Host +
    x-playground-absolute-url injection → COEP/COOP/CORP header
    injection on the response).

Demo:

WASM_POSIX_KERNEL_DIR=/path/to/wasm-posix-kernel \
  npm run dev:experimental-posix-kernel
# Open http://127.0.0.1:5400/website-server/ in incognito Chrome

Commits (web port)

Only the last 7 commits are new in this PR — the first 15 are
inherited from #3604:

  1. 993af91 Experimental posix-kernel (web): nginx + PHP-FPM VFS image
  2. bd5e063 Experimental posix-kernel (web): WordPress preparation
  3. 869845e Experimental posix-kernel (web): kernel boot orchestrator
  4. 57edf38 Experimental posix-kernel (web): KernelLimitedPHPApi shim
  5. ad93171 Experimental posix-kernel (web): Comlink worker endpoint
  6. f2b71de Experimental posix-kernel (web): iframe boot + entry HTML
  7. 5abc1c4 Experimental posix-kernel (web): Vite configs + nx target

Why this is experimental

  • Stacked on [wasm-posix-kernel] WordPress Playground CLI under wasm-posix-kernel - Proof of concept #3604 — which itself depends on five
    unupstreamed kernel-side fixes (PHP-FPM stack-size, kernel
    wakeBlockedPoll snapshot, host chown error swallowing,
    node-kernel-worker-entry bundle, PHP wasm size optimization)
    tracked in mho22/wasm-posix-kernel#50.
  • Submodule pinned to a DO NOT MERGE - CI artifact branch
    in wasm-posix-kernel because release-tag artifacts aren't
    fetched by the in-tree workflow yet. The submodule needs its
    own upstreaming before this PR is mergeable.
  • Auto-login mu-plugin not yet ported. Classic Playground's
    login blueprint step relies on a cookie-setting mu-plugin in
    packages/playground/wordpress/src/index.ts. Kernel mode has
    KernelLimitedPHPApi.defineConstant + a
    playground-defines.php mu-plugin to inject
    PLAYGROUND_AUTO_LOGIN_AS_USER, but the cookie-setting half
    isn't wired. A defensive goTo unwrap in
    boot-playground-remote.ts short-circuits the V1
    redirection-handler URL in the meantime; once auto-login lands
    the unwrap must be removed.
  • Normal-mode Chrome serves the WordPress zip truncated from
    the HTTP cache
    (keyed to downloads.w.org / GitHub, outside
    our origin, so site-data clears don't touch it). DevTools →
    "Disable cache" boots cleanly. Incognito is the workaround for
    now.
  • Production / build / preview paths are out of scope. The
    new vite config covers dev only; the website's existing build
    pipeline is untouched.

Notable design choices

  • Cross-origin isolation — three headers on the iframe, two
    on the parent.
    SAB requires COI; for an embedded iframe to
    be COI'd both the iframe response and the parent document
    must opt in. The parent uses COEP credentialless so
    cross-origin sub-resources (analytics, Octokit) keep loading
    without CORP. WordPress front-end responses don't ship COEP
    themselves (only wp_set_up_cross_origin_isolation on
    wp-admin pages), so requestStreamed injects COEP/COOP/CORP
    on every bridge response before constructing the PHPResponse.
  • x-playground-absolute-url on every bridge request.
    wp-config reads the header and uses it as WP_HOME /
    WP_SITEURL — avoids a boot-time substitution pass. nginx
    doesn't auto-forward arbitrary headers to fastcgi, so both
    location blocks in the in-VFS nginx config explicitly pass
    it through.
  • Origin-form URL on the bridge. requestStreamed strips
    the scope and reduces to origin-form before posting to the
    bridge. Absolute-URI form hangs nginx — RFC 7230 §5.3.2 allows
    it on origin servers but the kernel-resident nginx doesn't.
  • /cors-proxy.php? instead of /cors-proxy/?.
    offline-mode-cache.ts:shouldCacheUrl returns true for
    cors-proxy URLs (same-origin in dev), routing them through
    cacheFirstFetch which truncates around 19 MiB on HTTP
    origins. Pathnames ending in .php short-circuit the cache,
    so the .php? form bypasses it; the vite cors-proxy prefix
    still forwards because the rewrite no-ops for .php? paths.
  • Late-binding LimitedPHPApi over Comlink. The worker
    endpoint installs throwing stubs for every name in
    LIMITED_PHP_API_METHODS at construction; doBoot constructs
    a KernelLimitedPHPApi and rebinds those names via
    bindApiMethods. Comlink resolves method paths at
    message-receive time, so post-boot rebinding is visible to the
    iframe immediately.
  • waitForNginx polls because dinit returns before services
    bind.
    kernel.boot() resolves as soon as the kernel itself
    is ready, not when dinit's child services have bound their
    sockets. The boot orchestrator polls GET / until any HTTP
    status comes back (404 counts — nginx is up).
  • Per-spawn output capture via single-slot hook.
    BrowserKernel's onStdout / onStderr are constructor-time
    singletons (no per-pid routing). The boot installs a
    single-slot activeCapture handler that the spawn adapter
    swaps in before each kernel.spawn and clears in finally;
    the adapter's inFlight promise serializes spawns so the
    global slot is unambiguous.

Open follow-ups (deliberately out of scope)

  1. Wire auto-login (port the cookie-setting mu-plugin from
    @wp-playground/wordpress, then remove the goTo unwrap in
    boot-playground-remote.ts).
  2. Normal-mode Chrome zip-cache truncation — either accept
    "incognito-only for dev" or apply cache: 'no-store' on
    fetchZipBytes.
  3. Build / preview / production bundling. The new vite
    config covers dev only. Production paths need a separate pass
    once the kernel artifacts are npm-installable.
  4. Replace the wasm-posix-kernel submodule with an
    npm-installable package
    — same blocker as [wasm-posix-kernel] WordPress Playground CLI under wasm-posix-kernel - Proof of concept #3604.

Test plan

The 25 existing playground-remote unit tests continue to pass:

npx nx test playground-remote

Manual boot in incognito Chrome:

WASM_POSIX_KERNEL_DIR=/path/to/wasm-posix-kernel \
  npm run dev:experimental-posix-kernel
# Open http://127.0.0.1:5400/website-server/

The iframe should render the WordPress front page end-to-end.

🤖 Generated with Claude Code

mho22 added 7 commits May 13, 2026 17:13
Build the SAB-backed VFS image consumed by the kernel worker:
binaries, /etc files, nginx + php-fpm configs, fpm-router,
wp-config.php, wasm-optimizations mu-plugin, dinit service tree,
streaming zip extraction. Co-locates host-bridge re-exports from
the wasm-posix-kernel submodule and the playground-defines.php
mu-plugin that backs KernelLimitedPHPApi.defineConstant.
Download WordPress + SQLite zips through the playground CORS
proxy. Broadens the proxy allowlist to wordpress.org,
downloads.w.org and the GitHub release host so zip fetches
succeed under the iframe's cross-origin isolation regime.
Construct BrowserKernel + HttpBridgeHost from the prebuilt VFS
image, inject the Host header on every bridge request (nginx
returns 400 without one), poll waitForNginx until any HTTP
status comes back (dinit returns before its children bind their
sockets), and bump nextPid past the kernel-reserved range to
avoid an EEXIST on the first host-side spawn.
KernelSpawnAdapter wraps kernel.spawn(coreutils|php, ...) behind
a natural FS facade, serializing spawns through inFlight because
BrowserKernel's onStdout/onStderr are constructor-time singletons
with no per-pid routing. KernelLimitedPHPApi mirrors the CLI's
shape (mkdir/writeFile/run/request/defineConstant + cookie jar),
backed by the spawn adapter and the bridge sendRequest.
KernelPlaygroundWorkerEndpoint orchestrates boot
(prepareWordPressZips → buildVfsImage → bootKernelWordPress →
ensureWordPressInstalled), forwards every iframe HTTP request to
the in-kernel nginx via requestStreamed (scope strip → origin-
form → Host + x-playground-absolute-url injection → COEP/COOP/
CORP injection), and exposes LimitedPHPApi over Comlink via the
late-binding stubs pattern so the iframe sees methods the moment
KernelLimitedPHPApi is ready.
bootPlaygroundRemote registers the existing service worker,
spawns the Comlink worker endpoint and exposes playgroundApi
with progress/nav/goTo and URL helpers — mirroring the classic
remote's iframe-side surface. remote-posix-kernel.html is the
sibling entry the vite middleware aliases /remote.html to.
vite.posix-kernel.config.ts (remote) aliases the kernel binary
URLs, rewrites /remote.html to /remote-posix-kernel.html, and
serves the iframe with COEP require-corp / COOP same-origin /
DIP / Service-Worker-Allowed so SharedArrayBuffer is available
to the worker. The website wrapper config overlays COEP
credentialless + COOP same-origin on the parent document so the
iframe can be cross-origin isolated. Wired through nx targets
and a `dev:experimental-posix-kernel` script — additive only,
the classic `npm run dev` path is untouched.
- Switch submodule re-exports in `host-bridge.ts` / `vfs-builder.ts`
  to a `@wasm-posix-kernel/*` alias so TS doesn't descend into the
  submodule source (strict tsconfig vs. submodule's looser config).
- Vite alias in `vite.posix-kernel.config.ts` resolves the same
  specifiers at runtime against `WASM_POSIX_KERNEL_DIR` or the
  bundled submodule.
- New `wasm-posix-kernel.d.ts` declares opaque `any` shims for the
  alias plus Vite asset-suffix wildcards (`?url`, `?raw`,
  `?worker&url`) and the growable `SharedArrayBuffer` 2-arg form.
- Drop now-unneeded `@ts-ignore` / `eslint-disable` comments at the
  kernel-mode import sites.
- Replace the `Function` cast in `boot-playground-remote.ts` with an
  `as any` indexer; the dynamically-bound Comlink methods aren't in
  `keyof KernelPlaygroundWorkerEndpoint`.
- Annotate `BrowserKernelOptions` callback params locally in `boot.ts`
  since the alias surface is `any`.
@mho22 mho22 changed the base branch from trunk to playground-cli-experimental-posix-kernel May 14, 2026 09:00
@mho22
Copy link
Copy Markdown
Collaborator Author

mho22 commented May 14, 2026

Closing if favor of #3635

@mho22 mho22 closed this May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant