diff --git a/lib/hyperscript/parser.sx b/lib/hyperscript/parser.sx index 154f2a77..b0423811 100644 --- a/lib/hyperscript/parser.sx +++ b/lib/hyperscript/parser.sx @@ -693,7 +693,12 @@ nil (do (when - (and (number? left) (= (tp-type) "ident")) + (and (number? left) (= (tp-type) "ident") + (not (or (= (tp-val) "starts") (= (tp-val) "ends") + (= (tp-val) "contains") (= (tp-val) "matches") + (= (tp-val) "is") (= (tp-val) "does") + (= (tp-val) "in") (= (tp-val) "precedes") + (= (tp-val) "follows")))) (let ((unit (tp-val))) (do diff --git a/shared/static/wasm/sx/hs-parser.sx b/shared/static/wasm/sx/hs-parser.sx index 154f2a77..b0423811 100644 --- a/shared/static/wasm/sx/hs-parser.sx +++ b/shared/static/wasm/sx/hs-parser.sx @@ -693,7 +693,12 @@ nil (do (when - (and (number? left) (= (tp-type) "ident")) + (and (number? left) (= (tp-type) "ident") + (not (or (= (tp-val) "starts") (= (tp-val) "ends") + (= (tp-val) "contains") (= (tp-val) "matches") + (= (tp-val) "is") (= (tp-val) "does") + (= (tp-val) "in") (= (tp-val) "precedes") + (= (tp-val) "follows")))) (let ((unit (tp-val))) (do diff --git a/spec/tests/test-hyperscript-behavioral.sx b/spec/tests/test-hyperscript-behavioral.sx index e7124539..7ba70023 100644 --- a/spec/tests/test-hyperscript-behavioral.sx +++ b/spec/tests/test-hyperscript-behavioral.sx @@ -20,7 +20,7 @@ ;; Compiles the expression, wraps in a thunk, evaluates, returns result. (define eval-hs (fn (src) - (let ((sx (hs-to-sx-from-source (str "return " src)))) + (let ((sx (hs-to-sx (hs-compile src)))) (let ((handler (eval-expr-cek (list (quote fn) (list (quote me)) (list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx))))) (handler nil))))) diff --git a/sx/sx/analyzer.sx b/sx/sx/analyzer.sx deleted file mode 100644 index 9409eb86..00000000 --- a/sx/sx/analyzer.sx +++ /dev/null @@ -1,120 +0,0 @@ -;; Bundle analyzer — live demonstration of dependency analysis + IO detection. -;; Shows per-page component bundles vs total, visualizing payload savings. -;; 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 bg-blue-500 bg-amber-500 - -(defcomp ~analyzer/bundle-analyzer-content (&key (pages :as list) (total-components :as number) (total-macros :as number) - (pure-count :as number) (io-count :as number)) - (~docs/page :title "Page Bundle Analyzer" - - (p (~tw :tokens "text-stone-600 mb-6") - "Live analysis of component dependency graphs and IO classification across all pages. " - "Each bar shows how many of the " - (strong (str total-components)) - " total components a page actually needs, computed by the " - (a :href "/sx/(language.(spec.deps))" (~tw :tokens "text-violet-700 underline") "deps.sx") - " transitive closure algorithm. " - "Click a page to see its component tree; expand a component to see its SX source.") - - (div (~tw :tokens "mb-8 grid grid-cols-4 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 "Pure Components" :value (str pure-count) - :cls "text-blue-600") - (~analyzer/stat :label "IO-Dependent" :value (str io-count) - :cls "text-amber-600")) - - (~docs/section :title "Per-Page Bundles" :id "bundles" - (div (~tw :tokens "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") - :io-refs (get page "io-refs") - :pure-in-page (get page "pure-in-page") - :io-in-page (get page "io-in-page") - :components (get page "components"))) - pages))) - - (~docs/section :title "How It Works" :id "how" - (ol (~tw :tokens "list-decimal pl-5 space-y-2 text-stone-700") - (li (strong "Scan: ") "Regex finds all " (code "(~plans/content-addressed-components/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.") - (li (strong "IO detect: ") "Each component body is scanned for references to IO primitives (frag, query, service, etc.). Components with zero transitive IO refs are pure — safe for client rendering.")) - (p (~tw :tokens "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 :as string) (value :as string) (cls :as string)) - (div (~tw :tokens "rounded-lg border border-stone-200 p-4 text-center") - (div :class (str "text-3xl font-bold " cls) value) - (div (~tw :tokens "text-sm text-stone-500 mt-1") label))) - -(defcomp ~analyzer/row (&key (name :as string) (path :as string) (needed :as number) (direct :as number) (total :as number) (pct :as number) (savings :as number) - (io-refs :as list) (pure-in-page :as number) (io-in-page :as number) (components :as list)) - (details (~tw :tokens "rounded border border-stone-200") - (summary (~tw :tokens "p-4 cursor-pointer hover:bg-stone-50 transition-colors") - (div (~tw :tokens "flex items-center justify-between mb-2") - (div - (span (~tw :tokens "font-mono font-semibold text-stone-800") name) - (span (~tw :tokens "text-stone-400 text-sm ml-2") path)) - (div (~tw :tokens "flex items-center gap-2") - (span (~tw :tokens "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 (~tw :tokens "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 (~tw :tokens "text-right") - (span (~tw :tokens "font-mono text-sm") - (span (~tw :tokens "text-violet-700 font-bold") (str needed)) - (span (~tw :tokens "text-stone-400") (str " / " total))) - (span (~tw :tokens "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 (~tw :tokens "w-full bg-stone-200 rounded-full h-2.5") - (div (~tw :tokens "bg-violet-600 h-2.5 rounded-full transition-all") - :style (str "width: " pct "%")))) - - ;; Component tree (shown when expanded) - (div (~tw :tokens "border-t border-stone-200 p-4 bg-stone-50") - (div (~tw :tokens "text-xs font-medium text-stone-500 uppercase tracking-wide mb-3") - (str needed " components in bundle")) - (div (~tw :tokens "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 :as string) (is-pure :as boolean) (io-refs :as list) (deps :as list) (source :as string)) - (details :class (str "rounded border " - (if is-pure "border-blue-200 bg-blue-50" "border-amber-200 bg-amber-50")) - (summary (~tw :tokens "px-3 py-2 cursor-pointer hover:opacity-80 transition-opacity") - (div (~tw :tokens "flex items-center justify-between") - (div (~tw :tokens "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 (~tw :tokens "font-mono text-sm font-medium text-stone-800") comp-name)) - (div (~tw :tokens "flex items-center gap-2") - (when (not (empty? io-refs)) - (span (~tw :tokens "text-xs text-amber-700") - (str "IO: " (join ", " io-refs)))) - (when (not (empty? deps)) - (span (~tw :tokens "text-xs text-stone-500") - (str (len deps) " deps")))))) - - ;; SX source (shown when component expanded) - (div (~tw :tokens "not-prose border-t border-stone-200 p-3 bg-stone-100 rounded-b") - (pre (~tw :tokens "text-xs leading-relaxed whitespace-pre-wrap overflow-x-auto") - (code (highlight source "lisp")))))) diff --git a/sx/sx/app-config.sx b/sx/sx/app-config.sx index 95aba90a..ccbae3a0 100644 --- a/sx/sx/app-config.sx +++ b/sx/sx/app-config.sx @@ -3,4 +3,6 @@ (dict-set! __app-config "handler-prefixes" - (append (get __app-config "handler-prefixes") (list "handler:hs-"))) + (append + (get __app-config "handler-prefixes") + (list "handler:hs-" "handler:gql-"))) diff --git a/sx/sx/applications/cssx/async/index.sx b/sx/sx/applications/cssx/async/index.sx new file mode 100644 index 00000000..5058e142 --- /dev/null +++ b/sx/sx/applications/cssx/async/index.sx @@ -0,0 +1,48 @@ +;; --------------------------------------------------------------------------- +;; Async CSS +;; --------------------------------------------------------------------------- +(defcomp () + (~docs/page :title "Async CSS" + + (~docs/section :title "The Pattern" :id "pattern" + (p "A CSSX component that needs CSS it doesn't have yet can " + (strong "fetch and cache it before rendering") ". This is just " + (code "~shared:pages/suspense") " combined with a style component — no new infrastructure:") + (highlight + "(defcomp ~cssx/styled (&key css-url css-hash fallback &rest children)\n (if (css-cached? css-hash)\n ;; Already have it — render immediately\n children\n ;; Don't have it — suspense while we fetch\n (~shared:pages/suspense :id (str \"css-\" css-hash)\n :fallback (or fallback (span \"\"))\n (do\n (fetch-css css-url css-hash)\n children))))" + "lisp") + (p "The consumer never knows:") + (highlight + "(~cssx/styled :css-url \"/css/charts.css\" :css-hash \"abc123\"\n (~bar-chart :data metrics))" + "lisp")) + + (~docs/section :title "Use Cases" :id "use-cases" + (~docs/subsection :title "Federated Components" + (p "A " (code "~cssx/btn") " from another site arrives via IPFS with a CID pointing " + "to its CSS. The component fetches and caches it before rendering. " + "No coordination needed between sites.") + (highlight + "(defcomp ~cssx/federated-widget (&key cid &rest children)\n (let ((css-cid (str cid \"/style.css\"))\n (cached (css-cached? css-cid)))\n (if cached\n children\n (~shared:pages/suspense :id (str \"fed-\" cid)\n :fallback (div :class \"animate-pulse bg-stone-100 rounded h-20\")\n (do (fetch-css (str \"https://ipfs.io/ipfs/\" css-cid) css-cid)\n children)))))" + "lisp")) + + (~docs/subsection :title "Heavy UI Libraries" + (p "Code editors, chart libraries, rich text editors — their CSS only loads " + "when the component actually appears on screen:") + (highlight + "(defcomp ~cssx/code-editor (&key language value on-change)\n (~cssx/styled :css-url \"/css/codemirror.css\" :css-hash (asset-hash \"codemirror\")\n :fallback (pre :class \"p-4 bg-stone-900 text-stone-300 rounded\" value)\n (div :class \"cm-editor\"\n :data-language language\n :data-value value)))" + "lisp")) + + (~docs/subsection :title "Lazy Themes" + (p "Theme CSS loads on first use, then is instant on subsequent visits:") + (highlight + "(defcomp ~cssx/lazy-theme (&key name &rest children)\n (let ((css-url (str \"/css/themes/\" name \".css\"))\n (hash (str \"theme-\" name)))\n (~cssx/styled :css-url css-url :css-hash hash\n :fallback children ;; render unstyled immediately\n children)))" + "lisp"))) + + (~docs/section :title "How It Composes" :id "composition" + (p "Async CSS composes with everything already in SX:") + (ul (~tw :tokens "list-disc pl-5 space-y-1 text-stone-700") + (li (code "~shared:pages/suspense") " handles the async gap with fallback content") + (li "localStorage handles caching across sessions") + (li (code "