Fix JIT compiler, CSSX browser support, double-fetch, SPA layout

JIT compiler:
- Fix jit_compile_lambda: resolve `compile` via symbol lookup in env
  instead of embedding VmClosure in AST (CEK dispatches differently)
- Register eval-defcomp/eval-defisland/eval-defmacro runtime helpers
  in browser kernel for bytecoded defcomp forms
- Disable broken .sxbc.json path (missing arity in nested code blocks),
  use .sxbc text format only
- Mark JIT-failed closures as sentinel to stop retrying

CSSX in browser:
- Add cssx.sx symlink + cssx.sxbc to browser web stack
- Add flush-cssx! to orchestration.sx post-swap for SPA nav
- Add cssx.sx to compile-modules.js and mcp_tree.ml bytecode lists

SPA navigation:
- Fix double-fetch: check e.defaultPrevented in click delegation
  (bind-event already handled the click)
- Fix layout destruction: change nav links from outerHTML to innerHTML
  swap (outerHTML destroyed #main-panel when response lacked it)
- Guard JS popstate handler when SX engine is booted
- Rename sx-platform.js → sx-platform-2.js to bust immutable cache

Playwright tests:
- Add trackErrors() helper to all test specs
- Add SPA DOM comparison test (SPA nav vs fresh load)
- Add single-fetch + no-duplicate-elements test
- Improve MCP tool output: show failure details and error messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-30 20:48:43 +00:00
parent 5b55b75a9a
commit d81a518732
37 changed files with 688 additions and 405 deletions

View File

@@ -1,180 +1,189 @@
;; SX docs layout defcomps + in-page navigation.
;; Layout = root header only. Nav is in-page via ~layouts/doc wrapper.
;; ---------------------------------------------------------------------------
;; Nav components — logo header, sibling arrows, children links
;; ---------------------------------------------------------------------------
;; Styling via cssx-style utility tokens (cssx.sx) — same format as ~cssx/tw
;; Logo + tagline + copyright — always shown at top of page area.
;; The header itself is an island so the "reactive" word can cycle colours
;; on click — demonstrates inline signals without a separate component.
;;
;; Lakes (server-morphable slots) wrap the static content: logo and copyright.
;; The server can update these during navigation morphs without disturbing
;; the reactive colour-cycling state. This is Level 2-3: the water (server
;; content) flows through the island, around the rocks (reactive signals).
(defisland ~layouts/header (&key path)
(let ((families (list "violet" "rose" "blue" "emerald" "amber" "cyan" "red" "teal" "pink" "indigo"))
(store (if (client?) (def-store "header-color" (fn () {:idx (signal 0) :shade (signal 500)})) nil))
(idx (if store (get store "idx") (signal 0)))
(shade (if store (get store "shade") (signal 500)))
(current-family (computed (fn ()
(nth families (mod (deref idx) (len families)))))))
(div (~cssx/tw :tokens "block max-w-3xl mx-auto px-4 pt-8 pb-4 text-center")
;; Logo — only this navigates home
(a :href "/sx/"
:sx-get "/sx/" :sx-target "#main-panel" :sx-select "#main-panel"
:sx-swap "outerHTML" :sx-push-url "true"
(defisland
~layouts/header
(&key path)
(let
((families (list "violet" "rose" "blue" "emerald" "amber" "cyan" "red" "teal" "pink" "indigo"))
(store
(if (client?) (def-store "header-color" (fn () {:idx (signal 0) :shade (signal 500)})) nil))
(idx (if store (get store "idx") (signal 0)))
(shade (if store (get store "shade") (signal 500)))
(current-family
(computed (fn () (nth families (mod (deref idx) (len families)))))))
(div
(~cssx/tw :tokens "block max-w-3xl mx-auto px-4 pt-8 pb-4 text-center")
(a
:href "/sx/"
:sx-get "/sx/"
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-swap "innerHTML"
:sx-push-url "true"
(~cssx/tw :tokens "block no-underline")
(lake :id "logo"
(span (~cssx/tw :tokens "block mb-2 text-violet-699 text-4xl font-bold font-mono")
(lake
:id "logo"
(span
(~cssx/tw
:tokens "block mb-2 text-violet-699 text-4xl font-bold font-mono")
"(<sx>)")))
;; Tagline — clicking "reactive" cycles colour.
(p (~cssx/tw :tokens "mb-1 text-stone-500 text-lg")
(p
(~cssx/tw :tokens "mb-1 text-stone-500 text-lg")
"The framework-free "
(span
(~cssx/tw :tokens "font-bold")
:style (str "color:" (colour (deref current-family) (deref shade)) ";"
"cursor:pointer;transition:color 0.3s,font-weight 0.3s;")
:on-click (fn (e)
(batch (fn ()
(swap! idx inc)
(reset! shade (+ 400 (* (mod (* (deref idx) 137) 5) 50))))))
:style (str
"color:"
(colour (deref current-family) (deref shade))
";"
"cursor:pointer;transition:color 0.3s,font-weight 0.3s;")
:on-click (fn
(e)
(batch
(fn
()
(swap! idx inc)
(reset! shade (+ 400 (* (mod (* (deref idx) 137) 5) 50))))))
"reactive")
" hypermedium")
;; Lake: server morphs copyright on navigation without disturbing signals.
(lake :id "copyright"
(p (~cssx/tw :tokens "text-stone-400 text-xs")
(lake
:id "copyright"
(p
(~cssx/tw :tokens "text-stone-400 text-xs")
"© Giles Bradshaw 2026"
(when path
(span (~cssx/tw :tokens "text-stone-300 text-xs") :style "margin-left:0.5em;"
(when
path
(span
(~cssx/tw :tokens "text-stone-300 text-xs")
:style "margin-left:0.5em;"
(str "· " path))))))))
;; @css grid grid-cols-3
;; Current section with prev/next siblings.
;; 3-column grid: prev is right-aligned, current centered, next left-aligned.
;; Current page is larger in the leaf (bottom) row.
(defcomp ~layouts/nav-sibling-row (&key node siblings is-leaf level depth) :affinity :server
(let* ((sibs (or siblings (list)))
(count (len sibs))
;; opacity = (n/x * 3/4) + 1/4
(row-opacity (if (and level depth (> depth 0))
(+ (* (/ level depth) 0.75) 0.25)
1)))
(when (> count 0)
(let* ((idx (find-nav-index sibs node))
(prev-idx (mod (+ (- idx 1) count) count))
(next-idx (mod (+ idx 1) count))
(prev-node (nth sibs prev-idx))
(next-node (nth sibs next-idx)))
(div :class "w-full max-w-3xl mx-auto px-4 py-2 grid grid-cols-3 items-center"
:style (str "opacity:" row-opacity ";transition:opacity 0.3s;")
(a :href (get prev-node "href")
:sx-get (get prev-node "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "text-right min-w-0 truncate"
:style (tw "text-stone-500 text-sm")
(str "\u2190 " (get prev-node "label")))
(a :href (get node "href")
:sx-get (get node "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "text-center min-w-0 truncate px-1"
:style (if is-leaf
(tw "text-violet-700 text-2xl font-bold")
(tw "text-violet-700 text-lg font-semibold"))
(defcomp
~layouts/nav-sibling-row
(&key node siblings is-leaf level depth)
:affinity :server
(let*
((sibs (or siblings (list)))
(count (len sibs))
(row-opacity
(if
(and level depth (> depth 0))
(+ (* (/ level depth) 0.75) 0.25)
1)))
(when
(> count 0)
(let*
((idx (find-nav-index sibs node))
(prev-idx (mod (+ (- idx 1) count) count))
(next-idx (mod (+ idx 1) count))
(prev-node (nth sibs prev-idx))
(next-node (nth sibs next-idx)))
(div
:class "w-full max-w-3xl mx-auto px-4 py-2 grid grid-cols-3 items-center"
:style (str "opacity:" row-opacity ";transition:opacity 0.3s;")
(a
:href (get prev-node "href")
:sx-get (get prev-node "href")
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-swap "innerHTML"
:sx-push-url "true"
:class "text-right min-w-0 truncate"
:style (tw "text-stone-500 text-sm")
(str "← " (get prev-node "label")))
(a
:href (get node "href")
:sx-get (get node "href")
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-swap "innerHTML"
:sx-push-url "true"
:class "text-center min-w-0 truncate px-1"
:style (if
is-leaf
(tw "text-violet-700 text-2xl font-bold")
(tw "text-violet-700 text-lg font-semibold"))
(get node "label"))
(a :href (get next-node "href")
:sx-get (get next-node "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "text-left min-w-0 truncate"
:style (tw "text-stone-500 text-sm")
(str (get next-node "label") " \u2192")))))))
(a
:href (get next-node "href")
:sx-get (get next-node "href")
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-swap "innerHTML"
:sx-push-url "true"
:class "text-left min-w-0 truncate"
:style (tw "text-stone-500 text-sm")
(str (get next-node "label") " →")))))))
;; Children links — shown as clearly clickable buttons.
(defcomp ~layouts/nav-children (&key items) :affinity :server
(div :class "max-w-3xl mx-auto px-4 py-3"
(div :class "flex flex-wrap justify-center gap-2"
(map (fn (item)
(a :href (get item "href")
:sx-get (get item "href") :sx-target "#main-panel"
:sx-select "#main-panel" :sx-swap "outerHTML"
:sx-push-url "true"
:class "px-3 py-1.5 rounded border transition-colors"
:style (tw "text-violet-700 text-sm border-violet-200")
(get item "label")))
(defcomp
~layouts/nav-children
(&key items)
:affinity :server
(div
:class "max-w-3xl mx-auto px-4 py-3"
(div
:class "flex flex-wrap justify-center gap-2"
(map
(fn
(item)
(a
:href (get item "href")
:sx-get (get item "href")
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-swap "innerHTML"
:sx-push-url "true"
:class "px-3 py-1.5 rounded border transition-colors"
:style (tw "text-violet-700 text-sm border-violet-200")
(get item "label")))
items))))
;; ---------------------------------------------------------------------------
;; ~layouts/doc — in-page content wrapper with nav
;; Used by every defpage :content to embed nav inside the page content area.
;; ---------------------------------------------------------------------------
(defcomp ~layouts/doc (&key path &rest children) :affinity :server
(let* ((nav-state (resolve-nav-path sx-nav-tree (or path "/")))
(trail (or (get nav-state "trail") (list)))
(trail-len (len trail))
;; Total nav levels: logo (1) + trail rows
(depth (+ trail-len 1)))
(defcomp
~layouts/doc
(&key path &rest children)
:affinity :server
(let*
((nav-state (resolve-nav-path sx-nav-tree (or path "/")))
(trail (or (get nav-state "trail") (list)))
(trail-len (len trail))
(depth (+ trail-len 1)))
(<>
(div :id "sx-nav" :class "mb-6"
;; Logo opacity = (1/depth * 3/4) + 1/4
;; Wrapper is outside the island so the server morphs it directly
(div :id "logo-opacity"
:style (str "opacity:" (+ (* (/ 1 depth) 0.75) 0.25) ";"
"transition:opacity 0.3s;")
(div
:id "sx-nav"
:class "mb-6"
(div
:id "logo-opacity"
:style (str
"opacity:"
(+ (* (/ 1 depth) 0.75) 0.25)
";"
"transition:opacity 0.3s;")
(~layouts/header :path (or path "/")))
;; Sibling arrows for EVERY level in the trail
;; Trail row i is level (i+2) of depth — opacity = (i+2)/depth
;; Last row (leaf) gets is-leaf for larger current page title
(map-indexed (fn (i crumb)
(~layouts/nav-sibling-row
:node (get crumb "node")
:siblings (get crumb "siblings")
:is-leaf (= i (- trail-len 1))
:level (+ i 2)
:depth depth))
(map-indexed
(fn
(i crumb)
(~layouts/nav-sibling-row
:node (get crumb "node")
:siblings (get crumb "siblings")
:is-leaf (= i (- trail-len 1))
:level (+ i 2)
:depth depth))
trail)
;; Children as button links
(when (get nav-state "children")
(when
(get nav-state "children")
(~layouts/nav-children :items (get nav-state "children"))))
;; Page content
children
;; Flush CSSX rules after all content has rendered
(~cssx/flush))))
;; ---------------------------------------------------------------------------
;; SX docs layouts — root header only (nav is in page content via ~layouts/doc)
;; ---------------------------------------------------------------------------
(defcomp ~layouts/docs-layout-full () nil)
(defcomp ~layouts/docs-layout-full ()
nil)
(defcomp ~layouts/docs-layout-oob () nil)
(defcomp ~layouts/docs-layout-oob ()
nil)
(defcomp ~layouts/docs-layout-mobile () nil)
(defcomp ~layouts/docs-layout-mobile ()
nil)
(defcomp ~layouts/standalone-docs-layout-full () nil)
;; ---------------------------------------------------------------------------
;; Standalone layouts (no root header — for sx-web.org)
;; ---------------------------------------------------------------------------
(defcomp ~layouts/standalone-docs-layout-full ()
nil)
;; Standalone OOB: delegate to shared layout for proper OOB swap structure.
;; The content (with nav + header island) goes into #main-panel via sx-swap-oob.
;; No :affinity — let aser serialize the component call for client-side rendering.
(defcomp ~layouts/standalone-docs-layout-oob (&key content)
(defcomp
~layouts/standalone-docs-layout-oob
(&key content)
(~shared:layout/oob-sx :content content))
;; Standalone mobile: nothing — nav is in content.
(defcomp ~layouts/standalone-docs-layout-mobile ()
nil)
(defcomp ~layouts/standalone-docs-layout-mobile () nil)