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:
@@ -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.
|
"""Return defcomp/defmacro source for definitions the client doesn't have yet.
|
||||||
|
|
||||||
Reads the ``SX-Components`` header (comma-separated component names
|
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
|
is missing. If *source* is provided, only sends components needed
|
||||||
for that source (plus transitive deps). If the header is absent,
|
for that source (plus transitive deps). If the header is absent,
|
||||||
returns all needed defs.
|
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 quart import request
|
||||||
from .jinja_bridge import _COMPONENT_ENV
|
from .jinja_bridge import _COMPONENT_ENV
|
||||||
@@ -477,6 +482,12 @@ def components_for_request(source: str = "") -> str:
|
|||||||
else:
|
else:
|
||||||
needed = None # all
|
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_raw = request.headers.get("SX-Components", "")
|
||||||
loaded = set(loaded_raw.split(",")) if loaded_raw else set()
|
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,
|
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.
|
"""Return an s-expression wire-format response.
|
||||||
|
|
||||||
Takes a raw sx string::
|
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
|
For SX requests, missing component definitions are prepended as a
|
||||||
``<script type="text/sx" data-components>`` block so the client
|
``<script type="text/sx" data-components>`` block so the client
|
||||||
can process them before rendering OOB content.
|
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
|
from quart import request, Response
|
||||||
|
|
||||||
@@ -535,7 +551,7 @@ def sx_response(source: str, status: int = 200,
|
|||||||
# For SX requests, prepend missing component definitions
|
# For SX requests, prepend missing component definitions
|
||||||
comp_defs = ""
|
comp_defs = ""
|
||||||
if request.headers.get("SX-Request"):
|
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:
|
if comp_defs:
|
||||||
body = (f'<script type="text/sx" data-components>'
|
body = (f'<script type="text/sx" data-components>'
|
||||||
f'{comp_defs}</script>\n{body}')
|
f'{comp_defs}</script>\n{body}')
|
||||||
|
|||||||
@@ -279,13 +279,25 @@ async def execute_page(
|
|||||||
is_htmx = is_htmx_request()
|
is_htmx = is_htmx_request()
|
||||||
|
|
||||||
if is_htmx:
|
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(
|
return sx_response(await oob_page_sx(
|
||||||
oobs=oob_headers if oob_headers else "",
|
oobs=oob_headers if oob_headers else "",
|
||||||
filter=filter_sx,
|
filter=filter_sx,
|
||||||
aside=aside_sx,
|
aside=aside_sx,
|
||||||
content=content_sx,
|
content=content_sx,
|
||||||
menu=menu_sx,
|
menu=menu_sx,
|
||||||
))
|
), extra_component_names=extra_deps)
|
||||||
else:
|
else:
|
||||||
return await full_page_sx(
|
return await full_page_sx(
|
||||||
tctx,
|
tctx,
|
||||||
|
|||||||
Reference in New Issue
Block a user