Page helpers demo: defisland, map-in-children fix, _eval_slot ref evaluator
- Add page-helpers-demo page with defisland ~demo-client-runner (pure SX, zero JS files) showing spec functions running on both server and client - Fix _aser_component children serialization: flatten list results from map instead of serialize(list) which wraps in parens creating ((div ...) ...) that re-parses as invalid function call. Fixed in adapter-async.sx spec and async_eval_ref.py - Switch _eval_slot to use async_eval_ref.py when SX_USE_REF=1 (was hardcoded to async_eval.py) - Add Island type support to async_eval_ref.py: import, SSR rendering, aser dispatch, thread-first, defisland in _ASER_FORMS - Add server affinity check: components with :affinity :server expand even when _expand_components is False - Add diagnostic _aser_stack context to EvalError messages - New spec files: adapter-async.sx, page-helpers.sx, platform_js.py - Bootstrappers: page-helpers module support, performance.now() timing - 0-arity lambda event handler fix in adapter-dom.sx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -104,3 +104,8 @@
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
(define-page-helper "page-helpers-demo-data"
|
||||
:params ()
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
@@ -227,7 +227,8 @@
|
||||
(dict :label "JavaScript" :href "/bootstrappers/javascript")
|
||||
(dict :label "Python" :href "/bootstrappers/python")
|
||||
(dict :label "Self-Hosting (py.sx)" :href "/bootstrappers/self-hosting")
|
||||
(dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js")))
|
||||
(dict :label "Self-Hosting JS (js.sx)" :href "/bootstrappers/self-hosting-js")
|
||||
(dict :label "Page Helpers" :href "/bootstrappers/page-helpers")))
|
||||
|
||||
;; Spec file registry — canonical metadata for spec viewer pages.
|
||||
;; Python only handles file I/O (read-spec-file); all metadata lives here.
|
||||
|
||||
265
sx/sx/page-helpers-demo.sx
Normal file
265
sx/sx/page-helpers-demo.sx
Normal file
@@ -0,0 +1,265 @@
|
||||
;; page-helpers-demo.sx — Demo: same SX spec functions on server and client
|
||||
;;
|
||||
;; Shows page-helpers.sx functions running on Python (server-side, via sx_ref.py)
|
||||
;; and JavaScript (client-side, via sx-browser.js) with identical results.
|
||||
;; Server renders with render-to-html. Client runs as a defisland — pure SX,
|
||||
;; no JavaScript file. The button click triggers spec functions via signals.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Shared card component — used by both server and client results
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~demo-result-card (&key title ms desc theme &rest children)
|
||||
(let ((border (if (= theme "blue") "border-blue-200 bg-blue-50/30" "border-stone-200"))
|
||||
(title-c (if (= theme "blue") "text-blue-700" "text-stone-700"))
|
||||
(badge-c (if (= theme "blue") "text-blue-400" "text-stone-400"))
|
||||
(desc-c (if (= theme "blue") "text-blue-500" "text-stone-500"))
|
||||
(body-c (if (= theme "blue") "text-blue-600" "text-stone-600")))
|
||||
(div :class (str "rounded-lg border p-4 " border)
|
||||
(h4 :class (str "font-semibold text-sm mb-1 " title-c)
|
||||
title " "
|
||||
(span :class (str "text-xs " badge-c) (str ms "ms")))
|
||||
(p :class (str "text-xs mb-2 " desc-c) desc)
|
||||
(div :class (str "text-xs space-y-0.5 " body-c)
|
||||
children))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Client-side island — runs spec functions in the browser on button click
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defisland ~demo-client-runner (&key sf-source attr-detail req-attrs attr-keys)
|
||||
(let ((results (signal nil))
|
||||
(running (signal false))
|
||||
(run-demo (fn (e)
|
||||
(reset! running true)
|
||||
(let* ((t0 (now-ms))
|
||||
|
||||
;; 1. categorize-special-forms
|
||||
(t1 (now-ms))
|
||||
(sf-exprs (sx-parse sf-source))
|
||||
(sf-result (categorize-special-forms sf-exprs))
|
||||
(sf-ms (- (now-ms) t1))
|
||||
(sf-cats {})
|
||||
(sf-total 0)
|
||||
|
||||
;; 2. build-reference-data
|
||||
(t2 (now-ms))
|
||||
(ref-result (build-reference-data "attributes"
|
||||
{"req-attrs" req-attrs "beh-attrs" (list) "uniq-attrs" (list)}
|
||||
attr-keys))
|
||||
(ref-ms (- (now-ms) t2))
|
||||
(ref-sample (slice (or (get ref-result "req-attrs") (list)) 0 3))
|
||||
|
||||
;; 3. build-attr-detail
|
||||
(t3 (now-ms))
|
||||
(attr-result (build-attr-detail "sx-get" attr-detail))
|
||||
(attr-ms (- (now-ms) t3))
|
||||
|
||||
;; 4. build-component-source
|
||||
(t4 (now-ms))
|
||||
(comp-result (build-component-source
|
||||
{"type" "component" "name" "~demo-card"
|
||||
"params" (list "title" "subtitle")
|
||||
"has-children" true
|
||||
"body-sx" "(div :class \"card\"\n (h2 title)\n (when subtitle (p subtitle))\n children)"
|
||||
"affinity" "auto"}))
|
||||
(comp-ms (- (now-ms) t4))
|
||||
|
||||
;; 5. build-routing-analysis
|
||||
(t5 (now-ms))
|
||||
(routing-result (build-routing-analysis (list
|
||||
{"name" "home" "path" "/" "has-data" false "content-src" "(~home-content)"}
|
||||
{"name" "dashboard" "path" "/dash" "has-data" true "content-src" "(~dashboard)"}
|
||||
{"name" "about" "path" "/about" "has-data" false "content-src" "(~about-content)"}
|
||||
{"name" "settings" "path" "/settings" "has-data" true "content-src" "(~settings)"})))
|
||||
(routing-ms (- (now-ms) t5))
|
||||
|
||||
(total-ms (- (now-ms) t0)))
|
||||
|
||||
;; Post-process sf-result: count forms per category
|
||||
(for-each (fn (k)
|
||||
(let ((count (len (get sf-result k))))
|
||||
(set! sf-cats (assoc sf-cats k count))
|
||||
(set! sf-total (+ sf-total count))))
|
||||
(keys sf-result))
|
||||
|
||||
(reset! results
|
||||
{"sf-cats" sf-cats "sf-total" sf-total "sf-ms" sf-ms
|
||||
"ref-sample" ref-sample "ref-ms" ref-ms
|
||||
"attr-result" attr-result "attr-ms" attr-ms
|
||||
"comp-result" comp-result "comp-ms" comp-ms
|
||||
"routing-result" routing-result "routing-ms" routing-ms
|
||||
"total-ms" total-ms})))))
|
||||
|
||||
(<>
|
||||
(button
|
||||
:class (if (deref running)
|
||||
"px-4 py-2 rounded-md bg-blue-600 text-white font-medium text-sm cursor-default mb-4"
|
||||
"px-4 py-2 rounded-md bg-violet-600 text-white font-medium text-sm hover:bg-violet-700 transition-colors mb-4")
|
||||
:on-click run-demo
|
||||
(if (deref running)
|
||||
(str "Done (" (get (deref results) "total-ms") "ms total)")
|
||||
"Run in Browser"))
|
||||
|
||||
(when (deref results)
|
||||
(let ((r (deref results)))
|
||||
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
|
||||
(~demo-result-card
|
||||
:title "categorize-special-forms"
|
||||
:ms (get r "sf-ms") :theme "blue"
|
||||
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
|
||||
(p :class "text-sm mb-1"
|
||||
(str (get r "sf-total") " forms in "
|
||||
(len (keys (get r "sf-cats"))) " categories"))
|
||||
(map (fn (k)
|
||||
(div (str k ": " (get (get r "sf-cats") k))))
|
||||
(keys (get r "sf-cats"))))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-reference-data"
|
||||
:ms (get r "ref-ms") :theme "blue"
|
||||
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
|
||||
(p :class "text-sm mb-1"
|
||||
(str (len (get r "ref-sample")) " attributes with detail page links"))
|
||||
(map (fn (item)
|
||||
(div (str (get item "name") " → "
|
||||
(or (get item "href") "no detail page"))))
|
||||
(get r "ref-sample")))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-attr-detail"
|
||||
:ms (get r "attr-ms") :theme "blue"
|
||||
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
|
||||
(div (str "title: " (get (get r "attr-result") "attr-title")))
|
||||
(div (str "wire-id: " (or (get (get r "attr-result") "attr-wire-id") "none")))
|
||||
(div (str "has handler: " (if (get (get r "attr-result") "attr-handler") "yes" "no"))))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-component-source"
|
||||
:ms (get r "comp-ms") :theme "blue"
|
||||
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
|
||||
(pre :class "bg-blue-50 p-2 rounded overflow-x-auto"
|
||||
(get r "comp-result")))
|
||||
|
||||
(div :class "rounded-lg border border-blue-200 bg-blue-50/30 p-4 md:col-span-2"
|
||||
(h4 :class "font-semibold text-blue-700 text-sm mb-1"
|
||||
"build-routing-analysis "
|
||||
(span :class "text-xs text-blue-400" (str (get r "routing-ms") "ms")))
|
||||
(p :class "text-xs text-blue-500 mb-2"
|
||||
"Classifies pages as client-routable or server-only based on whether they have data dependencies.")
|
||||
(div :class "text-xs text-blue-600"
|
||||
(p :class "text-sm mb-1"
|
||||
(str (get (get r "routing-result") "total-pages") " pages: "
|
||||
(get (get r "routing-result") "client-count") " client-routable, "
|
||||
(get (get r "routing-result") "server-count") " server-only"))
|
||||
(div :class "space-y-0.5"
|
||||
(map (fn (pg)
|
||||
(div (str (get pg "name") " → " (get pg "mode")
|
||||
(when (not (empty? (get pg "reason")))
|
||||
(str " (" (get pg "reason") ")")))))
|
||||
(get (get r "routing-result") "pages")))))))))))
|
||||
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Main page component — server-rendered content + client island
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defcomp ~page-helpers-demo-content (&key
|
||||
sf-categories sf-total sf-ms
|
||||
ref-sample ref-ms
|
||||
attr-result attr-ms
|
||||
comp-source comp-ms
|
||||
routing-result routing-ms
|
||||
server-total-ms
|
||||
sf-source
|
||||
attr-detail req-attrs attr-keys)
|
||||
|
||||
(div :class "max-w-3xl mx-auto px-4"
|
||||
(div :class "mb-8"
|
||||
(h2 :class "text-2xl font-bold text-stone-800 mb-2" "Bootstrapped Page Helpers")
|
||||
(p :class "text-stone-600 mb-4"
|
||||
"These functions are defined once in "
|
||||
(code :class "text-violet-700" "page-helpers.sx")
|
||||
" and bootstrapped to both Python ("
|
||||
(code :class "text-violet-700" "sx_ref.py")
|
||||
") and JavaScript ("
|
||||
(code :class "text-violet-700" "sx-browser.js")
|
||||
"). The server ran them in Python during this page load. Click the button below to run the identical functions client-side in the browser — same spec, same inputs, same results."))
|
||||
|
||||
;; Server results
|
||||
(div :class "mb-8"
|
||||
(h3 :class "text-lg font-semibold text-stone-700 mb-3"
|
||||
"Server Results "
|
||||
(span :class "text-sm font-normal text-stone-500"
|
||||
(str "(Python, " server-total-ms "ms total)")))
|
||||
|
||||
(div :class "grid grid-cols-1 md:grid-cols-2 gap-4"
|
||||
|
||||
(~demo-result-card
|
||||
:title "categorize-special-forms"
|
||||
:ms sf-ms :theme "stone"
|
||||
:desc "Parses special-forms.sx and classifies each form by category (control flow, binding, quoting, etc)."
|
||||
(p :class "text-sm mb-1"
|
||||
(str sf-total " forms in "
|
||||
(len (keys sf-categories)) " categories"))
|
||||
(map (fn (k)
|
||||
(div (str k ": " (get sf-categories k))))
|
||||
(keys sf-categories)))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-reference-data"
|
||||
:ms ref-ms :theme "stone"
|
||||
:desc "Takes raw attribute tuples and generates reference table rows with documentation hrefs."
|
||||
(p :class "text-sm mb-1"
|
||||
(str (len ref-sample) " attributes with detail page links"))
|
||||
(map (fn (item)
|
||||
(div (str (get item "name") " → "
|
||||
(or (get item "href") "no detail page"))))
|
||||
ref-sample))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-attr-detail"
|
||||
:ms attr-ms :theme "stone"
|
||||
:desc "Builds a detail page data structure for a single attribute (sx-get): title, wire ID, handler status."
|
||||
(div (str "title: " (get attr-result "attr-title")))
|
||||
(div (str "wire-id: " (or (get attr-result "attr-wire-id") "none")))
|
||||
(div (str "has handler: " (if (get attr-result "attr-handler") "yes" "no"))))
|
||||
|
||||
(~demo-result-card
|
||||
:title "build-component-source"
|
||||
:ms comp-ms :theme "stone"
|
||||
:desc "Reconstructs a defcomp source definition from a component metadata dict (name, params, body)."
|
||||
(pre :class "bg-stone-50 p-2 rounded overflow-x-auto"
|
||||
comp-source))
|
||||
|
||||
(div :class "rounded-lg border border-stone-200 p-4 md:col-span-2"
|
||||
(h4 :class "font-semibold text-stone-700 text-sm mb-1"
|
||||
"build-routing-analysis "
|
||||
(span :class "text-xs text-stone-400" (str routing-ms "ms")))
|
||||
(p :class "text-xs text-stone-500 mb-2"
|
||||
"Classifies pages as client-routable or server-only based on whether they have data dependencies.")
|
||||
(div :class "text-xs text-stone-600"
|
||||
(p :class "text-sm mb-1"
|
||||
(str (get routing-result "total-pages") " pages: "
|
||||
(get routing-result "client-count") " client-routable, "
|
||||
(get routing-result "server-count") " server-only"))
|
||||
(div :class "space-y-0.5"
|
||||
(map (fn (pg)
|
||||
(div (str (get pg "name") " → " (get pg "mode")
|
||||
(when (not (empty? (get pg "reason")))
|
||||
(str " (" (get pg "reason") ")")))))
|
||||
(get routing-result "pages")))))))
|
||||
|
||||
;; Client execution area — pure SX island, no JavaScript file
|
||||
(div :class "mb-8"
|
||||
(h3 :class "text-lg font-semibold text-stone-700 mb-3"
|
||||
"Client Results "
|
||||
(span :class "text-sm font-normal text-stone-500" "(JavaScript, sx-browser.js)"))
|
||||
|
||||
(~demo-client-runner
|
||||
:sf-source sf-source
|
||||
:attr-detail attr-detail
|
||||
:req-attrs req-attrs
|
||||
:attr-keys attr-keys))))
|
||||
Reference in New Issue
Block a user