From 732923a7efd1ab5db37b0793b0e7f285b89b3d99 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 6 Mar 2026 23:53:55 +0000 Subject: [PATCH] Fix: auto-include router spec module when boot adapter is present boot.sx uses parse-route-pattern from router.sx, but router was only included as an opt-in spec module. Now auto-included when boot is in the adapter set. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 76 ++++++++++++++++++++++++++++- shared/sx/ref/bootstrap_js.py | 3 ++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 07d007c..44c4300 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-06T23:36:38Z"; + var SX_VERSION = "2026-03-06T23:52:56Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -2379,6 +2379,76 @@ callExpr.push(dictGet(kwargs, k)); } } var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), initStyleDict(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); }; + // === Transpiled from router (client-side route matching) === + + // split-path-segments + var splitPathSegments = function(path) { return (function() { + var trimmed = (isSxTruthy(startsWith(path, "/")) ? slice(path, 1) : path); + return (function() { + var trimmed2 = (isSxTruthy((isSxTruthy(!isSxTruthy(isEmpty(trimmed))) && endsWith(trimmed, "/"))) ? slice(trimmed, 0, (len(trimmed) - 1)) : trimmed); + return (isSxTruthy(isEmpty(trimmed2)) ? [] : split(trimmed2, "/")); +})(); +})(); }; + + // make-route-segment + var makeRouteSegment = function(seg) { return (isSxTruthy((isSxTruthy(startsWith(seg, "<")) && endsWith(seg, ">"))) ? (function() { + var paramName = slice(seg, 1, (len(seg) - 1)); + return (function() { + var d = {}; + d["type"] = "param"; + d["value"] = paramName; + return d; +})(); +})() : (function() { + var d = {}; + d["type"] = "literal"; + d["value"] = seg; + return d; +})()); }; + + // parse-route-pattern + var parseRoutePattern = function(pattern) { return (function() { + var segments = splitPathSegments(pattern); + return map(makeRouteSegment, segments); +})(); }; + + // match-route-segments + var matchRouteSegments = function(pathSegs, parsedSegs) { return (isSxTruthy(!isSxTruthy((len(pathSegs) == len(parsedSegs)))) ? NIL : (function() { + var params = {}; + var matched = true; + forEachIndexed(function(i, parsedSeg) { return (isSxTruthy(matched) ? (function() { + var pathSeg = nth(pathSegs, i); + var segType = get(parsedSeg, "type"); + return (isSxTruthy((segType == "literal")) ? (isSxTruthy(!isSxTruthy((pathSeg == get(parsedSeg, "value")))) ? (matched = false) : NIL) : (isSxTruthy((segType == "param")) ? dictSet(params, get(parsedSeg, "value"), pathSeg) : (matched = false))); +})() : NIL); }, parsedSegs); + return (isSxTruthy(matched) ? params : NIL); +})()); }; + + // match-route + var matchRoute = function(path, pattern) { return (function() { + var pathSegs = splitPathSegments(path); + var parsedSegs = parseRoutePattern(pattern); + return matchRouteSegments(pathSegs, parsedSegs); +})(); }; + + // find-matching-route + var findMatchingRoute = function(path, routes) { return (function() { + var pathSegs = splitPathSegments(path); + var result = NIL; + { var _c = routes; for (var _i = 0; _i < _c.length; _i++) { var route = _c[_i]; if (isSxTruthy(isNil(result))) { + (function() { + var params = matchRouteSegments(pathSegs, get(route, "parsed")); + return (isSxTruthy(!isSxTruthy(isNil(params))) ? (function() { + var matched = merge(route, {}); + matched["params"] = params; + return (result = matched); +})() : NIL); +})(); +} } } + return result; +})(); }; + + // ========================================================================= // Platform interface — DOM adapter (browser-only) // ========================================================================= @@ -3554,6 +3624,10 @@ callExpr.push(dictGet(kwargs, k)); } } renderComponent: typeof sxRenderComponent === "function" ? sxRenderComponent : null, getEnv: function() { return componentEnv; }, init: typeof bootInit === "function" ? bootInit : null, + splitPathSegments: splitPathSegments, + parseRoutePattern: parseRoutePattern, + matchRoute: matchRoute, + findMatchingRoute: findMatchingRoute, _version: "ref-2.0 (boot+cssx+dom+engine+html+orchestration+parser+sx, bootstrap-compiled)" }; diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index 8936396..e1704ea 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -1199,6 +1199,9 @@ def compile_ref_to_js( if sm not in SPEC_MODULES: raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}") spec_mod_set.add(sm) + # boot.sx uses parse-route-pattern from router.sx + if "boot" in adapter_set: + spec_mod_set.add("router") has_deps = "deps" in spec_mod_set has_router = "router" in spec_mod_set