SxExpr aser wire format fix + Playwright test infrastructure + blob protocol
Aser serialization: aser-call/fragment now return SxExpr instead of String. serialize/inspect passes SxExpr through unquoted, preventing the double- escaping (\" → \\\" ) that broke client-side parsing when aser wire format was output via raw! into <script> tags. Added make-sx-expr + sx-expr-source primitives to OCaml and JS hosts. Binary blob protocol: eval, aser, aser-slot, and sx-page-full now send SX source as length-prefixed blobs instead of escaped strings. Eliminates pipe desync from concurrent requests and removes all string-escape round-trips between Python and OCaml. Bridge safety: re-entrancy guard (_in_io_handler) raises immediately if an IO handler tries to call the bridge, preventing silent deadlocks. Fetch error logging: orchestration.sx error callback now logs method + URL via log-warn. Platform catches (fetchAndRestore, fetchPreload, bindBoostForm) also log errors instead of silently swallowing them. Transpiler fixes: makeEnv, scopePeek, scopeEmit, makeSxExpr added as platform function definitions + transpiler mappings — were referenced in transpiled code but never defined as JS functions. Playwright test infrastructure: - nav() captures JS errors and fails fast with the actual error message - Checks for [object Object] rendering artifacts - New tests: delete-row interaction, full page refresh, back button, direct load with fresh context, code block content verification - Default base URL changed to localhost:8013 (standalone dev server) - docker-compose.dev-sx.yml: port 8013 exposed for local testing - test-sx-build.sh: build + unit tests + Playwright smoke tests Geography content: index page component written (sx/sx/geography/index.sx) describing OCaml evaluator, wire formats, rendering pipeline, and topic links. Wiring blocked by aser-expand-component children passing issue. Tests: 1080/1080 JS, 952/952 OCaml, 66/66 Playwright Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,11 +14,12 @@
|
||||
(define render-to-sx :effects [render]
|
||||
(fn (expr (env :as dict))
|
||||
(let ((result (aser expr env)))
|
||||
;; aser-call already returns serialized SX strings;
|
||||
;; only serialize non-string values
|
||||
(if (= (type-of result) "string")
|
||||
result
|
||||
(serialize result)))))
|
||||
;; aser-call returns SxExpr which serialize passes through unquoted.
|
||||
;; Plain strings from data need serialization (quoting).
|
||||
(cond
|
||||
(= (type-of result) "sx-expr") (sx-expr-source result)
|
||||
(= (type-of result) "string") result
|
||||
:else (serialize result)))))
|
||||
|
||||
(define aser :effects [render]
|
||||
(fn ((expr :as any) (env :as dict))
|
||||
@@ -143,21 +144,15 @@
|
||||
(let ((result (aser c env)))
|
||||
(cond
|
||||
(nil? result) nil
|
||||
;; Serialized SX from aser (tags, components, fragments)
|
||||
;; starts with "(" — use directly without re-quoting
|
||||
(and (= (type-of result) "string")
|
||||
(> (string-length result) 0)
|
||||
(starts-with? result "("))
|
||||
(append! parts result)
|
||||
(= (type-of result) "sx-expr")
|
||||
(append! parts (sx-expr-source result))
|
||||
;; list results (from map etc.)
|
||||
(= (type-of result) "list")
|
||||
(for-each
|
||||
(fn (item)
|
||||
(when (not (nil? item))
|
||||
(if (and (= (type-of item) "string")
|
||||
(> (string-length item) 0)
|
||||
(starts-with? item "("))
|
||||
(append! parts item)
|
||||
(if (= (type-of item) "sx-expr")
|
||||
(append! parts (sx-expr-source item))
|
||||
(append! parts (serialize item)))))
|
||||
result)
|
||||
;; Everything else — serialize normally (quotes strings)
|
||||
@@ -167,8 +162,8 @@
|
||||
(if (empty? parts)
|
||||
""
|
||||
(if (= (len parts) 1)
|
||||
(first parts)
|
||||
(str "(<> " (join " " parts) ")"))))))
|
||||
(make-sx-expr (first parts))
|
||||
(make-sx-expr (str "(<> " (join " " parts) ")")))))))
|
||||
|
||||
|
||||
(define aser-call :effects [render]
|
||||
@@ -193,33 +188,23 @@
|
||||
(let ((val (aser (nth args (inc i)) env)))
|
||||
(when (not (nil? val))
|
||||
(append! attr-parts (str ":" (keyword-name arg)))
|
||||
;; 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)
|
||||
(if (= (type-of val) "sx-expr")
|
||||
(append! attr-parts (sx-expr-source val))
|
||||
(append! attr-parts (serialize val))))
|
||||
(set! skip true)
|
||||
(set! i (inc i)))
|
||||
(let ((val (aser arg env)))
|
||||
(when (not (nil? val))
|
||||
(cond
|
||||
;; Serialized SX (tags, components) — use directly
|
||||
(and (= (type-of val) "string")
|
||||
(> (string-length val) 0)
|
||||
(starts-with? val "("))
|
||||
(append! child-parts val)
|
||||
(= (type-of val) "sx-expr")
|
||||
(append! child-parts (sx-expr-source val))
|
||||
;; List results (from map etc.)
|
||||
(= (type-of val) "list")
|
||||
(for-each
|
||||
(fn (item)
|
||||
(when (not (nil? item))
|
||||
(if (and (= (type-of item) "string")
|
||||
(> (string-length item) 0)
|
||||
(starts-with? item "("))
|
||||
(append! child-parts item)
|
||||
(if (= (type-of item) "sx-expr")
|
||||
(append! child-parts (sx-expr-source item))
|
||||
(append! child-parts (serialize item)))))
|
||||
val)
|
||||
;; Plain values — serialize normally
|
||||
@@ -239,7 +224,7 @@
|
||||
(scope-peek "element-attrs"))
|
||||
(scope-pop! "element-attrs")
|
||||
(let ((parts (concat (list name) attr-parts child-parts)))
|
||||
(str "(" (join " " parts) ")")))))
|
||||
(make-sx-expr (str "(" (join " " parts) ")"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -272,6 +257,7 @@
|
||||
(if (and (= (type-of arg) "keyword")
|
||||
(< (inc i) (len args)))
|
||||
;; Keyword arg: bind name = aser'd next arg
|
||||
;; SxExpr values pass through serialize unquoted automatically
|
||||
(do
|
||||
(env-bind! local (keyword-name arg)
|
||||
(aser (nth args (inc i)) env))
|
||||
|
||||
@@ -197,6 +197,7 @@
|
||||
(clear-loading-state el indicator disabled-elts)
|
||||
(revert-optimistic optimistic-state)
|
||||
(when (not (abort-error? err))
|
||||
(log-warn (str "sx:fetch error " method " " final-url " — " err))
|
||||
(dom-dispatch el "sx:requestError"
|
||||
(dict "error" err))))))))))))
|
||||
|
||||
|
||||
@@ -199,7 +199,16 @@
|
||||
(deftest "component with children serializes unexpanded"
|
||||
(assert-equal "(~box (p \"inside\"))"
|
||||
(render-sx "(do (defcomp ~box (&key &rest children) (div children))
|
||||
(~box (p \"inside\")))"))))
|
||||
(~box (p \"inside\")))")))
|
||||
|
||||
(deftest "string keyword arg starting with paren is quoted"
|
||||
(assert-equal "(~info :text \"(hello world)\")"
|
||||
(render-sx "(do (defcomp ~info (&key text) (code text))
|
||||
(~info :text \"(hello world)\"))")))
|
||||
|
||||
(deftest "string child starting with paren is quoted"
|
||||
(assert-equal "(p \"(not code)\")"
|
||||
(render-sx "(let ((x \"(not code)\")) (p x))"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user