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 <details>/<summary> for zero-JS
expand/collapse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 13:59:55 +00:00
parent 65ed8a8941
commit fbfd203746
2 changed files with 82 additions and 27 deletions

View File

@@ -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))))

View File

@@ -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)