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.
|
;; Bundle analyzer — live demonstration of dependency analysis + IO detection.
|
||||||
;; Shows per-page component bundles vs total, visualizing payload savings.
|
;; Shows per-page component bundles vs total, visualizing payload savings.
|
||||||
;; Phase 2 adds IO classification: which components are pure vs IO-dependent.
|
;; 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
|
;; @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
|
(defcomp ~bundle-analyzer-content (&key pages total-components total-macros
|
||||||
pure-count io-count)
|
pure-count io-count)
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
" total components a page actually needs, computed by the "
|
" total components a page actually needs, computed by the "
|
||||||
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
|
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
|
||||||
" transitive closure algorithm. "
|
" 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"
|
(div :class "mb-8 grid grid-cols-4 gap-4"
|
||||||
(~analyzer-stat :label "Total Components" :value (str total-components)
|
(~analyzer-stat :label "Total Components" :value (str total-components)
|
||||||
@@ -39,7 +39,8 @@
|
|||||||
:savings (get page "savings")
|
:savings (get page "savings")
|
||||||
:io-refs (get page "io-refs")
|
:io-refs (get page "io-refs")
|
||||||
:pure-in-page (get page "pure-in-page")
|
: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)))
|
pages)))
|
||||||
|
|
||||||
(~doc-section :title "How It Works" :id "how"
|
(~doc-section :title "How It Works" :id "how"
|
||||||
@@ -60,23 +61,60 @@
|
|||||||
(div :class "text-sm text-stone-500 mt-1" label)))
|
(div :class "text-sm text-stone-500 mt-1" label)))
|
||||||
|
|
||||||
(defcomp ~analyzer-row (&key name path needed direct total pct savings
|
(defcomp ~analyzer-row (&key name path needed direct total pct savings
|
||||||
io-refs pure-in-page io-in-page)
|
io-refs pure-in-page io-in-page components)
|
||||||
(div :class "rounded border border-stone-200 p-4"
|
(details :class "rounded border border-stone-200"
|
||||||
(div :class "flex items-center justify-between mb-2"
|
(summary :class "p-4 cursor-pointer hover:bg-stone-50 transition-colors"
|
||||||
(div
|
(div :class "flex items-center justify-between mb-2"
|
||||||
(span :class "font-mono font-semibold text-stone-800" name)
|
(div
|
||||||
(span :class "text-stone-400 text-sm ml-2" path))
|
(span :class "font-mono font-semibold text-stone-800" name)
|
||||||
(div :class "flex items-center gap-2"
|
(span :class "text-stone-400 text-sm ml-2" path))
|
||||||
(span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"
|
(div :class "flex items-center gap-2"
|
||||||
(str pure-in-page " pure"))
|
(span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800"
|
||||||
(span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800"
|
(str pure-in-page " pure"))
|
||||||
(str io-in-page " IO"))
|
(span :class "inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800"
|
||||||
(div :class "text-right"
|
(str io-in-page " IO"))
|
||||||
(span :class "font-mono text-sm"
|
(div :class "text-right"
|
||||||
(span :class "text-violet-700 font-bold" (str needed))
|
(span :class "font-mono text-sm"
|
||||||
(span :class "text-stone-400" (str " / " total)))
|
(span :class "text-violet-700 font-bold" (str needed))
|
||||||
(span :class "ml-2 inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800"
|
(span :class "text-stone-400" (str " / " total)))
|
||||||
(str savings "% saved")))))
|
(span :class "ml-2 inline-block px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800"
|
||||||
(div :class "w-full bg-stone-200 rounded-full h-2.5"
|
(str savings "% saved")))))
|
||||||
(div :class "bg-violet-600 h-2.5 rounded-full transition-all"
|
(div :class "w-full bg-stone-200 rounded-full h-2.5"
|
||||||
:style (str "width: " pct "%")))))
|
(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
|
pct = round(n / total_components * 100) if total_components else 0
|
||||||
savings = 100 - pct
|
savings = 100 - pct
|
||||||
|
|
||||||
# IO classification for components in this page
|
# IO classification + component details for this page
|
||||||
pure_in_page = 0
|
pure_in_page = 0
|
||||||
io_in_page = 0
|
io_in_page = 0
|
||||||
page_io_refs: set[str] = set()
|
page_io_refs: set[str] = set()
|
||||||
for comp_name in needed:
|
comp_details = []
|
||||||
|
for comp_name in sorted(needed):
|
||||||
val = env.get(comp_name)
|
val = env.get(comp_name)
|
||||||
if isinstance(val, Component):
|
if isinstance(val, Component):
|
||||||
if val.is_pure:
|
is_pure = val.is_pure
|
||||||
|
if is_pure:
|
||||||
pure_in_page += 1
|
pure_in_page += 1
|
||||||
else:
|
else:
|
||||||
io_in_page += 1
|
io_in_page += 1
|
||||||
page_io_refs.update(val.io_refs)
|
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({
|
pages_data.append({
|
||||||
"name": name,
|
"name": name,
|
||||||
@@ -312,6 +328,7 @@ def _bundle_analyzer_data() -> dict:
|
|||||||
"io-refs": len(page_io_refs),
|
"io-refs": len(page_io_refs),
|
||||||
"pure-in-page": pure_in_page,
|
"pure-in-page": pure_in_page,
|
||||||
"io-in-page": io_in_page,
|
"io-in-page": io_in_page,
|
||||||
|
"components": comp_details,
|
||||||
})
|
})
|
||||||
|
|
||||||
pages_data.sort(key=lambda p: p["needed"], reverse=True)
|
pages_data.sort(key=lambda p: p["needed"], reverse=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user