Implement isomorphic Phase 1: per-page component bundling
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m43s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m43s
Add component dependency analyzer (shared/sx/deps.py) that walks component AST bodies to compute transitive dependency sets. sx_page() and sx_response() now send only the components each page needs instead of the entire registry. Changes: - New: shared/sx/deps.py — transitive_deps(), components_needed(), scan_components_from_sx(), compute_all_deps() - shared/sx/types.py — Add deps: set[str] field to Component - shared/sx/jinja_bridge.py — Compute deps on registration, add components_for_page() and css_classes_for_page() - shared/sx/helpers.py — sx_page() uses per-page bundle + hash, sx_response() passes source to components_for_request() for page-scoped component diffing - New: shared/sx/tests/test_deps.py — 15 tests covering AST scanning, transitive deps, circular refs, per-page bundling Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -456,34 +456,39 @@ def sx_call(component_name: str, **kwargs: Any) -> str:
|
||||
|
||||
|
||||
|
||||
def components_for_request() -> str:
|
||||
def components_for_request(source: str = "") -> str:
|
||||
"""Return defcomp/defmacro source for definitions the client doesn't have yet.
|
||||
|
||||
Reads the ``SX-Components`` header (comma-separated component names
|
||||
like ``~card,~nav-item``) and returns only the definitions the client
|
||||
is missing. If the header is absent, returns all defs.
|
||||
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.
|
||||
"""
|
||||
from quart import request
|
||||
from .jinja_bridge import client_components_tag, _COMPONENT_ENV
|
||||
from .jinja_bridge import _COMPONENT_ENV
|
||||
from .deps import components_needed
|
||||
from .types import Component, Macro
|
||||
from .parser import serialize
|
||||
|
||||
loaded_raw = request.headers.get("SX-Components", "")
|
||||
if not loaded_raw:
|
||||
# Client has nothing — send all
|
||||
tag = client_components_tag()
|
||||
if not tag:
|
||||
return ""
|
||||
start = tag.find(">") + 1
|
||||
end = tag.rfind("</script>")
|
||||
return tag[start:end] if start > 0 and end > start else ""
|
||||
# Determine which components the page needs
|
||||
if source:
|
||||
needed = components_needed(source, _COMPONENT_ENV)
|
||||
else:
|
||||
needed = None # all
|
||||
|
||||
loaded_raw = request.headers.get("SX-Components", "")
|
||||
loaded = set(loaded_raw.split(",")) if loaded_raw else set()
|
||||
|
||||
loaded = set(loaded_raw.split(","))
|
||||
parts = []
|
||||
for key, val in _COMPONENT_ENV.items():
|
||||
if isinstance(val, Component):
|
||||
comp_name = f"~{val.name}"
|
||||
# Skip if not needed for this page
|
||||
if needed is not None and comp_name not in needed and key not in needed:
|
||||
continue
|
||||
# Skip components the client already has
|
||||
if f"~{val.name}" in loaded or val.name in loaded:
|
||||
if comp_name in loaded or val.name in loaded:
|
||||
continue
|
||||
# Reconstruct defcomp source
|
||||
param_strs = ["&key"] + list(val.params)
|
||||
@@ -530,7 +535,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()
|
||||
comp_defs = components_for_request(source)
|
||||
if comp_defs:
|
||||
body = (f'<script type="text/sx" data-components>'
|
||||
f'{comp_defs}</script>\n{body}')
|
||||
@@ -641,11 +646,11 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
renders everything client-side. CSS rules are scanned from the sx
|
||||
source and component defs, then injected as a <style> block.
|
||||
"""
|
||||
from .jinja_bridge import client_components_tag, _COMPONENT_ENV, get_component_hash
|
||||
from .css_registry import scan_classes_from_sx, lookup_rules, get_preamble, registry_loaded, store_css_hash
|
||||
from .types import Component
|
||||
from .jinja_bridge import components_for_page, css_classes_for_page
|
||||
from .css_registry import lookup_rules, get_preamble, registry_loaded, store_css_hash
|
||||
|
||||
component_hash = get_component_hash()
|
||||
# Per-page component bundle: only definitions this page needs
|
||||
component_defs, component_hash = components_for_page(page_sx)
|
||||
|
||||
# Check if client already has this version cached (via cookie)
|
||||
# In dev mode, always send full source so edits are visible immediately
|
||||
@@ -653,28 +658,13 @@ def sx_page(ctx: dict, page_sx: str, *,
|
||||
if not _is_dev_mode() and client_hash and client_hash == component_hash:
|
||||
# Client has current components cached — send empty source
|
||||
component_defs = ""
|
||||
else:
|
||||
components_tag = client_components_tag()
|
||||
# Extract just the inner source from the <script> tag
|
||||
component_defs = ""
|
||||
if components_tag:
|
||||
start = components_tag.find(">") + 1
|
||||
end = components_tag.rfind("</script>")
|
||||
if start > 0 and end > start:
|
||||
component_defs = components_tag[start:end]
|
||||
|
||||
# Scan for CSS classes — use pre-computed sets for components, scan page sx at request time
|
||||
# Scan for CSS classes — only from components this page uses + page source
|
||||
sx_css = ""
|
||||
sx_css_classes = ""
|
||||
sx_css_hash = ""
|
||||
if registry_loaded():
|
||||
# Union pre-computed component classes instead of re-scanning source
|
||||
classes: set[str] = set()
|
||||
for val in _COMPONENT_ENV.values():
|
||||
if isinstance(val, Component) and val.css_classes:
|
||||
classes.update(val.css_classes)
|
||||
# Page sx is unique per request — scan it
|
||||
classes.update(scan_classes_from_sx(page_sx))
|
||||
classes = css_classes_for_page(page_sx)
|
||||
# Always include body classes
|
||||
classes.update(["bg-stone-50", "text-stone-900"])
|
||||
rules = lookup_rules(classes)
|
||||
|
||||
Reference in New Issue
Block a user