Add live bundle analyzer page to sx-docs
Demonstrates Phase 1 dep analysis in action: computes per-page component bundles for all sx-docs pages using the deps.sx transitive closure algorithm, showing needed vs total components with visual progress bars. - New page at /plans/bundle-analyzer with Python data helper - New components: ~bundle-analyzer-content, ~analyzer-stat, ~analyzer-row - Linked from Phase 1 section and Plans nav - Added sx/sx/ to tailwind content paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@ module.exports = {
|
||||
'/root/rose-ash/federation/sx/sx_components.py',
|
||||
'/root/rose-ash/account/sx/sx_components.py',
|
||||
'/root/rose-ash/orders/sx/sx_components.py',
|
||||
'/root/rose-ash/sx/sx/**/*.sx',
|
||||
'/root/rose-ash/sx/sxc/**/*.sx',
|
||||
'/root/rose-ash/sx/sxc/sx_components.py',
|
||||
'/root/rose-ash/sx/content/highlight.py',
|
||||
|
||||
67
sx/sx/analyzer.sx
Normal file
67
sx/sx/analyzer.sx
Normal file
@@ -0,0 +1,67 @@
|
||||
;; Bundle analyzer — live demonstration of Phase 1 component dependency analysis.
|
||||
;; Shows per-page component bundles vs total, visualizing payload savings.
|
||||
;; @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
|
||||
|
||||
(defcomp ~bundle-analyzer-content (&key pages total-components total-macros)
|
||||
(~doc-page :title "Page Bundle Analyzer"
|
||||
|
||||
(p :class "text-stone-600 mb-6"
|
||||
"Live analysis of component dependency graphs across all pages in this app. "
|
||||
"Each bar shows how many of the "
|
||||
(strong (str total-components))
|
||||
" total components a page actually needs, computed by the "
|
||||
(a :href "/specs/deps" :class "text-violet-700 underline" "deps.sx")
|
||||
" transitive closure algorithm.")
|
||||
|
||||
(div :class "mb-8 grid grid-cols-3 gap-4"
|
||||
(~analyzer-stat :label "Total Components" :value (str total-components)
|
||||
:cls "text-violet-600")
|
||||
(~analyzer-stat :label "Total Macros" :value (str total-macros)
|
||||
:cls "text-stone-600")
|
||||
(~analyzer-stat :label "Pages Analyzed" :value (str (length pages))
|
||||
:cls "text-green-600"))
|
||||
|
||||
(~doc-section :title "Per-Page Bundles" :id "bundles"
|
||||
(div :class "space-y-3"
|
||||
(map (fn (page)
|
||||
(~analyzer-row
|
||||
:name (get page "name")
|
||||
:path (get page "path")
|
||||
:needed (get page "needed")
|
||||
:direct (get page "direct")
|
||||
:total total-components
|
||||
:pct (get page "pct")
|
||||
:savings (get page "savings")))
|
||||
pages)))
|
||||
|
||||
(~doc-section :title "How It Works" :id "how"
|
||||
(ol :class "list-decimal pl-5 space-y-2 text-stone-700"
|
||||
(li (strong "Scan: ") "Regex finds all " (code "(~name") " patterns in the page's content expression.")
|
||||
(li (strong "Resolve: ") "Each referenced component's body AST is walked to find transitive " (code "~") " references.")
|
||||
(li (strong "Closure: ") "The full set is the union of direct + transitive deps, following chains through the component graph.")
|
||||
(li (strong "Bundle: ") "Only these component definitions are serialized into the page payload. Everything else is omitted."))
|
||||
(p :class "mt-4 text-stone-600"
|
||||
"The analysis handles circular references (via seen-set), "
|
||||
"walks all branches of control flow (if/when/cond/case), "
|
||||
"and includes macro definitions shared across components."))))
|
||||
|
||||
(defcomp ~analyzer-stat (&key label value cls)
|
||||
(div :class "rounded-lg border border-stone-200 p-4 text-center"
|
||||
(div :class (str "text-3xl font-bold " cls) value)
|
||||
(div :class "text-sm text-stone-500 mt-1" label)))
|
||||
|
||||
(defcomp ~analyzer-row (&key name path needed direct total pct savings)
|
||||
(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 "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 "%")))))
|
||||
@@ -105,6 +105,8 @@
|
||||
(define plans-nav-items (list
|
||||
(dict :label "Isomorphic Architecture" :href "/plans/isomorphic-architecture"
|
||||
:summary "Making the server/client boundary a sliding window — per-page bundles, smart expansion, SPA routing, client IO, streaming suspense.")
|
||||
(dict :label "Bundle Analyzer" :href "/plans/bundle-analyzer"
|
||||
:summary "Live per-page component dependency analysis — see which components each page needs and the payload savings.")
|
||||
(dict :label "Reader Macros" :href "/plans/reader-macros"
|
||||
:summary "Extensible parse-time transformations via # dispatch — datum comments, raw strings, and quote shorthand.")
|
||||
(dict :label "SX-Activity" :href "/plans/sx-activity"
|
||||
|
||||
@@ -624,7 +624,8 @@
|
||||
(div :class "rounded border border-green-300 bg-green-50 p-4 mb-4"
|
||||
(div :class "flex items-center gap-2 mb-2"
|
||||
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete")
|
||||
(a :href "/specs/deps" :class "text-green-700 underline text-sm font-medium" "View canonical spec: deps.sx"))
|
||||
(a :href "/specs/deps" :class "text-green-700 underline text-sm font-medium" "View canonical spec: deps.sx")
|
||||
(a :href "/plans/bundle-analyzer" :class "text-green-700 underline text-sm font-medium" "Live bundle analyzer"))
|
||||
(p :class "text-green-900 font-medium" "What it enables")
|
||||
(p :class "text-green-800" "Per-page component bundles instead of sending every definition to every page. Smaller payloads, faster boot, better cache hit rates."))
|
||||
|
||||
|
||||
@@ -417,3 +417,16 @@
|
||||
"reader-macros" (~plan-reader-macros-content)
|
||||
"sx-activity" (~plan-sx-activity-content)
|
||||
:else (~plans-index-content)))
|
||||
|
||||
(defpage bundle-analyzer
|
||||
:path "/plans/bundle-analyzer"
|
||||
:auth :public
|
||||
:layout (:sx-section
|
||||
:section "Plans"
|
||||
:sub-label "Plans"
|
||||
:sub-href "/plans/"
|
||||
:sub-nav (~section-nav :items plans-nav-items :current "Bundle Analyzer")
|
||||
:selected "Bundle Analyzer")
|
||||
:data (bundle-analyzer-data)
|
||||
:content (~bundle-analyzer-content
|
||||
:pages pages :total-components total-components :total-macros total-macros))
|
||||
|
||||
@@ -21,6 +21,7 @@ def _register_sx_helpers() -> None:
|
||||
"event-detail-data": _event_detail_data,
|
||||
"read-spec-file": _read_spec_file,
|
||||
"bootstrapper-data": _bootstrapper_data,
|
||||
"bundle-analyzer-data": _bundle_analyzer_data,
|
||||
})
|
||||
|
||||
|
||||
@@ -265,6 +266,44 @@ def _bootstrapper_data(target: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _bundle_analyzer_data() -> dict:
|
||||
"""Compute per-page component bundle analysis for the sx-docs app."""
|
||||
from shared.sx.jinja_bridge import get_component_env
|
||||
from shared.sx.pages import get_all_pages
|
||||
from shared.sx.deps import components_needed, scan_components_from_sx
|
||||
from shared.sx.parser import serialize
|
||||
from shared.sx.types import Component, Macro
|
||||
|
||||
env = get_component_env()
|
||||
total_components = sum(1 for v in env.values() if isinstance(v, Component))
|
||||
total_macros = sum(1 for v in env.values() if isinstance(v, Macro))
|
||||
|
||||
pages_data = []
|
||||
for name, page_def in sorted(get_all_pages("sx").items()):
|
||||
content_sx = serialize(page_def.content_expr)
|
||||
direct = scan_components_from_sx(content_sx)
|
||||
needed = components_needed(content_sx, env)
|
||||
n = len(needed)
|
||||
pct = round(n / total_components * 100) if total_components else 0
|
||||
savings = 100 - pct
|
||||
pages_data.append({
|
||||
"name": name,
|
||||
"path": page_def.path,
|
||||
"direct": len(direct),
|
||||
"needed": n,
|
||||
"pct": pct,
|
||||
"savings": savings,
|
||||
})
|
||||
|
||||
pages_data.sort(key=lambda p: p["needed"], reverse=True)
|
||||
|
||||
return {
|
||||
"pages": pages_data,
|
||||
"total-components": total_components,
|
||||
"total-macros": total_macros,
|
||||
}
|
||||
|
||||
|
||||
def _attr_detail_data(slug: str) -> dict:
|
||||
"""Return attribute detail data for a specific attribute slug.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user