Add component deps to page registry, check before client routing

Each page entry now includes a deps list of component names needed.
Client checks all deps are loaded before attempting eval — if any
are missing, falls through to server fetch with a clear log message.
No bundle bloat: server sends components for the current page only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 22:06:28 +00:00
parent 3b00a7095a
commit 14c5316d17
3 changed files with 49 additions and 10 deletions

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-06T22:01:24Z";
var SX_VERSION = "2026-03-06T22:06:19Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -1932,21 +1932,32 @@ return forEach(function(form) { return (isSxTruthy((isSxTruthy(!isSxTruthy(isPro
return bindBoostForm(form, method, action);
})()) : NIL); }, domQueryAll(container, "form")); };
// has-all-deps?
var hasAllDeps_p = function(deps) { return (function() {
var loaded = loadedComponentNames();
var missing = false;
{ var _c = deps; for (var _i = 0; _i < _c.length; _i++) { var dep = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(loaded, dep)))) {
missing = dep;
} } }
return (isSxTruthy(missing) ? (logInfo((String("sx:route missing component ") + String(missing))), false) : true);
})(); };
// try-client-route
var tryClientRoute = function(pathname) { return (function() {
var match = findMatchingRoute(pathname, _pageRoutes);
return (isSxTruthy(isNil(match)) ? (logInfo((String("sx:route no match (") + String(len(_pageRoutes)) + String(" routes) ") + String(pathname))), false) : (isSxTruthy(get(match, "has-data")) ? (logInfo((String("sx:route server (has data) ") + String(pathname))), false) : (function() {
var contentSrc = get(match, "content");
var deps = sxOr(get(match, "deps"), []);
var closure = sxOr(get(match, "closure"), {});
var params = get(match, "params");
return (isSxTruthy(sxOr(isNil(contentSrc), isEmpty(contentSrc))) ? (logWarn((String("sx:route no content for ") + String(pathname))), false) : (function() {
return (isSxTruthy(sxOr(isNil(contentSrc), isEmpty(contentSrc))) ? (logWarn((String("sx:route no content for ") + String(pathname))), false) : (isSxTruthy(!isSxTruthy(hasAllDeps_p(deps))) ? (logInfo((String("sx:route server (missing deps) ") + String(pathname))), false) : (function() {
var env = merge(closure, params);
var rendered = tryEvalContent(contentSrc, env);
return (isSxTruthy(isNil(rendered)) ? (logWarn((String("sx:route eval failed for ") + String(pathname))), false) : (function() {
var target = domQueryById("main-panel");
return (isSxTruthy(isNil(target)) ? (logWarn("sx:route #main-panel not found"), false) : (domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), logInfo((String("sx:route client ") + String(pathname))), true));
})());
})());
})()));
})()));
})(); };

View File

@@ -644,12 +644,14 @@ def _build_pages_sx(service: str) -> str:
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.
content, closure, deps.
"""
import logging
_log = logging.getLogger("sx.pages")
from .pages import get_all_pages
from .parser import serialize as sx_serialize
from .deps import components_needed
from .jinja_bridge import _COMPONENT_ENV
pages = get_all_pages(service)
_log.debug("_build_pages_sx(%s): %d pages in registry", service, len(pages))
@@ -666,6 +668,12 @@ def _build_pages_sx(service: str) -> str:
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"
# Component deps needed to render this page client-side
deps: set[str] = set()
if content_src and page_def.data_expr is None:
deps = components_needed(content_src, _COMPONENT_ENV)
deps_sx = "(" + " ".join(_sx_literal(d) for d in sorted(deps)) + ")"
# Build closure as SX dict
closure_parts: list[str] = []
for k, v in page_def.closure.items():
@@ -679,6 +687,7 @@ def _build_pages_sx(service: str) -> str:
+ " :auth " + _sx_literal(auth)
+ " :has-data " + has_data
+ " :content " + _sx_literal(content_src)
+ " :deps " + deps_sx
+ " :closure " + closure_sx + "}"
)
entries.append(entry)
@@ -702,6 +711,7 @@ def _sx_literal(v: object) -> str:
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.

View File

@@ -543,24 +543,42 @@
;; Client-side routing
;; --------------------------------------------------------------------------
(define has-all-deps?
(fn (deps)
;; Check if all component deps are loaded in the client env.
;; deps is a list of component name strings (e.g. "~essay-foo").
(let ((loaded (loaded-component-names))
(missing false))
(for-each
(fn (dep)
(when (not (contains? loaded dep))
(set! missing dep)))
deps)
(if missing
(do (log-info (str "sx:route missing component " missing)) false)
true))))
(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.
;; Only works for pages without :data dependencies and with all deps loaded.
(let ((match (find-matching-route pathname _page-routes)))
(if (nil? match)
(do (log-info (str "sx:route no match (" (len _page-routes) " routes) " pathname)) false)
(if (get match "has-data")
(do (log-info (str "sx:route server (has data) " pathname)) false)
(let ((content-src (get match "content"))
(deps (or (get match "deps") (list)))
(closure (or (get match "closure") {}))
(params (get match "params")))
(if (or (nil? content-src) (empty? content-src))
(do (log-warn (str "sx:route no content for " pathname)) false)
(let ((env (merge closure params))
(rendered (try-eval-content content-src env)))
(if (nil? rendered)
(do (log-warn (str "sx:route eval failed for " pathname)) false)
(if (not (has-all-deps? deps))
(do (log-info (str "sx:route server (missing deps) " pathname)) false)
(let ((env (merge closure params))
(rendered (try-eval-content content-src env)))
(if (nil? rendered)
(do (log-warn (str "sx:route eval failed for " pathname)) false)
(let ((target (dom-query-by-id "main-panel")))
(if (nil? target)
(do (log-warn "sx:route #main-panel not found") false)
@@ -571,7 +589,7 @@
(process-elements target)
(sx-hydrate-elements target)
(log-info (str "sx:route client " pathname))
true))))))))))))
true)))))))))))))
(define bind-client-route-link