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:
@@ -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))))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user