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:
2026-03-06 15:47:56 +00:00
parent 631394989c
commit cf5e767510
16 changed files with 2059 additions and 99 deletions

View File

@@ -631,6 +631,7 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details.
<body class="bg-stone-50 text-stone-900">
<script type="text/sx-styles" data-hash="{styles_hash}">{styles_json}</script>
<script type="text/sx" data-components data-hash="{component_hash}">{component_defs}</script>
<script type="text/sx-pages">{pages_sx}</script>
<script type="text/sx" data-mount="body">{page_sx}</script>
<script src="{asset_url}/scripts/sx-browser.js?v={sx_js_hash}"></script>
<script src="{asset_url}/scripts/body.js?v={body_js_hash}"></script>
@@ -638,6 +639,66 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details.
</html>"""
def _build_pages_sx(service: str) -> str:
"""Build SX page registry for client-side routing.
Returns SX dict literals (one per page) parseable by the client's
``parse`` function. Each dict has keys: name, path, auth, has-data,
content, closure.
"""
from .pages import get_all_pages
from .parser import serialize as sx_serialize
pages = get_all_pages(service)
if not pages:
return ""
entries = []
for page_def in pages.values():
content_src = ""
if page_def.content_expr is not None:
try:
content_src = sx_serialize(page_def.content_expr)
except Exception:
pass
auth = page_def.auth if isinstance(page_def.auth, str) else "custom"
has_data = "true" if page_def.data_expr is not None else "false"
# Build closure as SX dict
closure_parts: list[str] = []
for k, v in page_def.closure.items():
if isinstance(v, (str, int, float, bool)):
closure_parts.append(f":{k} {_sx_literal(v)}")
closure_sx = "{" + " ".join(closure_parts) + "}"
entry = (
"{:name " + _sx_literal(page_def.name)
+ " :path " + _sx_literal(page_def.path)
+ " :auth " + _sx_literal(auth)
+ " :has-data " + has_data
+ " :content " + _sx_literal(content_src)
+ " :closure " + closure_sx + "}"
)
entries.append(entry)
return "\n".join(entries)
def _sx_literal(v: object) -> str:
"""Serialize a Python value as an SX literal."""
if v is None:
return "nil"
if isinstance(v, bool):
return "true" if v else "false"
if isinstance(v, (int, float)):
return str(v)
if isinstance(v, str):
escaped = v.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
return f'"{escaped}"'
return "nil"
def sx_page(ctx: dict, page_sx: str, *,
meta_html: str = "") -> str:
"""Return a minimal HTML shell that boots the page from sx source.
@@ -692,6 +753,14 @@ def sx_page(ctx: dict, page_sx: str, *,
else:
styles_json = _build_style_dict_json()
# Page registry for client-side routing
pages_sx = ""
try:
from quart import current_app
pages_sx = _build_pages_sx(current_app.name)
except Exception:
pass
return _SX_PAGE_TEMPLATE.format(
title=_html_escape(title),
asset_url=asset_url,
@@ -701,6 +770,7 @@ def sx_page(ctx: dict, page_sx: str, *,
component_defs=component_defs,
styles_hash=styles_hash,
styles_json=styles_json,
pages_sx=pages_sx,
page_sx=page_sx,
sx_css=sx_css,
sx_css_classes=sx_css_classes,