Include all :data page component deps in every page's client bundle

Per-page bundling now unions deps from all :data pages in the service,
so navigating between data pages uses client-side rendering + cache
instead of expensive server fetch + SX parse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 01:26:39 +00:00
parent fa295acfe3
commit 2c56d3e14b
2 changed files with 30 additions and 6 deletions

View File

@@ -740,8 +740,9 @@ def sx_page(ctx: dict, page_sx: str, *,
from .jinja_bridge import components_for_page, css_classes_for_page from .jinja_bridge import components_for_page, css_classes_for_page
from .css_registry import lookup_rules, get_preamble, registry_loaded, store_css_hash from .css_registry import lookup_rules, get_preamble, registry_loaded, store_css_hash
# Per-page component bundle: only definitions this page needs # Per-page component bundle: this page's deps + all :data page deps
component_defs, component_hash = components_for_page(page_sx) from quart import current_app as _ca
component_defs, component_hash = components_for_page(page_sx, service=_ca.name)
# Check if client already has this version cached (via cookie) # Check if client already has this version cached (via cookie)
# In dev mode, always send full source so edits are visible immediately # In dev mode, always send full source so edits are visible immediately
@@ -755,7 +756,7 @@ def sx_page(ctx: dict, page_sx: str, *,
sx_css_classes = "" sx_css_classes = ""
sx_css_hash = "" sx_css_hash = ""
if registry_loaded(): if registry_loaded():
classes = css_classes_for_page(page_sx) classes = css_classes_for_page(page_sx, service=_ca.name)
# Always include body classes # Always include body classes
classes.update(["bg-stone-50", "text-stone-900"]) classes.update(["bg-stone-50", "text-stone-900"])
rules = lookup_rules(classes) rules = lookup_rules(classes)

View File

@@ -332,17 +332,32 @@ def client_components_tag(*names: str) -> str:
return f'<script type="text/sx" data-components>{source}</script>' return f'<script type="text/sx" data-components>{source}</script>'
def components_for_page(page_sx: str) -> tuple[str, str]: def components_for_page(page_sx: str, service: str | None = None) -> tuple[str, str]:
"""Return (component_defs_source, page_hash) for a page. """Return (component_defs_source, page_hash) for a page.
Scans *page_sx* for component references, computes the transitive Scans *page_sx* for component references, computes the transitive
closure, and returns only the definitions needed for this page. closure, and returns only the definitions needed for this page.
When *service* is given, also includes deps for all :data pages
in that service so the client can render them without a server
roundtrip on navigation.
The hash is computed from the page-specific bundle for caching. The hash is computed from the page-specific bundle for caching.
""" """
from .deps import components_needed from .deps import components_needed
from .parser import serialize from .parser import serialize
needed = components_needed(page_sx, _COMPONENT_ENV) needed = components_needed(page_sx, _COMPONENT_ENV)
# Include deps for all :data pages so the client can render them
if service:
from .pages import get_all_pages
for page_def in get_all_pages(service).values():
if page_def.data_expr is not None and page_def.content_expr is not None:
content_src = serialize(page_def.content_expr)
data_deps = components_needed(content_src, _COMPONENT_ENV)
needed |= data_deps
if not needed: if not needed:
return "", "" return "", ""
@@ -375,16 +390,24 @@ def components_for_page(page_sx: str) -> tuple[str, str]:
return source, digest return source, digest
def css_classes_for_page(page_sx: str) -> set[str]: def css_classes_for_page(page_sx: str, service: str | None = None) -> set[str]:
"""Return CSS classes needed for a page's component bundle + page source. """Return CSS classes needed for a page's component bundle + page source.
Instead of unioning ALL component CSS classes, only includes classes Instead of unioning ALL component CSS classes, only includes classes
from components the page actually uses. from components the page actually uses (plus all :data page deps).
""" """
from .deps import components_needed from .deps import components_needed
from .css_registry import scan_classes_from_sx from .css_registry import scan_classes_from_sx
from .parser import serialize
needed = components_needed(page_sx, _COMPONENT_ENV) needed = components_needed(page_sx, _COMPONENT_ENV)
if service:
from .pages import get_all_pages
for page_def in get_all_pages(service).values():
if page_def.data_expr is not None and page_def.content_expr is not None:
content_src = serialize(page_def.content_expr)
needed |= components_needed(content_src, _COMPONENT_ENV)
classes: set[str] = set() classes: set[str] = set()
for key, val in _COMPONENT_ENV.items(): for key, val in _COMPONENT_ENV.items():