""" Component dependency analysis. Walks component AST bodies to compute transitive dependency sets. A component's deps are all other components (~name references) it can potentially render, including through control flow branches. """ from __future__ import annotations from typing import Any from .types import Component, Macro, Symbol def _scan_ast(node: Any) -> set[str]: """Scan an AST node for ~component references. Walks all branches of control flow (if/when/cond/case) to find every component that *could* be rendered. Returns a set of component names (with ~ prefix). """ refs: set[str] = set() _walk(node, refs) return refs def _walk(node: Any, refs: set[str]) -> None: """Recursively walk an AST node collecting ~name references.""" if isinstance(node, Symbol): if node.name.startswith("~"): refs.add(node.name) return if isinstance(node, list): for item in node: _walk(item, refs) return if isinstance(node, dict): for v in node.values(): _walk(v, refs) return # Literals (str, int, float, bool, None, Keyword) — no refs return def transitive_deps(name: str, env: dict[str, Any]) -> set[str]: """Compute transitive component dependencies for *name*. Returns the set of all component names (with ~ prefix) that *name* can transitively render, NOT including *name* itself. """ seen: set[str] = set() def walk(n: str) -> None: if n in seen: return seen.add(n) val = env.get(n) if isinstance(val, Component): for dep in _scan_ast(val.body): walk(dep) elif isinstance(val, Macro): for dep in _scan_ast(val.body): walk(dep) key = name if name.startswith("~") else f"~{name}" walk(key) return seen - {key} def compute_all_deps(env: dict[str, Any]) -> None: """Compute and cache deps for all Component entries in *env*. Mutates each Component's ``deps`` field in place. """ for key, val in env.items(): if isinstance(val, Component): val.deps = transitive_deps(key, env) def scan_components_from_sx(source: str) -> set[str]: """Extract component names referenced in SX source text. Uses regex to find (~name patterns in serialized SX wire format. Returns names with ~ prefix, e.g. {"~card", "~nav-link"}. """ import re return set(re.findall(r'\(~([a-zA-Z_][a-zA-Z0-9_\-]*)', source)) def components_needed(page_sx: str, env: dict[str, Any]) -> set[str]: """Compute the full set of component names needed for a page. Scans *page_sx* for direct component references, then computes the transitive closure over the component dependency graph. Returns names with ~ prefix. """ # Direct refs from the page source direct = {f"~{n}" for n in scan_components_from_sx(page_sx)} # Transitive closure all_needed: set[str] = set() for name in direct: all_needed.add(name) val = env.get(name) if isinstance(val, Component) and val.deps: all_needed.update(val.deps) else: # deps not cached yet — compute on the fly all_needed.update(transitive_deps(name, env)) return all_needed