diff --git a/shared/sx/layouts.py b/shared/sx/layouts.py index e21c649..e4d3f79 100644 --- a/shared/sx/layouts.py +++ b/shared/sx/layouts.py @@ -22,7 +22,7 @@ from typing import Any, Callable, Awaitable class Layout: """A named layout that generates header rows for full and OOB rendering.""" - __slots__ = ("name", "_full_fn", "_oob_fn", "_mobile_fn") + __slots__ = ("name", "_full_fn", "_oob_fn", "_mobile_fn", "component_names") def __init__( self, @@ -30,11 +30,13 @@ class Layout: full_fn: Callable[..., str | Awaitable[str]], oob_fn: Callable[..., str | Awaitable[str]], mobile_fn: Callable[..., str | Awaitable[str]] | None = None, + component_names: list[str] | None = None, ): self.name = name self._full_fn = full_fn self._oob_fn = oob_fn self._mobile_fn = mobile_fn + self.component_names = component_names or [] async def full_headers(self, ctx: dict, **kwargs: Any) -> str: result = self._full_fn(ctx, **kwargs) @@ -109,12 +111,14 @@ def register_sx_layout(name: str, full_defcomp: str, oob_defcomp: str, return await _render_to_sx_with_env(oob_defcomp, env) mobile_fn = None + comp_names = [f"~{full_defcomp}", f"~{oob_defcomp}"] if mobile_defcomp: async def mobile_fn(ctx: dict, **kw: Any) -> str: env = {k.replace("_", "-"): v for k, v in kw.items()} return await _render_to_sx_with_env(mobile_defcomp, env) + comp_names.append(f"~{mobile_defcomp}") - register_layout(Layout(name, full_fn, oob_fn, mobile_fn)) + register_layout(Layout(name, full_fn, oob_fn, mobile_fn, comp_names)) # Register built-in layouts via .sx defcomps diff --git a/shared/sx/pages.py b/shared/sx/pages.py index 98aa704..7f85c3d 100644 --- a/shared/sx/pages.py +++ b/shared/sx/pages.py @@ -426,9 +426,15 @@ async def execute_page_streaming( content=SxExpr(suspense_content_sx), ) - # Pass the SX source for component scanning (resolution scripts may - # contain component calls that the client needs to render) - page_sx_for_scan = f'(~app-body :header-rows {suspense_header_sx} :content {suspense_content_sx})' + # Include layout component refs + page content so the scan picks up + # their transitive deps (e.g. ~cart-mini, ~auth-menu in headers). + layout_refs = "" + if layout is not None and hasattr(layout, "component_names"): + layout_refs = " ".join(f"({n})" for n in layout.component_names) + content_ref = "" + if page_def.content_expr is not None: + content_ref = sx_serialize(page_def.content_expr) + page_sx_for_scan = f'(<> {layout_refs} {content_ref} (~app-body :header-rows {suspense_header_sx} :content {suspense_content_sx}))' shell, tail = sx_page_streaming_parts( tctx, initial_page_html, page_sx=page_sx_for_scan, )