diff --git a/shared/static/styles/tailwind.config.js b/shared/static/styles/tailwind.config.js index a8ae93c..0a2e2ab 100644 --- a/shared/static/styles/tailwind.config.js +++ b/shared/static/styles/tailwind.config.js @@ -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', diff --git a/sx/sx/analyzer.sx b/sx/sx/analyzer.sx new file mode 100644 index 0000000..2a3a549 --- /dev/null +++ b/sx/sx/analyzer.sx @@ -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 "%"))))) diff --git a/sx/sx/nav-data.sx b/sx/sx/nav-data.sx index 61f75f9..2d75117 100644 --- a/sx/sx/nav-data.sx +++ b/sx/sx/nav-data.sx @@ -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" diff --git a/sx/sx/plans.sx b/sx/sx/plans.sx index 99a21eb..b5ac84f 100644 --- a/sx/sx/plans.sx +++ b/sx/sx/plans.sx @@ -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.")) diff --git a/sx/sxc/pages/docs.sx b/sx/sxc/pages/docs.sx index 686ffbf..78829b0 100644 --- a/sx/sxc/pages/docs.sx +++ b/sx/sxc/pages/docs.sx @@ -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)) diff --git a/sx/sxc/pages/helpers.py b/sx/sxc/pages/helpers.py index 0aaf364..709bb2a 100644 --- a/sx/sxc/pages/helpers.py +++ b/sx/sxc/pages/helpers.py @@ -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.