From fbfd203746fce298277135810a9b943ba23d74c8 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 6 Mar 2026 13:59:55 +0000 Subject: [PATCH] Bundle analyzer: drill-down component tree with SX source viewer Click a page row to expand its component bundle tree. Each component shows pure/IO badge, IO refs, dep count. Click a component to expand its full defcomp SX source. Uses
/ for zero-JS expand/collapse. Co-Authored-By: Claude Opus 4.6 --- sx/sx/analyzer.sx | 86 +++++++++++++++++++++++++++++------------ sx/sxc/pages/helpers.py | 23 +++++++++-- 2 files changed, 82 insertions(+), 27 deletions(-) diff --git a/sx/sx/analyzer.sx b/sx/sx/analyzer.sx index 43853f8..3d7fe24 100644 --- a/sx/sx/analyzer.sx +++ b/sx/sx/analyzer.sx @@ -1,7 +1,7 @@ ;; Bundle analyzer — live demonstration of dependency analysis + IO detection. ;; Shows per-page component bundles vs total, visualizing payload savings. -;; Phase 2 adds IO classification: which components are pure vs IO-dependent. -;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-3 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 +;; Drill down into each bundle to see component tree; expand to see SX source. +;; @css bg-green-100 text-green-800 bg-violet-600 bg-stone-200 text-violet-600 text-stone-600 text-green-600 rounded-full h-2.5 grid-cols-3 bg-blue-100 text-blue-800 bg-amber-100 text-amber-800 grid-cols-4 marker:text-stone-400 bg-blue-50 bg-amber-50 text-blue-700 text-amber-700 border-blue-200 border-amber-200 (defcomp ~bundle-analyzer-content (&key pages total-components total-macros pure-count io-count) @@ -14,7 +14,7 @@ " total components a page actually needs, computed by the " (a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx") " transitive closure algorithm. " - "Phase 2 IO detection classifies each component as pure or IO-dependent.") + "Click a page to see its component tree; expand a component to see its SX source.") (div :class "mb-8 grid grid-cols-4 gap-4" (~analyzer-stat :label "Total Components" :value (str total-components) @@ -39,7 +39,8 @@ :savings (get page "savings") :io-refs (get page "io-refs") :pure-in-page (get page "pure-in-page") - :io-in-page (get page "io-in-page"))) + :io-in-page (get page "io-in-page") + :components (get page "components"))) pages))) (~doc-section :title "How It Works" :id "how" @@ -60,23 +61,60 @@ (div :class "text-sm text-stone-500 mt-1" label))) (defcomp ~analyzer-row (&key name path needed direct total pct savings - io-refs pure-in-page io-in-page) - (div :class "rounded border border-stone-200 p-4" - (div :class "flex items-center justify-between mb-2" - (div - (span :class "font-mono font-semibold text-stone-800" name) - (span :class "text-stone-400 text-sm ml-2" path)) - (div :class "flex items-center gap-2" - (span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800" - (str pure-in-page " pure")) - (span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800" - (str io-in-page " IO")) - (div :class "text-right" - (span :class "font-mono text-sm" - (span :class "text-violet-700 font-bold" (str needed)) - (span :class "text-stone-400" (str " / " total))) - (span :class "ml-2 inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" - (str savings "% saved"))))) - (div :class "w-full bg-stone-200 rounded-full h-2.5" - (div :class "bg-violet-600 h-2.5 rounded-full transition-all" - :style (str "width: " pct "%"))))) + io-refs pure-in-page io-in-page components) + (details :class "rounded border border-stone-200" + (summary :class "p-4 cursor-pointer hover:bg-stone-50 transition-colors" + (div :class "flex items-center justify-between mb-2" + (div + (span :class "font-mono font-semibold text-stone-800" name) + (span :class "text-stone-400 text-sm ml-2" path)) + (div :class "flex items-center gap-2" + (span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800" + (str pure-in-page " pure")) + (span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800" + (str io-in-page " IO")) + (div :class "text-right" + (span :class "font-mono text-sm" + (span :class "text-violet-700 font-bold" (str needed)) + (span :class "text-stone-400" (str " / " total))) + (span :class "ml-2 inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800" + (str savings "% saved"))))) + (div :class "w-full bg-stone-200 rounded-full h-2.5" + (div :class "bg-violet-600 h-2.5 rounded-full transition-all" + :style (str "width: " pct "%")))) + + ;; Component tree (shown when expanded) + (div :class "border-t border-stone-200 p-4 bg-stone-50" + (div :class "text-xs font-medium text-stone-500 uppercase tracking-wide mb-3" + (str needed " components in bundle")) + (div :class "space-y-1" + (map (fn (comp) + (~analyzer-component + :comp-name (get comp "name") + :is-pure (get comp "is-pure") + :io-refs (get comp "io-refs") + :deps (get comp "deps") + :source (get comp "source"))) + components))))) + +(defcomp ~analyzer-component (&key comp-name is-pure io-refs deps source) + (details :class (str "rounded border " + (if is-pure "border-blue-200 bg-blue-50" "border-amber-200 bg-amber-50")) + (summary :class "px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity" + (div :class "flex items-center justify-between" + (div :class "flex items-center gap-2" + (span :class (str "inline-block w-2 h-2 rounded-full " + (if is-pure "bg-blue-500" "bg-amber-500"))) + (span :class "font-mono text-sm font-medium text-stone-800" comp-name)) + (div :class "flex items-center gap-2" + (when (not (empty? io-refs)) + (span :class "text-xs text-amber-700" + (str "IO: " (join ", " io-refs)))) + (when (not (empty? deps)) + (span :class "text-xs text-stone-500" + (str (len deps) " deps")))))) + + ;; SX source (shown when component expanded) + (div :class "border-t border-stone-200 p-3" + (pre :class "text-xs font-mono text-stone-700 whitespace-pre-wrap overflow-x-auto leading-relaxed" + source)))) diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 03ff445..ae35aaf 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -289,18 +289,34 @@ def _bundle_analyzer_data() -> dict: pct = round(n / total_components * 100) if total_components else 0 savings = 100 - pct - # IO classification for components in this page + # IO classification + component details for this page pure_in_page = 0 io_in_page = 0 page_io_refs: set[str] = set() - for comp_name in needed: + comp_details = [] + for comp_name in sorted(needed): val = env.get(comp_name) if isinstance(val, Component): - if val.is_pure: + is_pure = val.is_pure + if is_pure: pure_in_page += 1 else: io_in_page += 1 page_io_refs.update(val.io_refs) + # Reconstruct defcomp source + param_strs = ["&key"] + list(val.params) + if val.has_children: + param_strs.extend(["&rest", "children"]) + params_sx = "(" + " ".join(param_strs) + ")" + body_sx = serialize(val.body, pretty=True) + source = f"(defcomp ~{val.name} {params_sx}\n {body_sx})" + comp_details.append({ + "name": comp_name, + "is-pure": is_pure, + "io-refs": sorted(val.io_refs), + "deps": sorted(val.deps), + "source": source, + }) pages_data.append({ "name": name, @@ -312,6 +328,7 @@ def _bundle_analyzer_data() -> dict: "io-refs": len(page_io_refs), "pure-in-page": pure_in_page, "io-in-page": io_in_page, + "components": comp_details, }) pages_data.sort(key=lambda p: p["needed"], reverse=True)