Turn the blog into a SPA using the SX-htmx engine (web/engine.sx) booting the
WASM OCaml kernel (same evaluator as the server) in-browser, with sx-boost
fragment-swapping every link into #content.
Server side DONE + verified:
- lib/host/static.sx: GET /static/** serves shared/static via the file-read
primitive (ctype by ext, traversal-guarded, 404 on missing). Wired into
serve.sh (module + route group). Tested: kernel JS + .wasm binary-exact.
- host/blog--page is now the SPA shell: full page = WASM boot scripts +
sx-boost=#content wrapper + #content; on SX-Request:true returns ONLY the
inner content fragment for the engine to swap. All 13 handlers thread req.
- docker-compose mounts ./shared/static.
- lib/host/playwright/spa-check.{spec.js,run-spa-check.sh}: boot/boost/swap/back.
Client side: the WASM kernel BOOTS (SxKernel object, data-sx-ready=true, web
stack loads). BLOCKER: the bundled .sxbc throw 'VM: unknown opcode 0' vs this
worktree's kernel -> .sx source fallback -> boot.sx source fails 'Expected
list, got string' -> process-boosted never binds links (boosted 0/N). Fix =
rebuild a consistent WASM bundle (recompile .sxbc against the kernel via
scripts/sx-build-all.sh); the browser wasm target isn't built here yet. See
plans/host-spa.md. Live NOT redeployed (stays on pre-SPA process).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
68 lines
3.9 KiB
Markdown
68 lines
3.9 KiB
Markdown
# Host blog → SPA via the SX-htmx engine (WASM OCaml kernel)
|
|
|
|
Turn the blog (lib/host/blog.sx) into a single-page app using the in-repo SX
|
|
hypermedia engine (web/engine.sx — "our htmx"): boot the **WASM OCaml kernel**
|
|
(the same evaluator the server runs) in the browser, and `sx-boost` every
|
|
link/form into a fragment swap into `#content` — no full reloads, history kept,
|
|
graceful degradation to plain server-rendered pages with no JS.
|
|
|
|
## Status
|
|
|
|
**DONE — server side (verified, all green):**
|
|
- `lib/host/static.sx` — `GET /static/**` serves files under `shared/static` via
|
|
the `file-read` primitive (content-type by extension, path-traversal guarded,
|
|
404 on missing). Mounted in serve.sh + the route list. Tested: kernel JS 200 +
|
|
correct ctype + exact bytes; `.wasm` binary-exact with `application/wasm`;
|
|
traversal/missing → 404.
|
|
- `lib/host/blog.sx` `host/blog--page` is now the SPA shell: full page = WASM boot
|
|
scripts (`/static/wasm/sx_browser.bc.wasm.js` + `sx-platform.js`) + a
|
|
`sx-boost="#content"` wrapper div + `#content`. On the `SX-Request: true` header
|
|
(a boosted nav) it returns ONLY the inner content (fragment) so the engine swaps
|
|
it into `#content`. All 13 page handlers thread `req`. Tested: full page carries
|
|
scripts+boost+#content; `SX-Request` returns the bare fragment.
|
|
- `docker-compose.dev-sx-host.yml` mounts `./shared/static` so the live container
|
|
can serve the kernel.
|
|
- `lib/host/playwright/spa-check.spec.js` + `run-spa-check.sh` — browser check
|
|
(boot, boost, fragment swap, back button).
|
|
|
|
**DONE — client side, partial:**
|
|
- The WASM kernel BOOTS in a headless browser: `globalThis.SxKernel` is an object,
|
|
`<html data-sx-ready="true">` is set, the web-stack modules load.
|
|
- Fixed: this worktree's `shared/static/wasm/sx_browser.bc.wasm.assets/` was
|
|
missing 5 of 11 `.wasm` units (`sx-`, `unix-`, `re-`, `start-`,
|
|
`dune__exe__Sx_browser-`); copied the complete set from the main worktree.
|
|
|
|
**BLOCKER — boost does not activate (`boosted links: 0 / N`):**
|
|
- The bundled `.sxbc` bytecode throws `VM: unknown opcode 0` against this
|
|
worktree's `sx_browser.bc.wasm.js` kernel, so sx-platform.js falls back to `.sx`
|
|
source for every web-stack module. Source fallback works for all modules EXCEPT
|
|
`boot.sx`, which then fails with `Expected list, got string` — so the boot
|
|
sequence that wires `process-elements → process-boosted` doesn't complete and no
|
|
link gets `_sxBoundboost`.
|
|
- Root cause: the `.sxbc` in `shared/static/wasm/sx/` are out of sync with the
|
|
WASM kernel (sx.rose-ash.com avoids this because its Docker image ships a
|
|
consistent bundle and it navigates via client-router page-routes, not boost).
|
|
|
|
## Next step — rebuild a consistent WASM bundle
|
|
|
|
`scripts/sx-build-all.sh` does: build the browser wasm target → sync web `.sx`
|
|
into `hosts/ocaml/browser/dist/sx/` → `node hosts/ocaml/browser/compile-modules.js`
|
|
(recompiles `.sxbc` via the native sx_server binary) → copy into
|
|
`shared/static/wasm/`. The browser wasm target is NOT built in this worktree
|
|
(`hosts/ocaml/_build/default/browser/` is empty), so this needs the
|
|
`wasm_of_ocaml` toolchain set up first. Once the `.sxbc` match the kernel, the
|
|
bytecode path loads (no source fallback), `boot.sx` runs, and `process-boosted`
|
|
binds the links — then the SPA Playwright check should pass.
|
|
|
|
Alternatively: build the browser kernel in the main worktree (which has the
|
|
pipeline) and copy a consistent `sx_browser.bc.wasm.js` + assets + `.sxbc` set
|
|
into this worktree's `shared/static/wasm/`.
|
|
|
|
## Deploy note
|
|
|
|
The live container is NOT redeployed with the SPA shell yet — it keeps running the
|
|
pre-SPA `blog.sx` in memory (the native host doesn't hot-reload). Don't recreate
|
|
the container until the bundle is consistent and the SPA Playwright check is green,
|
|
to avoid shipping a kernel that boots but doesn't boost. (Even if it is recreated,
|
|
pages degrade gracefully: links still do normal full-page nav.)
|