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:
@@ -376,6 +376,11 @@ class JSEmitter:
|
||||
"event-source-listen": "eventSourceListen",
|
||||
"bind-boost-link": "bindBoostLink",
|
||||
"bind-boost-form": "bindBoostForm",
|
||||
"bind-client-route-link": "bindClientRouteLink",
|
||||
"bind-client-route-click": "bindClientRouteClick",
|
||||
"try-client-route": "tryClientRoute",
|
||||
"try-eval-content": "tryEvalContent",
|
||||
"url-pathname": "urlPathname",
|
||||
"bind-inline-handler": "bindInlineHandler",
|
||||
"bind-preload": "bindPreload",
|
||||
"mark-processed!": "markProcessed",
|
||||
@@ -490,6 +495,9 @@ class JSEmitter:
|
||||
"log-info": "logInfo",
|
||||
"log-parse-error": "logParseError",
|
||||
"parse-and-load-style-dict": "parseAndLoadStyleDict",
|
||||
"_page-routes": "_pageRoutes",
|
||||
"process-page-scripts": "processPageScripts",
|
||||
"query-page-scripts": "queryPageScripts",
|
||||
# deps.sx
|
||||
"scan-refs": "scanRefs",
|
||||
"scan-refs-walk": "scanRefsWalk",
|
||||
@@ -513,6 +521,14 @@ class JSEmitter:
|
||||
"transitive-io-refs": "transitiveIoRefs",
|
||||
"compute-all-io-refs": "computeAllIoRefs",
|
||||
"component-pure?": "componentPure_p",
|
||||
# router.sx
|
||||
"split-path-segments": "splitPathSegments",
|
||||
"make-route-segment": "makeRouteSegment",
|
||||
"parse-route-pattern": "parseRoutePattern",
|
||||
"match-route-segments": "matchRouteSegments",
|
||||
"match-route": "matchRoute",
|
||||
"find-matching-route": "findMatchingRoute",
|
||||
"for-each-indexed": "forEachIndexed",
|
||||
}
|
||||
if name in RENAMES:
|
||||
return RENAMES[name]
|
||||
@@ -1026,6 +1042,7 @@ ADAPTER_DEPS = {
|
||||
|
||||
SPEC_MODULES = {
|
||||
"deps": ("deps.sx", "deps (component dependency analysis)"),
|
||||
"router": ("router.sx", "router (client-side route matching)"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1170,6 +1187,7 @@ def compile_ref_to_js(
|
||||
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
|
||||
spec_mod_set.add(sm)
|
||||
has_deps = "deps" in spec_mod_set
|
||||
has_router = "router" in spec_mod_set
|
||||
|
||||
# Core files always included, then selected adapters, then spec modules
|
||||
sx_files = [
|
||||
@@ -1256,7 +1274,7 @@ def compile_ref_to_js(
|
||||
parts.append(fixups_js(has_html, has_sx, has_dom))
|
||||
if has_continuations:
|
||||
parts.append(CONTINUATIONS_JS)
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps))
|
||||
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps, has_router))
|
||||
parts.append(EPILOGUE)
|
||||
return "\n".join(parts)
|
||||
|
||||
@@ -2589,6 +2607,50 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
});
|
||||
}
|
||||
|
||||
// --- Client-side route bindings ---
|
||||
|
||||
function bindClientRouteClick(link, href, fallbackFn) {
|
||||
link.addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
var pathname = urlPathname(href);
|
||||
if (tryClientRoute(pathname)) {
|
||||
try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {}
|
||||
if (typeof window !== "undefined") window.scrollTo(0, 0);
|
||||
} else {
|
||||
logInfo("sx:route server " + pathname);
|
||||
executeRequest(link, { method: "GET", url: href }).then(function() {
|
||||
try { history.pushState({ sxUrl: href, scrollY: window.scrollY }, "", href); } catch (err) {}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function tryEvalContent(source, env) {
|
||||
try {
|
||||
var merged = merge(componentEnv);
|
||||
if (env && !isNil(env)) {
|
||||
var ks = Object.keys(env);
|
||||
for (var i = 0; i < ks.length; i++) merged[ks[i]] = env[ks[i]];
|
||||
}
|
||||
return sxRenderWithEnv(source, merged);
|
||||
} catch (e) {
|
||||
return NIL;
|
||||
}
|
||||
}
|
||||
|
||||
function urlPathname(href) {
|
||||
try {
|
||||
return new URL(href, location.href).pathname;
|
||||
} catch (e) {
|
||||
// Fallback: strip query/hash
|
||||
var idx = href.indexOf("?");
|
||||
if (idx >= 0) href = href.substring(0, idx);
|
||||
idx = href.indexOf("#");
|
||||
if (idx >= 0) href = href.substring(0, idx);
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Inline handlers ---
|
||||
|
||||
function bindInlineHandler(el, eventName, body) {
|
||||
@@ -2861,6 +2923,12 @@ PLATFORM_BOOT_JS = """
|
||||
document.querySelectorAll('script[type="text/sx-styles"]'));
|
||||
}
|
||||
|
||||
function queryPageScripts() {
|
||||
if (!_hasDom) return [];
|
||||
return Array.prototype.slice.call(
|
||||
document.querySelectorAll('script[type="text/sx-pages"]'));
|
||||
}
|
||||
|
||||
// --- localStorage ---
|
||||
|
||||
function localStorageGet(key) {
|
||||
@@ -2968,7 +3036,7 @@ def fixups_js(has_html, has_sx, has_dom):
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps=False):
|
||||
def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has_boot, has_parser, adapter_label, has_deps=False, has_router=False):
|
||||
# Parser: use compiled sxParse from parser.sx, or inline a minimal fallback
|
||||
if has_parser:
|
||||
parser = '''
|
||||
@@ -3101,6 +3169,11 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
|
||||
api_lines.append(' transitiveIoRefs: transitiveIoRefs,')
|
||||
api_lines.append(' computeAllIoRefs: computeAllIoRefs,')
|
||||
api_lines.append(' componentPure_p: componentPure_p,')
|
||||
if has_router:
|
||||
api_lines.append(' splitPathSegments: splitPathSegments,')
|
||||
api_lines.append(' parseRoutePattern: parseRoutePattern,')
|
||||
api_lines.append(' matchRoute: matchRoute,')
|
||||
api_lines.append(' findMatchingRoute: findMatchingRoute,')
|
||||
|
||||
api_lines.append(f' _version: "{version}"')
|
||||
api_lines.append(' };')
|
||||
|
||||
Reference in New Issue
Block a user