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 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 08:35:20 +00:00
parent 093050059d
commit 6215d3573b
2 changed files with 32 additions and 4 deletions

View File

@@ -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
``<script type="text/sx" data-components>`` block so the client
can process them before rendering OOB content.
*extra_component_names* — additional component names to include beyond
what *source* references. Used by defpage to send components the page's
content expression needs for client-side routing.
"""
from quart import request, Response
@@ -535,7 +551,7 @@ def sx_response(source: str, status: int = 200,
# For SX requests, prepend missing component definitions
comp_defs = ""
if request.headers.get("SX-Request"):
comp_defs = components_for_request(source)
comp_defs = components_for_request(source, extra_names=extra_component_names)
if comp_defs:
body = (f'<script type="text/sx" data-components>'
f'{comp_defs}</script>\n{body}')

View File

@@ -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,