Fix aser server-affinity expansion: keyword values, OOB wrapper, page helpers

Three bugs in aser-expand-component (adapter-sx.sx):
- Keyword values were eval'd (eval-expr can't handle <>, HTML tags);
  now asered, matching the aser's rendering capabilities
- Missing default nil binding for unset &key params (caused
  "Undefined symbol" errors for optional params like header-rows)
- aserCall string-quoted keyword values that were already serialized
  SX — now inlines values starting with "(" directly

Server-affinity annotations for layout/nav shells:
- ~shared:layout/app-body, ~shared:layout/oob-sx — page structure
- ~layouts/nav-sibling-row, ~layouts/nav-children — server-side data
- ~layouts/doc already had :affinity :server
- ~cssx/flush marked :affinity :client (browser-only state)

Navigation fix: restore oob_page_sx wrapper for HTMX responses
so #main-panel section exists for sx-select/sx-swap targeting.

OCaml bridge: lazy page helper injection into kernel via IO proxy
(define name (fn (...) (helper "name" ...))) — enables aser_slot
to evaluate highlight/component-source etc. via coroutine bridge.

Playwright tests: added pageerror listener to test_no_console_errors,
new test_navigate_from_home_to_geography for HTMX nav regression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 12:06:24 +00:00
parent 171c18d3be
commit 109ca7c70b
10 changed files with 201 additions and 31 deletions

View File

@@ -71,13 +71,30 @@
(= name "<>")
(aser-fragment args env)
;; Component call — expand server-affinity, serialize others
;; raw! — pass through as serialized call
(= name "raw!")
(aser-call "raw!" args env)
;; Component call — expand if server-affinity or expand-components? is set.
;; expand-components? is a platform primitive (like eval-expr, trampoline);
;; adapter-async.sx uses the same pattern at line 684.
;; Guard with env-has? for backward compat with older kernels.
(starts-with? name "~")
(let ((comp (if (env-has? env name) (env-get env name) nil)))
(if (and comp (component? comp)
(= (component-affinity comp) "server"))
(aser-expand-component comp args env)
(aser-call name args env)))
(let ((comp (if (env-has? env name) (env-get env name) nil))
(expand-all (if (env-has? env "expand-components?")
(expand-components?) false)))
(cond
(and comp (macro? comp))
(aser (expand-macro comp args env) env)
(and comp (component? comp)
(or expand-all
(= (component-affinity comp) "server"))
;; :affinity :client components are never expanded
;; server-side — they depend on browser-only state.
(not (= (component-affinity comp) "client")))
(aser-expand-component comp args env)
:else
(aser-call name args env)))
;; Lake — serialize (server-morphable slot)
(= name "lake")
@@ -175,7 +192,14 @@
(let ((val (aser (nth args (inc i)) env)))
(when (not (nil? val))
(append! attr-parts (str ":" (keyword-name arg)))
(append! attr-parts (serialize val)))
;; If the aser result is already serialized SX (starts
;; with "("), inline it directly — don't re-serialize
;; which would quote it as a string literal.
(if (and (= (type-of val) "string")
(> (string-length val) 0)
(starts-with? val "("))
(append! attr-parts val)
(append! attr-parts (serialize val))))
(set! skip true)
(set! i (inc i)))
(let ((val (aser arg env)))
@@ -232,19 +256,24 @@
(i 0)
(skip false)
(children (list)))
;; Parse keyword args and positional children from args
;; Keyword values are eval'd (they're data). Children are NOT eval'd
;; (they may contain HTML tags that only the aser can handle).
;; Default all keyword params to nil (same as the CEK evaluator)
(for-each (fn (p) (env-bind! local p nil)) params)
;; Parse keyword args and positional children from args.
;; Keyword values are ASERED (not eval'd) — they may contain
;; rendering constructs (<>, HTML tags) that eval-expr can't
;; handle. The aser result is a string/value that the body's
;; aser will inline correctly (strings starting with "(" are
;; recognized as serialized SX by aserCall).
(for-each
(fn (arg)
(if skip
(do (set! skip false) (set! i (inc i)))
(if (and (= (type-of arg) "keyword")
(< (inc i) (len args)))
;; Keyword arg: bind name = eval'd next arg
;; Keyword arg: bind name = aser'd next arg
(do
(env-bind! local (keyword-name arg)
(trampoline (eval-expr (nth args (inc i)) env)))
(aser (nth args (inc i)) env))
(set! skip true)
(set! i (inc i)))
;; Positional child: keep as unevaluated AST for aser