Phase 3: Client-side routing with SX page registry + routing analyzer demo
Add client-side route matching so pure pages (no IO deps) can render instantly without a server roundtrip. Page metadata serialized as SX dict literals (not JSON) in <script type="text/sx-pages"> blocks. - New router.sx spec: route pattern parsing and matching (6 pure functions) - boot.sx: process page registry using SX parser at startup - orchestration.sx: intercept boost links for client routing with try-first/fallback — client attempts local eval, falls back to server - helpers.py: _build_pages_sx() serializes defpage metadata as SX - Routing analyzer demo page showing per-page client/server classification - 32 tests for Phase 2 IO detection (scan_io_refs, transitive_io_refs, compute_all_io_refs, component_pure?) + fallback/ref parity - 37 tests for Phase 3 router functions + page registry serialization - Fix bootstrap_py.py _emit_let cell variable initialization bug - Fix missing primitive aliases (split, length, merge) in bootstrap_py.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -505,7 +505,7 @@
|
||||
(dom-set-attr link "sx-swap" "innerHTML"))
|
||||
(when (not (dom-has-attr? link "sx-push-url"))
|
||||
(dom-set-attr link "sx-push-url" "true"))
|
||||
(bind-boost-link link (dom-get-attr link "href"))))
|
||||
(bind-client-route-link link (dom-get-attr link "href"))))
|
||||
(dom-query-all container "a[href]"))
|
||||
(for-each
|
||||
(fn (form)
|
||||
@@ -523,6 +523,52 @@
|
||||
(dom-query-all container "form"))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Client-side routing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define try-client-route
|
||||
(fn (pathname)
|
||||
;; Try to render a page client-side. Returns true if successful, false otherwise.
|
||||
;; Only works for pages without :data dependencies.
|
||||
(let ((match (find-matching-route pathname _page-routes)))
|
||||
(if (nil? match)
|
||||
false
|
||||
(if (get match "has-data")
|
||||
false
|
||||
(let ((content-src (get match "content"))
|
||||
(closure (or (get match "closure") {}))
|
||||
(params (get match "params")))
|
||||
(if (or (nil? content-src) (empty? content-src))
|
||||
false
|
||||
(let ((env (merge closure params))
|
||||
(rendered (try-eval-content content-src env)))
|
||||
(if (nil? rendered)
|
||||
false
|
||||
(let ((target (dom-query-by-id "main-panel")))
|
||||
(if (nil? target)
|
||||
false
|
||||
(do
|
||||
(dom-set-text-content target "")
|
||||
(dom-append target rendered)
|
||||
(hoist-head-elements-full target)
|
||||
(process-elements target)
|
||||
(sx-hydrate-elements target)
|
||||
(log-info (str "sx:route client " pathname))
|
||||
true))))))))))))
|
||||
|
||||
|
||||
(define bind-client-route-link
|
||||
(fn (link href)
|
||||
;; Bind a boost link with client-side routing. If the route can be
|
||||
;; rendered client-side (pure page, no :data), do so. Otherwise
|
||||
;; fall back to standard server fetch via bind-boost-link.
|
||||
(bind-client-route-click link href
|
||||
(fn ()
|
||||
;; Fallback: use standard boost link binding
|
||||
(bind-boost-link link href)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; SSE processing
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -668,13 +714,17 @@
|
||||
|
||||
(define handle-popstate
|
||||
(fn (scrollY)
|
||||
;; Handle browser back/forward navigation
|
||||
;; Handle browser back/forward navigation.
|
||||
;; Try client-side route first, fall back to server fetch.
|
||||
(let ((main (dom-query-by-id "main-panel"))
|
||||
(url (browser-location-href)))
|
||||
(when main
|
||||
(let ((headers (build-request-headers main
|
||||
(loaded-component-names) _css-hash)))
|
||||
(fetch-and-restore main url headers scrollY))))))
|
||||
(let ((pathname (url-pathname url)))
|
||||
(if (try-client-route pathname)
|
||||
(browser-scroll-to 0 scrollY)
|
||||
(let ((headers (build-request-headers main
|
||||
(loaded-component-names) _css-hash)))
|
||||
(fetch-and-restore main url headers scrollY))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -773,6 +823,7 @@
|
||||
;; === Boost bindings ===
|
||||
;; (bind-boost-link el href) → void (click handler + pushState)
|
||||
;; (bind-boost-form form method action) → void (submit handler)
|
||||
;; (bind-client-route-click link href fallback-fn) → void (client route click handler)
|
||||
;;
|
||||
;; === Inline handlers ===
|
||||
;; (bind-inline-handler el event-name body) → void (new Function)
|
||||
@@ -803,10 +854,22 @@
|
||||
;; === Parsing ===
|
||||
;; (try-parse-json s) → parsed value or nil
|
||||
;;
|
||||
;; === Client-side routing ===
|
||||
;; (try-eval-content source env) → DOM node or nil (catches eval errors)
|
||||
;; (url-pathname href) → extract pathname from URL string
|
||||
;;
|
||||
;; From boot.sx:
|
||||
;; _page-routes → list of route entries
|
||||
;;
|
||||
;; From router.sx:
|
||||
;; (find-matching-route path routes) → matching entry with params, or nil
|
||||
;; (parse-route-pattern pattern) → parsed pattern segments
|
||||
;;
|
||||
;; === Browser (via engine.sx) ===
|
||||
;; (browser-location-href) → current URL string
|
||||
;; (browser-navigate url) → void
|
||||
;; (browser-reload) → void
|
||||
;; (browser-scroll-to x y) → void
|
||||
;; (browser-media-matches? query) → boolean
|
||||
;; (browser-confirm msg) → boolean
|
||||
;; (browser-prompt msg) → string or nil
|
||||
|
||||
Reference in New Issue
Block a user