boot-init prints SX_VERSION (build timestamp) to console on startup. tryClientRoute logs why it falls through: has-data, no content, eval failed, #main-panel not found. tryEvalContent logs the actual error. Added logWarn platform function. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
416 lines
16 KiB
Plaintext
416 lines
16 KiB
Plaintext
;; ==========================================================================
|
|
;; boot.sx — Browser boot, mount, hydrate, script processing
|
|
;;
|
|
;; Handles the browser startup lifecycle:
|
|
;; 1. CSS tracking init
|
|
;; 2. Style dictionary loading (from <script type="text/sx-styles">)
|
|
;; 3. Component script processing (from <script type="text/sx">)
|
|
;; 4. Hydration of [data-sx] elements
|
|
;; 5. Engine element processing
|
|
;;
|
|
;; Also provides the public mounting/hydration API:
|
|
;; mount, hydrate, update, render-component
|
|
;;
|
|
;; Depends on:
|
|
;; cssx.sx — load-style-dict
|
|
;; orchestration.sx — process-elements, engine-init
|
|
;; adapter-dom.sx — render-to-dom
|
|
;; render.sx — shared registries
|
|
;; ==========================================================================
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Head element hoisting (full version)
|
|
;; --------------------------------------------------------------------------
|
|
;; Moves <meta>, <title>, <link rel=canonical>, <script type=application/ld+json>
|
|
;; from rendered content to <head>, deduplicating as needed.
|
|
|
|
(define HEAD_HOIST_SELECTOR
|
|
"meta, title, link[rel='canonical'], script[type='application/ld+json']")
|
|
|
|
(define hoist-head-elements-full
|
|
(fn (root)
|
|
(let ((els (dom-query-all root HEAD_HOIST_SELECTOR)))
|
|
(for-each
|
|
(fn (el)
|
|
(let ((tag (lower (dom-tag-name el))))
|
|
(cond
|
|
;; <title> — replace document title
|
|
(= tag "title")
|
|
(do
|
|
(set-document-title (dom-text-content el))
|
|
(dom-remove-child (dom-parent el) el))
|
|
|
|
;; <meta> — deduplicate by name or property
|
|
(= tag "meta")
|
|
(do
|
|
(let ((name (dom-get-attr el "name"))
|
|
(prop (dom-get-attr el "property")))
|
|
(when name
|
|
(remove-head-element (str "meta[name=\"" name "\"]")))
|
|
(when prop
|
|
(remove-head-element (str "meta[property=\"" prop "\"]"))))
|
|
(dom-remove-child (dom-parent el) el)
|
|
(dom-append-to-head el))
|
|
|
|
;; <link rel=canonical> — deduplicate
|
|
(and (= tag "link")
|
|
(= (dom-get-attr el "rel") "canonical"))
|
|
(do
|
|
(remove-head-element "link[rel=\"canonical\"]")
|
|
(dom-remove-child (dom-parent el) el)
|
|
(dom-append-to-head el))
|
|
|
|
;; Everything else (ld+json, etc.) — just move
|
|
:else
|
|
(do
|
|
(dom-remove-child (dom-parent el) el)
|
|
(dom-append-to-head el)))))
|
|
els))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Mount — render SX source into a DOM element
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define sx-mount
|
|
(fn (target source extra-env)
|
|
;; Render SX source string into target element.
|
|
;; target: Element or CSS selector string
|
|
;; source: SX source string
|
|
;; extra-env: optional extra bindings dict
|
|
(let ((el (resolve-mount-target target)))
|
|
(when el
|
|
(let ((node (sx-render-with-env source extra-env)))
|
|
(dom-set-text-content el "")
|
|
(dom-append el node)
|
|
;; Hoist head elements from rendered content
|
|
(hoist-head-elements-full el)
|
|
;; Process sx- attributes and hydrate
|
|
(process-elements el)
|
|
(sx-hydrate-elements el))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Hydrate — render all [data-sx] elements
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define sx-hydrate-elements
|
|
(fn (root)
|
|
;; Find all [data-sx] elements within root and render them.
|
|
(let ((els (dom-query-all (or root (dom-body)) "[data-sx]")))
|
|
(for-each
|
|
(fn (el)
|
|
(when (not (is-processed? el "hydrated"))
|
|
(mark-processed! el "hydrated")
|
|
(sx-update-element el nil)))
|
|
els))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Update — re-render a [data-sx] element with new env data
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define sx-update-element
|
|
(fn (el new-env)
|
|
;; Re-render a [data-sx] element.
|
|
;; Reads source from data-sx attr, base env from data-sx-env attr.
|
|
(let ((target (resolve-mount-target el)))
|
|
(when target
|
|
(let ((source (dom-get-attr target "data-sx")))
|
|
(when source
|
|
(let ((base-env (parse-env-attr target))
|
|
(env (merge-envs base-env new-env)))
|
|
(let ((node (sx-render-with-env source env)))
|
|
(dom-set-text-content target "")
|
|
(dom-append target node)
|
|
;; Update stored env if new-env provided
|
|
(when new-env
|
|
(store-env-attr target base-env new-env))))))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Render component — build synthetic call from kwargs dict
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define sx-render-component
|
|
(fn (name kwargs extra-env)
|
|
;; Render a named component with keyword args.
|
|
;; name: component name (with or without ~ prefix)
|
|
;; kwargs: dict of param-name → value
|
|
;; extra-env: optional extra env bindings
|
|
(let ((full-name (if (starts-with? name "~") name (str "~" name))))
|
|
(let ((env (get-render-env extra-env))
|
|
(comp (env-get env full-name)))
|
|
(if (not (component? comp))
|
|
(error (str "Unknown component: " full-name))
|
|
;; Build synthetic call expression
|
|
(let ((call-expr (list (make-symbol full-name))))
|
|
(for-each
|
|
(fn (k)
|
|
(append! call-expr (make-keyword (to-kebab k)))
|
|
(append! call-expr (dict-get kwargs k)))
|
|
(keys kwargs))
|
|
(render-to-dom call-expr env nil)))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Script processing — <script type="text/sx">
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define process-sx-scripts
|
|
(fn (root)
|
|
;; Process all <script type="text/sx"> tags.
|
|
;; - data-components + data-hash → localStorage cache
|
|
;; - data-mount="<selector>" → render into target
|
|
;; - Default: load as components
|
|
(let ((scripts (query-sx-scripts root)))
|
|
(for-each
|
|
(fn (s)
|
|
(when (not (is-processed? s "script"))
|
|
(mark-processed! s "script")
|
|
(let ((text (dom-text-content s)))
|
|
(cond
|
|
;; Component definitions
|
|
(dom-has-attr? s "data-components")
|
|
(process-component-script s text)
|
|
|
|
;; Empty script — skip
|
|
(or (nil? text) (empty? (trim text)))
|
|
nil
|
|
|
|
;; Mount directive
|
|
(dom-has-attr? s "data-mount")
|
|
(let ((mount-sel (dom-get-attr s "data-mount"))
|
|
(target (dom-query mount-sel)))
|
|
(when target
|
|
(sx-mount target text nil)))
|
|
|
|
;; Default: load as components
|
|
:else
|
|
(sx-load-components text)))))
|
|
scripts))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Component script with caching
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define process-component-script
|
|
(fn (script text)
|
|
;; Handle <script type="text/sx" data-components data-hash="...">
|
|
(let ((hash (dom-get-attr script "data-hash")))
|
|
(if (nil? hash)
|
|
;; Legacy: no hash — just load inline
|
|
(when (and text (not (empty? (trim text))))
|
|
(sx-load-components text))
|
|
;; Hash-based caching
|
|
(let ((has-inline (and text (not (empty? (trim text))))))
|
|
(let ((cached-hash (local-storage-get "sx-components-hash")))
|
|
(if (= cached-hash hash)
|
|
;; Cache hit
|
|
(if has-inline
|
|
;; Server sent full source (cookie stale) — update cache
|
|
(do
|
|
(local-storage-set "sx-components-hash" hash)
|
|
(local-storage-set "sx-components-src" text)
|
|
(sx-load-components text)
|
|
(log-info "components: downloaded (cookie stale)"))
|
|
;; Server omitted source — load from cache
|
|
(let ((cached (local-storage-get "sx-components-src")))
|
|
(if cached
|
|
(do
|
|
(sx-load-components cached)
|
|
(log-info (str "components: cached (" hash ")")))
|
|
;; Cache entry missing — clear cookie and reload
|
|
(do
|
|
(clear-sx-comp-cookie)
|
|
(browser-reload)))))
|
|
;; Cache miss — hash mismatch
|
|
(if has-inline
|
|
;; Server sent full source — cache it
|
|
(do
|
|
(local-storage-set "sx-components-hash" hash)
|
|
(local-storage-set "sx-components-src" text)
|
|
(sx-load-components text)
|
|
(log-info (str "components: downloaded (" hash ")")))
|
|
;; Server omitted but cache stale — clear and reload
|
|
(do
|
|
(local-storage-remove "sx-components-hash")
|
|
(local-storage-remove "sx-components-src")
|
|
(clear-sx-comp-cookie)
|
|
(browser-reload)))))
|
|
(set-sx-comp-cookie hash))))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Style dictionary initialization
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define init-style-dict
|
|
(fn ()
|
|
;; Process <script type="text/sx-styles"> tags with caching.
|
|
(let ((scripts (query-style-scripts)))
|
|
(for-each
|
|
(fn (s)
|
|
(when (not (is-processed? s "styles"))
|
|
(mark-processed! s "styles")
|
|
(let ((text (dom-text-content s))
|
|
(hash (dom-get-attr s "data-hash")))
|
|
(if (nil? hash)
|
|
;; No hash — just parse inline
|
|
(when (and text (not (empty? (trim text))))
|
|
(parse-and-load-style-dict text))
|
|
;; Hash-based caching
|
|
(let ((has-inline (and text (not (empty? (trim text))))))
|
|
(let ((cached-hash (local-storage-get "sx-styles-hash")))
|
|
(if (= cached-hash hash)
|
|
;; Cache hit
|
|
(if has-inline
|
|
(do
|
|
(local-storage-set "sx-styles-src" text)
|
|
(parse-and-load-style-dict text)
|
|
(log-info "styles: downloaded (cookie stale)"))
|
|
(let ((cached (local-storage-get "sx-styles-src")))
|
|
(if cached
|
|
(do
|
|
(parse-and-load-style-dict cached)
|
|
(log-info (str "styles: cached (" hash ")")))
|
|
(do
|
|
(clear-sx-styles-cookie)
|
|
(browser-reload)))))
|
|
;; Cache miss
|
|
(if has-inline
|
|
(do
|
|
(local-storage-set "sx-styles-hash" hash)
|
|
(local-storage-set "sx-styles-src" text)
|
|
(parse-and-load-style-dict text)
|
|
(log-info (str "styles: downloaded (" hash ")")))
|
|
(do
|
|
(local-storage-remove "sx-styles-hash")
|
|
(local-storage-remove "sx-styles-src")
|
|
(clear-sx-styles-cookie)
|
|
(browser-reload)))))
|
|
(set-sx-styles-cookie hash))))))
|
|
scripts))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Page registry for client-side routing
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define _page-routes (list))
|
|
|
|
(define process-page-scripts
|
|
(fn ()
|
|
;; Process <script type="text/sx-pages"> tags.
|
|
;; Parses SX page registry and builds route entries with parsed patterns.
|
|
(let ((scripts (query-page-scripts)))
|
|
(for-each
|
|
(fn (s)
|
|
(when (not (is-processed? s "pages"))
|
|
(mark-processed! s "pages")
|
|
(let ((text (dom-text-content s)))
|
|
(when (and text (not (empty? (trim text))))
|
|
(let ((pages (parse text)))
|
|
(for-each
|
|
(fn (page)
|
|
(append! _page-routes
|
|
(merge page
|
|
{"parsed" (parse-route-pattern (get page "path"))})))
|
|
pages))))))
|
|
scripts))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Full boot sequence
|
|
;; --------------------------------------------------------------------------
|
|
|
|
(define boot-init
|
|
(fn ()
|
|
;; Full browser initialization:
|
|
;; 1. CSS tracking
|
|
;; 2. Style dictionary
|
|
;; 3. Process scripts (components + mounts)
|
|
;; 4. Process page registry (client-side routing)
|
|
;; 5. Hydrate [data-sx] elements
|
|
;; 6. Process engine elements
|
|
(do
|
|
(log-info (str "sx-browser " SX_VERSION))
|
|
(init-css-tracking)
|
|
(init-style-dict)
|
|
(process-sx-scripts nil)
|
|
(process-page-scripts)
|
|
(sx-hydrate-elements nil)
|
|
(process-elements nil))))
|
|
|
|
|
|
;; --------------------------------------------------------------------------
|
|
;; Platform interface — Boot
|
|
;; --------------------------------------------------------------------------
|
|
;;
|
|
;; From orchestration.sx:
|
|
;; process-elements, init-css-tracking
|
|
;;
|
|
;; From cssx.sx:
|
|
;; load-style-dict
|
|
;;
|
|
;; === DOM / Render ===
|
|
;; (resolve-mount-target target) → Element (string → querySelector, else identity)
|
|
;; (sx-render-with-env source extra-env) → DOM node (parse + render with componentEnv + extra)
|
|
;; (get-render-env extra-env) → merged component env + extra
|
|
;; (merge-envs base new) → merged env dict
|
|
;; (render-to-dom expr env ns) → DOM node
|
|
;; (sx-load-components text) → void (parse + eval into componentEnv)
|
|
;;
|
|
;; === DOM queries ===
|
|
;; (dom-query sel) → Element or nil
|
|
;; (dom-query-all root sel) → list of Elements
|
|
;; (dom-body) → document.body
|
|
;; (dom-get-attr el name) → string or nil
|
|
;; (dom-has-attr? el name) → boolean
|
|
;; (dom-text-content el) → string
|
|
;; (dom-set-text-content el s) → void
|
|
;; (dom-append el child) → void
|
|
;; (dom-remove-child parent el) → void
|
|
;; (dom-parent el) → Element
|
|
;; (dom-append-to-head el) → void
|
|
;; (dom-tag-name el) → string
|
|
;;
|
|
;; === Head hoisting ===
|
|
;; (set-document-title s) → void (document.title = s)
|
|
;; (remove-head-element sel) → void (remove matching element from <head>)
|
|
;;
|
|
;; === Script queries ===
|
|
;; (query-sx-scripts root) → list of <script type="text/sx"> elements
|
|
;; (query-style-scripts) → list of <script type="text/sx-styles"> elements
|
|
;; (query-page-scripts) → list of <script type="text/sx-pages"> elements
|
|
;;
|
|
;; === localStorage ===
|
|
;; (local-storage-get key) → string or nil
|
|
;; (local-storage-set key val) → void
|
|
;; (local-storage-remove key) → void
|
|
;;
|
|
;; === Cookies ===
|
|
;; (set-sx-comp-cookie hash) → void
|
|
;; (clear-sx-comp-cookie) → void
|
|
;; (set-sx-styles-cookie hash) → void
|
|
;; (clear-sx-styles-cookie) → void
|
|
;;
|
|
;; === Env ===
|
|
;; (parse-env-attr el) → dict (parse data-sx-env JSON attr)
|
|
;; (store-env-attr el base new) → void (merge and store back as JSON)
|
|
;; (to-kebab s) → string (underscore → kebab-case)
|
|
;;
|
|
;; === Logging ===
|
|
;; (log-info msg) → void (console.log with prefix)
|
|
;; (log-parse-error label text err) → void (diagnostic parse error)
|
|
;;
|
|
;; === JSON parsing ===
|
|
;; (parse-and-load-style-dict text) → void (JSON.parse + load-style-dict)
|
|
;;
|
|
;; === Processing markers ===
|
|
;; (mark-processed! el key) → void
|
|
;; (is-processed? el key) → boolean
|
|
;; --------------------------------------------------------------------------
|