From 6215d3573bc52b0106a4e8225ef4582d79ec12a2 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 7 Mar 2026 08:35:20 +0000 Subject: [PATCH] Send content expression component deps in SX responses for client routing When a page has a content expression but no data dependency, compute its transitive component deps and pass them as extra_component_names to sx_response(). This ensures the client has all component definitions needed for future client-side route rendering. Co-Authored-By: Claude Opus 4.6 --- shared/sx/helpers.py | 22 +++++++++++++++++++--- shared/sx/pages.py | 14 +++++++++++++- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index 3174184..659ac9f 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -456,7 +456,8 @@ def sx_call(component_name: str, **kwargs: Any) -> str: -def components_for_request(source: str = "") -> str: +def components_for_request(source: str = "", + extra_names: set[str] | None = None) -> str: """Return defcomp/defmacro source for definitions the client doesn't have yet. Reads the ``SX-Components`` header (comma-separated component names @@ -464,6 +465,10 @@ def components_for_request(source: str = "") -> str: is missing. If *source* is provided, only sends components needed for that source (plus transitive deps). If the header is absent, returns all needed defs. + + *extra_names* — additional component names (``~foo``) to include + beyond what *source* references. Used by ``execute_page`` to send + components the page's content expression needs for client-side routing. """ from quart import request from .jinja_bridge import _COMPONENT_ENV @@ -477,6 +482,12 @@ def components_for_request(source: str = "") -> str: else: needed = None # all + # Merge in extra names (e.g. from page content expression deps) + if extra_names and needed is not None: + needed = needed | extra_names + elif extra_names: + needed = extra_names + loaded_raw = request.headers.get("SX-Components", "") loaded = set(loaded_raw.split(",")) if loaded_raw else set() @@ -510,7 +521,8 @@ def components_for_request(source: str = "") -> str: def sx_response(source: str, status: int = 200, - headers: dict | None = None): + headers: dict | None = None, + extra_component_names: set[str] | None = None): """Return an s-expression wire-format response. Takes a raw sx string:: @@ -520,6 +532,10 @@ def sx_response(source: str, status: int = 200, For SX requests, missing component definitions are prepended as a ``\n{body}') diff --git a/shared/sx/pages.py b/shared/sx/pages.py index 8912808..2c1ee1a 100644 --- a/shared/sx/pages.py +++ b/shared/sx/pages.py @@ -279,13 +279,25 @@ async def execute_page( is_htmx = is_htmx_request() if is_htmx: + # Compute content expression deps so the server sends component + # definitions the client needs for future client-side routing + extra_deps: set[str] | None = None + if page_def.content_expr is not None and page_def.data_expr is None: + from .deps import components_needed + from .parser import serialize + try: + content_src = serialize(page_def.content_expr) + extra_deps = components_needed(content_src, get_component_env()) + except Exception: + pass # non-critical — client will just fall back to server + return sx_response(await oob_page_sx( oobs=oob_headers if oob_headers else "", filter=filter_sx, aside=aside_sx, content=content_sx, menu=menu_sx, - )) + ), extra_component_names=extra_deps) else: return await full_page_sx( tctx,