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:
@@ -100,11 +100,13 @@
|
||||
(dict :label "CSSX" :href "/specs/cssx")
|
||||
(dict :label "Continuations" :href "/specs/continuations")
|
||||
(dict :label "call/cc" :href "/specs/callcc")
|
||||
(dict :label "Deps" :href "/specs/deps")))
|
||||
(dict :label "Deps" :href "/specs/deps")
|
||||
(dict :label "Router" :href "/specs/router")))
|
||||
|
||||
(define isomorphism-nav-items (list
|
||||
(dict :label "Roadmap" :href "/isomorphism/")
|
||||
(dict :label "Bundle Analyzer" :href "/isomorphism/bundle-analyzer")))
|
||||
(dict :label "Bundle Analyzer" :href "/isomorphism/bundle-analyzer")
|
||||
(dict :label "Routing Analyzer" :href "/isomorphism/routing-analyzer")))
|
||||
|
||||
(define plans-nav-items (list
|
||||
(dict :label "Reader Macros" :href "/plans/reader-macros"
|
||||
@@ -175,7 +177,10 @@
|
||||
(define module-spec-items (list
|
||||
(dict :slug "deps" :filename "deps.sx" :title "Deps"
|
||||
:desc "Component dependency analysis and IO detection — per-page bundling, transitive closure, CSS scoping, pure/IO classification."
|
||||
:prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations — frag, query, service, current-user, highlight, etc.), computes transitive IO refs through the component graph, and caches the result on each component. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side. The spec provides the classification; each host's async partial evaluator acts on it (expand IO-dependent server-side, serialize pure for client). All functions are pure — each host bootstraps them to native code via --spec-modules deps. Platform functions (component-deps, component-set-deps!, component-css-classes, component-io-refs, component-set-io-refs!, env-components, regex-find-all, scan-css-classes) are implemented natively per target.")))
|
||||
:prose "The deps module analyzes component dependency graphs and classifies components as pure or IO-dependent. Phase 1 (bundling): walks component AST bodies to find transitive ~component references, computes the minimal set needed per page, and collects per-page CSS classes from only the used components. Phase 2 (IO detection): scans component ASTs for references to IO primitive names (from boundary.sx declarations — frag, query, service, current-user, highlight, etc.), computes transitive IO refs through the component graph, and caches the result on each component. Components with no transitive IO refs are pure — they can render anywhere without server data. IO-dependent components must expand server-side. The spec provides the classification; each host's async partial evaluator acts on it (expand IO-dependent server-side, serialize pure for client). All functions are pure — each host bootstraps them to native code via --spec-modules deps. Platform functions (component-deps, component-set-deps!, component-css-classes, component-io-refs, component-set-io-refs!, env-components, regex-find-all, scan-css-classes) are implemented natively per target.")
|
||||
(dict :slug "router" :filename "router.sx" :title "Router"
|
||||
:desc "Client-side route matching — Flask-style pattern parsing, segment matching, route table search."
|
||||
:prose "The router module provides pure functions for matching URL paths against Flask-style route patterns (e.g. /docs/<slug>). Used by client-side routing (Phase 3) to determine if a page can be rendered locally without a server roundtrip. split-path-segments breaks a path into segments, parse-route-pattern converts patterns into typed segment descriptors, match-route-segments tests a path against a parsed pattern returning extracted params, and find-matching-route searches a route table for the first match. No platform interface needed — uses only pure string and list primitives. Bootstrapped via --spec-modules deps,router.")))
|
||||
|
||||
(define all-spec-items (concat core-spec-items (concat adapter-spec-items (concat browser-spec-items (concat extension-spec-items module-spec-items)))))
|
||||
|
||||
|
||||
@@ -615,7 +615,8 @@
|
||||
(li (strong "CSS on-demand: ") "CSSX resolves keywords to CSS rules, injects only used rules.")
|
||||
(li (strong "Boundary enforcement: ") "boundary.sx + SX_BOUNDARY_STRICT=1 validates all primitives/IO/helpers at registration.")
|
||||
(li (strong "Dependency analysis: ") "deps.sx computes per-page component bundles — only definitions a page actually uses are sent.")
|
||||
(li (strong "IO detection: ") "deps.sx classifies every component as pure or IO-dependent by scanning for boundary primitive references transitively. The spec provides the classification; each host's async evaluator acts on it — expanding IO-dependent components server-side, serializing pure ones for client rendering.")))
|
||||
(li (strong "IO detection: ") "deps.sx classifies every component as pure or IO-dependent. Server expands IO components, serializes pure ones for client.")
|
||||
(li (strong "Client-side routing: ") "router.sx matches URL patterns. Pure pages render instantly without server roundtrips. Pages with :data fall through to server transparently.")))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Phase 1
|
||||
@@ -747,46 +748,74 @@
|
||||
|
||||
(~doc-section :title "Phase 3: Client-Side Routing (SPA Mode)" :id "phase-3"
|
||||
|
||||
(div :class "rounded border border-violet-200 bg-violet-50 p-4 mb-4"
|
||||
(p :class "text-violet-900 font-medium" "What it enables")
|
||||
(p :class "text-violet-800" "After initial page load, client resolves routes locally using cached components + data. Only hits server for fresh data or unknown routes. Like Next.js client-side navigation."))
|
||||
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
||||
(a :href "/specs/router" :class "text-green-700 underline text-sm font-medium" "View canonical spec: router.sx")
|
||||
(a :href "/isomorphism/routing-analyzer" :class "text-green-700 underline text-sm font-medium" "Live routing analyzer"))
|
||||
(p :class "text-green-900 font-medium" "What it enables")
|
||||
(p :class "text-green-800" "After initial page load, pure pages render instantly without server roundtrips. Client matches routes locally, evaluates content expressions with cached components, and only falls back to server for pages with :data dependencies."))
|
||||
|
||||
(~doc-subsection :title "Current Mechanism"
|
||||
(p "All routing is server-side via defpage → Quart routes. Client navigates via sx-boost links doing sx-get + morphing. Every navigation = server roundtrip."))
|
||||
|
||||
(~doc-subsection :title "Approach"
|
||||
(~doc-subsection :title "Architecture"
|
||||
(p "Three-layer approach: spec defines pure route matching, page registry bridges server metadata to client, orchestration intercepts navigation for try-first/fallback.")
|
||||
|
||||
(div :class "space-y-4"
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "1. Client-side page registry")
|
||||
(p "Serialize defpage routing info to client:")
|
||||
(~doc-code :code (highlight "(script :type \"text/sx-pages\")\n;; {\"docs-page\": {\"path\": \"/docs/:slug\", \"auth\": \"public\",\n;; \"content\": \"(case slug ...)\", \"data\": null}}" "lisp")))
|
||||
(h4 :class "font-semibold text-stone-700" "1. Route matching spec (router.sx)")
|
||||
(p "New spec module with pure functions for Flask-style route pattern matching:")
|
||||
(~doc-code :code (highlight "(define split-path-segments ;; \"/docs/hello\" → (\"docs\" \"hello\")\n(define parse-route-pattern ;; \"/docs/<slug>\" → segment descriptors\n(define match-route-segments ;; segments + pattern → params dict or nil\n(define find-matching-route ;; path + route table → first match" "lisp"))
|
||||
(p "No platform interface needed — uses only pure string and list primitives. Bootstrapped to both hosts via " (code "--spec-modules deps,router") "."))
|
||||
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "2. Client route matcher")
|
||||
(p "New spec file shared/sx/ref/router.sx — convert /docs/<slug> patterns to matchers. On boost-link click: match URL → if found and pure, evaluate locally. If IO needed: fetch data from server, evaluate content locally. No match: fall through to standard fetch."))
|
||||
(h4 :class "font-semibold text-stone-700" "2. Page registry")
|
||||
(p "Server serializes defpage metadata as SX dict literals inside " (code "<script type=\"text/sx-pages\">") ". Each entry carries name, path pattern, auth level, has-data flag, serialized content expression, and closure values.")
|
||||
(~doc-code :code (highlight "{:name \"docs-page\" :path \"/docs/<slug>\"\n :auth \"public\" :has-data false\n :content \"(case slug ...)\" :closure {}}" "lisp"))
|
||||
(p "boot.sx processes these at startup using the SX parser — the same " (code "parse") " function from parser.sx — building route entries with parsed patterns into the " (code "_page-routes") " table. No JSON dependency."))
|
||||
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "3. Data endpoint")
|
||||
(~doc-code :code (highlight "GET /internal/page-data/<page-name>?<params>\n# Returns JSON with evaluated :data expression\n# Reuses execute_page() logic, stops after :data step" "python")))
|
||||
(h4 :class "font-semibold text-stone-700" "3. Client-side interception (orchestration.sx)")
|
||||
(p (code "bind-client-route-link") " replaces " (code "bind-boost-link") " in boost processing. On click:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li "Extract pathname from href")
|
||||
(li "Call " (code "find-matching-route") " against " (code "_page-routes"))
|
||||
(li "If match found AND no :data: evaluate content expression locally with component env + URL params")
|
||||
(li "If evaluation succeeds: swap into #main-panel, pushState, log " (code "\"sx:route client /path\""))
|
||||
(li "If anything fails (no match, has data, eval error): transparent fallback to server fetch"))
|
||||
(p (code "handle-popstate") " also tries client routing before server fetch on back/forward."))))
|
||||
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "4. Layout caching")
|
||||
(p "Layouts depend on auth/fragments, so cache current layout and reuse across navigations. SX-Layout-Hash header tracks staleness."))
|
||||
(~doc-subsection :title "What becomes client-routable"
|
||||
(p "Pages WITHOUT " (code ":data") " that have pure content expressions — most of this docs app:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (code "/") ", " (code "/docs/") ", " (code "/docs/<slug>") " (most slugs), " (code "/protocols/") ", " (code "/protocols/<slug>"))
|
||||
(li (code "/examples/") ", " (code "/examples/<slug>") ", " (code "/essays/") ", " (code "/essays/<slug>"))
|
||||
(li (code "/plans/") ", " (code "/plans/<slug>") ", " (code "/isomorphism/") ", " (code "/bootstrappers/")))
|
||||
(p "Pages that fall through to server:")
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li (code "/docs/primitives") " and " (code "/docs/special-forms") " (call " (code "primitives-data") " / " (code "special-forms-data") " helpers)")
|
||||
(li (code "/reference/<slug>") " (has " (code ":data (reference-data slug)") ")")
|
||||
(li (code "/bootstrappers/<slug>") " (has " (code ":data (bootstrapper-data slug)") ")")
|
||||
(li (code "/isomorphism/bundle-analyzer") " (has " (code ":data (bundle-analyzer-data)") ")")))
|
||||
|
||||
(div
|
||||
(h4 :class "font-semibold text-stone-700" "5. Integration with orchestration.sx")
|
||||
(p "Intercept bind-boost-link to try client-side resolution first."))))
|
||||
(~doc-subsection :title "Try-first/fallback design"
|
||||
(p "Client routing uses a try-first approach: attempt local evaluation in a try/catch, fall back to server fetch on any failure. This avoids needing perfect static analysis of content expressions — if a content expression calls a page helper the client doesn't have, the eval throws, and the server handles it transparently.")
|
||||
(p "Console messages provide visibility: " (code "sx:route client /essays/why-sexps") " vs " (code "sx:route server /specs/eval") "."))
|
||||
|
||||
(div :class "rounded border border-amber-200 bg-amber-50 p-3 mt-2"
|
||||
(p :class "text-amber-800 text-sm" (strong "Depends on: ") "Phase 1 (client knows which components each page needs), Phase 2 (which pages are pure vs IO)."))
|
||||
(~doc-subsection :title "Files"
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1 font-mono text-sm"
|
||||
(li "shared/sx/ref/router.sx — route pattern matching spec")
|
||||
(li "shared/sx/ref/boot.sx — process page registry scripts")
|
||||
(li "shared/sx/ref/orchestration.sx — client route interception")
|
||||
(li "shared/sx/ref/bootstrap_js.py — router spec module + platform functions")
|
||||
(li "shared/sx/ref/bootstrap_py.py — router spec module (parity)")
|
||||
(li "shared/sx/helpers.py — page registry SX serialization")))
|
||||
|
||||
(~doc-subsection :title "Verification"
|
||||
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
|
||||
(li "Pure page navigation: zero server requests")
|
||||
(li "IO page navigation: exactly one data request (not full page fetch)")
|
||||
(li "Browser back/forward works with client-resolved routes")
|
||||
(li "Disabling client registry → identical behavior to current"))))
|
||||
(li "Pure page navigation: zero server requests, console shows \"sx:route client\"")
|
||||
(li "IO/data page fallback: falls through to server fetch transparently")
|
||||
(li "Browser back/forward works with client-routed pages")
|
||||
(li "Disabling page registry → identical behavior to before")
|
||||
(li "Bootstrap parity: sx_ref.py and sx-ref.js both contain router functions"))))
|
||||
|
||||
;; -----------------------------------------------------------------------
|
||||
;; Phase 4
|
||||
|
||||
96
sx/sx/routing-analyzer.sx
Normal file
96
sx/sx/routing-analyzer.sx
Normal file
@@ -0,0 +1,96 @@
|
||||
;; Routing analyzer — live demonstration of client-side routing classification.
|
||||
;; Shows which pages route client-side (pure, instant) vs server-side (IO/data).
|
||||
;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-2 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 marker:text-stone-400 bg-blue-50 bg-amber-50 text-blue-700 text-amber-700 border-blue-200 border-amber-200 bg-blue-500 bg-amber-500 grid-cols-3 border-green-200 bg-green-50 text-green-700
|
||||
|
||||
(defcomp ~routing-analyzer-content (&key pages total-pages client-count
|
||||
server-count registry-sample)
|
||||
(~doc-page :title "Routing Analyzer"
|
||||
|
||||
(p :class "text-stone-600 mb-6"
|
||||
"Live classification of all " (strong (str total-pages)) " pages by routing mode. "
|
||||
"Pages without " (code ":data") " dependencies are "
|
||||
(span :class "text-green-700 font-medium" "client-routable")
|
||||
" — after initial load they render instantly from the page registry without a server roundtrip. "
|
||||
"Pages with data dependencies fall back to "
|
||||
(span :class "text-amber-700 font-medium" "server fetch")
|
||||
" transparently. Powered by "
|
||||
(a :href "/specs/router" :class "text-violet-700 underline" "router.sx")
|
||||
" route matching and "
|
||||
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
|
||||
" IO detection.")
|
||||
|
||||
(div :class "mb-8 grid grid-cols-4 gap-4"
|
||||
(~analyzer-stat :label "Total Pages" :value (str total-pages)
|
||||
:cls "text-violet-600")
|
||||
(~analyzer-stat :label "Client-Routable" :value (str client-count)
|
||||
:cls "text-green-600")
|
||||
(~analyzer-stat :label "Server-Only" :value (str server-count)
|
||||
:cls "text-amber-600")
|
||||
(~analyzer-stat :label "Client Ratio" :value (str (round (* (/ client-count total-pages) 100)) "%")
|
||||
:cls "text-blue-600"))
|
||||
|
||||
;; Route classification bar
|
||||
(div :class "mb-8"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
(span :class "text-sm font-medium text-stone-600" "Client")
|
||||
(div :class "flex-1")
|
||||
(span :class "text-sm font-medium text-stone-600" "Server"))
|
||||
(div :class "w-full bg-amber-200 rounded-full h-4 overflow-hidden"
|
||||
(div :class "bg-green-500 h-4 rounded-l-full transition-all"
|
||||
:style (str "width: " (round (* (/ client-count total-pages) 100)) "%"))))
|
||||
|
||||
(~doc-section :title "Route Table" :id "routes"
|
||||
(div :class "space-y-2"
|
||||
(map (fn (page)
|
||||
(~routing-row
|
||||
:name (get page "name")
|
||||
:path (get page "path")
|
||||
:mode (get page "mode")
|
||||
:has-data (get page "has-data")
|
||||
:content-expr (get page "content-expr")
|
||||
:reason (get page "reason")))
|
||||
pages)))
|
||||
|
||||
(~doc-section :title "Page Registry Format" :id "registry"
|
||||
(p :class "text-stone-600 mb-4"
|
||||
"The server serializes page metadata as SX dict literals inside "
|
||||
(code "<script type=\"text/sx-pages\">")
|
||||
". The client's parser reads these at boot, building a route table with parsed URL patterns. "
|
||||
"No JSON involved — the same SX parser handles everything.")
|
||||
(when (not (empty? registry-sample))
|
||||
(div :class "not-prose"
|
||||
(pre :class "text-xs leading-relaxed whitespace-pre-wrap overflow-x-auto bg-stone-100 rounded border border-stone-200 p-4"
|
||||
(code (highlight registry-sample "lisp"))))))
|
||||
|
||||
(~doc-section :title "How Client Routing Works" :id "how"
|
||||
(ol :class "list-decimal pl-5 space-y-2 text-stone-700"
|
||||
(li (strong "Boot: ") "boot.sx finds " (code "<script type=\"text/sx-pages\">") ", calls " (code "parse") " on the SX content, then " (code "parse-route-pattern") " on each page's path to build " (code "_page-routes") ".")
|
||||
(li (strong "Click: ") "orchestration.sx intercepts boost link clicks via " (code "bind-client-route-link") ". Extracts the pathname from the href.")
|
||||
(li (strong "Match: ") (code "find-matching-route") " from router.sx tests the pathname against all parsed patterns. Returns the first match with extracted URL params.")
|
||||
(li (strong "Check: ") "If the matched page has " (code ":has-data true") ", skip to server fetch. Otherwise proceed to client eval.")
|
||||
(li (strong "Eval: ") (code "try-eval-content") " merges the component env + URL params + closure, then parses and renders the content expression to DOM.")
|
||||
(li (strong "Swap: ") "On success, the rendered DOM replaces " (code "#main-panel") " contents, " (code "pushState") " updates the URL, and the console logs " (code "sx:route client /path") ".")
|
||||
(li (strong "Fallback: ") "If anything fails (no match, eval error, missing component), the click falls through to a standard server fetch. Console logs " (code "sx:route server /path") ". The user sees no difference.")))))
|
||||
|
||||
(defcomp ~routing-row (&key name path mode has-data content-expr reason)
|
||||
(div :class (str "rounded border p-3 flex items-center gap-3 "
|
||||
(if (= mode "client")
|
||||
"border-green-200 bg-green-50"
|
||||
"border-amber-200 bg-amber-50"))
|
||||
;; Mode badge
|
||||
(span :class (str "inline-block px-2 py-0.5 rounded text-xs font-bold uppercase "
|
||||
(if (= mode "client")
|
||||
"bg-green-600 text-white"
|
||||
"bg-amber-500 text-white"))
|
||||
mode)
|
||||
;; Page info
|
||||
(div :class "flex-1 min-w-0"
|
||||
(div :class "flex items-center gap-2"
|
||||
(span :class "font-mono font-semibold text-stone-800 text-sm" name)
|
||||
(span :class "text-stone-400 text-xs font-mono" path))
|
||||
(when reason
|
||||
(div :class "text-xs text-stone-500 mt-0.5" reason)))
|
||||
;; Content expression
|
||||
(when content-expr
|
||||
(div :class "hidden md:block max-w-xs truncate"
|
||||
(code :class "text-xs text-stone-500" content-expr)))))
|
||||
@@ -182,7 +182,8 @@ continuations.sx depends on: eval (optional)
|
||||
callcc.sx depends on: eval (optional)
|
||||
|
||||
;; Spec modules (optional — loaded via --spec-modules)
|
||||
deps.sx depends on: eval (optional)")))
|
||||
deps.sx depends on: eval (optional)
|
||||
router.sx (standalone — pure string/list ops)")))
|
||||
|
||||
(div :class "space-y-3"
|
||||
(h2 :class "text-2xl font-semibold text-stone-800" "Extensions")
|
||||
|
||||
@@ -416,6 +416,9 @@
|
||||
"bundle-analyzer" (~bundle-analyzer-content
|
||||
:pages pages :total-components total-components :total-macros total-macros
|
||||
:pure-count pure-count :io-count io-count)
|
||||
"routing-analyzer" (~routing-analyzer-content
|
||||
:pages pages :total-pages total-pages :client-count client-count
|
||||
:server-count server-count :registry-sample registry-sample)
|
||||
:else (~plan-isomorphic-content)))
|
||||
|
||||
(defpage bundle-analyzer
|
||||
@@ -432,6 +435,20 @@
|
||||
:pages pages :total-components total-components :total-macros total-macros
|
||||
:pure-count pure-count :io-count io-count))
|
||||
|
||||
(defpage routing-analyzer
|
||||
:path "/isomorphism/routing-analyzer"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Isomorphism"
|
||||
:sub-label "Isomorphism"
|
||||
:sub-href "/isomorphism/"
|
||||
:sub-nav (~section-nav :items isomorphism-nav-items :current "Routing Analyzer")
|
||||
:selected "Routing Analyzer")
|
||||
:data (routing-analyzer-data)
|
||||
:content (~routing-analyzer-content
|
||||
:pages pages :total-pages total-pages :client-count client-count
|
||||
:server-count server-count :registry-sample registry-sample))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Plans section
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
@@ -22,6 +22,7 @@ def _register_sx_helpers() -> None:
|
||||
"read-spec-file": _read_spec_file,
|
||||
"bootstrapper-data": _bootstrapper_data,
|
||||
"bundle-analyzer-data": _bundle_analyzer_data,
|
||||
"routing-analyzer-data": _routing_analyzer_data,
|
||||
})
|
||||
|
||||
|
||||
@@ -342,6 +343,82 @@ def _bundle_analyzer_data() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _routing_analyzer_data() -> dict:
|
||||
"""Compute per-page routing classification for the sx-docs app."""
|
||||
from shared.sx.pages import get_all_pages
|
||||
from shared.sx.parser import serialize as sx_serialize
|
||||
from shared.sx.helpers import _sx_literal
|
||||
|
||||
pages_data = []
|
||||
full_content: list[tuple[str, str, bool]] = [] # (name, full_content, has_data)
|
||||
client_count = 0
|
||||
server_count = 0
|
||||
|
||||
for name, page_def in sorted(get_all_pages("sx").items()):
|
||||
has_data = page_def.data_expr is not None
|
||||
content_src = ""
|
||||
if page_def.content_expr is not None:
|
||||
try:
|
||||
content_src = sx_serialize(page_def.content_expr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
full_content.append((name, content_src, has_data))
|
||||
|
||||
# Determine routing mode and reason
|
||||
if has_data:
|
||||
mode = "server"
|
||||
reason = "Has :data expression — needs server IO"
|
||||
server_count += 1
|
||||
elif not content_src:
|
||||
mode = "server"
|
||||
reason = "No content expression"
|
||||
server_count += 1
|
||||
else:
|
||||
mode = "client"
|
||||
reason = ""
|
||||
client_count += 1
|
||||
|
||||
pages_data.append({
|
||||
"name": name,
|
||||
"path": page_def.path,
|
||||
"mode": mode,
|
||||
"has-data": has_data,
|
||||
"content-expr": content_src[:80] + ("..." if len(content_src) > 80 else ""),
|
||||
"reason": reason,
|
||||
})
|
||||
|
||||
# Sort: client pages first, then server
|
||||
pages_data.sort(key=lambda p: (0 if p["mode"] == "client" else 1, p["name"]))
|
||||
|
||||
# Build a sample of the SX page registry format (use full content, first 3)
|
||||
total = client_count + server_count
|
||||
sample_entries = []
|
||||
sorted_full = sorted(full_content, key=lambda x: x[0])
|
||||
for name, csrc, hd in sorted_full[:3]:
|
||||
page_def = get_all_pages("sx").get(name)
|
||||
if not page_def:
|
||||
continue
|
||||
entry = (
|
||||
"{:name " + _sx_literal(name)
|
||||
+ "\n :path " + _sx_literal(page_def.path)
|
||||
+ "\n :auth " + _sx_literal("public")
|
||||
+ " :has-data " + ("true" if hd else "false")
|
||||
+ "\n :content " + _sx_literal(csrc)
|
||||
+ "\n :closure {}}"
|
||||
)
|
||||
sample_entries.append(entry)
|
||||
registry_sample = "\n\n".join(sample_entries)
|
||||
|
||||
return {
|
||||
"pages": pages_data,
|
||||
"total-pages": total,
|
||||
"client-count": client_count,
|
||||
"server-count": server_count,
|
||||
"registry-sample": registry_sample,
|
||||
}
|
||||
|
||||
|
||||
def _attr_detail_data(slug: str) -> dict:
|
||||
"""Return attribute detail data for a specific attribute slug.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user