Add flush-cssx-to-dom: client-side CSSX rule injection
Islands render independently on the client, so ~cssx/tw calls
collect!("cssx", rule) but no ~cssx/flush runs. Add flush-cssx-to-dom
in boot.sx that injects collected rules into a persistent <style>
element in <head>.
Called at all lifecycle points: boot-init, sx-mount, resolve-suspense,
post-swap (navigation morph), and swap-rendered-content (client routes).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-13T03:40:30Z";
|
var SX_VERSION = "2026-03-13T04:08:59Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -2947,6 +2947,7 @@ return postSwap(target); });
|
|||||||
sxProcessScripts(root);
|
sxProcessScripts(root);
|
||||||
sxHydrate(root);
|
sxHydrate(root);
|
||||||
sxHydrateIslands(root);
|
sxHydrateIslands(root);
|
||||||
|
flushCssxToDom();
|
||||||
return processElements(root); };
|
return processElements(root); };
|
||||||
|
|
||||||
// process-settle-hooks
|
// process-settle-hooks
|
||||||
@@ -3181,7 +3182,7 @@ return logWarn((String("sx:offline sync failed ") + String(get(entry, "action"))
|
|||||||
})(); };
|
})(); };
|
||||||
|
|
||||||
// swap-rendered-content
|
// swap-rendered-content
|
||||||
var swapRenderedContent = function(target, rendered, pathname) { return (disposeIslandsIn(target), domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname)))); };
|
var swapRenderedContent = function(target, rendered, pathname) { return (disposeIslandsIn(target), domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), flushCssxToDom(), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname)))); };
|
||||||
|
|
||||||
// resolve-route-target
|
// resolve-route-target
|
||||||
var resolveRouteTarget = function(targetSel) { return (isSxTruthy((isSxTruthy(targetSel) && !isSxTruthy((targetSel == "true")))) ? domQuery(targetSel) : NIL); };
|
var resolveRouteTarget = function(targetSel) { return (isSxTruthy((isSxTruthy(targetSel) && !isSxTruthy((targetSel == "true")))) ? domQuery(targetSel) : NIL); };
|
||||||
@@ -3400,7 +3401,8 @@ return processEmitElements(root); };
|
|||||||
hoistHeadElementsFull(el);
|
hoistHeadElementsFull(el);
|
||||||
processElements(el);
|
processElements(el);
|
||||||
sxHydrateElements(el);
|
sxHydrateElements(el);
|
||||||
return sxHydrateIslands(el);
|
sxHydrateIslands(el);
|
||||||
|
return flushCssxToDom();
|
||||||
})() : NIL);
|
})() : NIL);
|
||||||
})(); };
|
})(); };
|
||||||
|
|
||||||
@@ -3416,6 +3418,7 @@ return (function() {
|
|||||||
processElements(el);
|
processElements(el);
|
||||||
sxHydrateElements(el);
|
sxHydrateElements(el);
|
||||||
sxHydrateIslands(el);
|
sxHydrateIslands(el);
|
||||||
|
flushCssxToDom();
|
||||||
return domDispatch(el, "sx:resolved", {"id": id});
|
return domDispatch(el, "sx:resolved", {"id": id});
|
||||||
})() : logWarn((String("resolveSuspense: no element for id=") + String(id))));
|
})() : logWarn((String("resolveSuspense: no element for id=") + String(id))));
|
||||||
})(); };
|
})(); };
|
||||||
@@ -3561,8 +3564,23 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
})() : NIL);
|
})() : NIL);
|
||||||
})() : NIL); };
|
})() : NIL); };
|
||||||
|
|
||||||
|
// flush-cssx-to-dom
|
||||||
|
var flushCssxToDom = function() { return (function() {
|
||||||
|
var rules = sxCollected("cssx");
|
||||||
|
return (isSxTruthy(!isSxTruthy(isEmpty(rules))) ? ((function() {
|
||||||
|
var style = sxOr(domQuery("#sx-cssx-live"), (function() {
|
||||||
|
var s = domCreateElement("style", NIL);
|
||||||
|
domSetAttr(s, "id", "sx-cssx-live");
|
||||||
|
domSetAttr(s, "data-cssx", "");
|
||||||
|
domAppendToHead(s);
|
||||||
|
return s;
|
||||||
|
})());
|
||||||
|
return domSetProp(style, "textContent", (String(sxOr(domGetProp(style, "textContent"), "")) + String(join("", rules))));
|
||||||
|
})(), sxClearCollected("cssx")) : NIL);
|
||||||
|
})(); };
|
||||||
|
|
||||||
// boot-init
|
// boot-init
|
||||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), sxHydrateIslands(NIL), processElements(NIL)); };
|
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), sxHydrateIslands(NIL), flushCssxToDom(), processElements(NIL)); };
|
||||||
|
|
||||||
|
|
||||||
// === Transpiled from deps (component dependency analysis) ===
|
// === Transpiled from deps (component dependency analysis) ===
|
||||||
|
|||||||
@@ -87,7 +87,8 @@
|
|||||||
;; Process sx- attributes, hydrate data-sx and islands
|
;; Process sx- attributes, hydrate data-sx and islands
|
||||||
(process-elements el)
|
(process-elements el)
|
||||||
(sx-hydrate-elements el)
|
(sx-hydrate-elements el)
|
||||||
(sx-hydrate-islands el))))))
|
(sx-hydrate-islands el)
|
||||||
|
(flush-cssx-to-dom))))))
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
(process-elements el)
|
(process-elements el)
|
||||||
(sx-hydrate-elements el)
|
(sx-hydrate-elements el)
|
||||||
(sx-hydrate-islands el)
|
(sx-hydrate-islands el)
|
||||||
|
(flush-cssx-to-dom)
|
||||||
(dom-dispatch el "sx:resolved" {:id id})))
|
(dom-dispatch el "sx:resolved" {:id id})))
|
||||||
(log-warn (str "resolveSuspense: no element for id=" id))))))
|
(log-warn (str "resolveSuspense: no element for id=" id))))))
|
||||||
|
|
||||||
@@ -415,6 +417,32 @@
|
|||||||
(for-each dispose-island to-dispose))))))))
|
(for-each dispose-island to-dispose))))))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; CSSX live flush — inject collected CSS rules into the DOM
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;;
|
||||||
|
;; ~cssx/tw collects CSS rules via collect!("cssx" ...) during rendering.
|
||||||
|
;; On the server, ~cssx/flush emits a batch <style> tag. On the client,
|
||||||
|
;; islands render independently and no batch flush runs. This function
|
||||||
|
;; injects any unflushed rules into a persistent <style> element in <head>.
|
||||||
|
;; Called after hydration (boot + post-swap) to cover all render paths.
|
||||||
|
|
||||||
|
(define flush-cssx-to-dom :effects [mutation io]
|
||||||
|
(fn ()
|
||||||
|
(let ((rules (collected "cssx")))
|
||||||
|
(when (not (empty? rules))
|
||||||
|
(let ((style (or (dom-query "#sx-cssx-live")
|
||||||
|
(let ((s (dom-create-element "style" nil)))
|
||||||
|
(dom-set-attr s "id" "sx-cssx-live")
|
||||||
|
(dom-set-attr s "data-cssx" "")
|
||||||
|
(dom-append-to-head s)
|
||||||
|
s))))
|
||||||
|
(dom-set-prop style "textContent"
|
||||||
|
(str (or (dom-get-prop style "textContent") "")
|
||||||
|
(join "" rules))))
|
||||||
|
(clear-collected! "cssx")))))
|
||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; Full boot sequence
|
;; Full boot sequence
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
@@ -436,6 +464,7 @@
|
|||||||
(process-sx-scripts nil)
|
(process-sx-scripts nil)
|
||||||
(sx-hydrate-elements nil)
|
(sx-hydrate-elements nil)
|
||||||
(sx-hydrate-islands nil)
|
(sx-hydrate-islands nil)
|
||||||
|
(flush-cssx-to-dom)
|
||||||
(process-elements nil))))
|
(process-elements nil))))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -460,6 +460,7 @@
|
|||||||
(sx-process-scripts root)
|
(sx-process-scripts root)
|
||||||
(sx-hydrate root)
|
(sx-hydrate root)
|
||||||
(sx-hydrate-islands root)
|
(sx-hydrate-islands root)
|
||||||
|
(flush-cssx-to-dom)
|
||||||
(process-elements root)))
|
(process-elements root)))
|
||||||
|
|
||||||
|
|
||||||
@@ -870,6 +871,7 @@
|
|||||||
(hoist-head-elements-full target)
|
(hoist-head-elements-full target)
|
||||||
(process-elements target)
|
(process-elements target)
|
||||||
(sx-hydrate-elements target)
|
(sx-hydrate-elements target)
|
||||||
|
(flush-cssx-to-dom)
|
||||||
(dom-dispatch target "sx:clientRoute"
|
(dom-dispatch target "sx:clientRoute"
|
||||||
(dict "pathname" pathname))
|
(dict "pathname" pathname))
|
||||||
(log-info (str "sx:route client " pathname)))))
|
(log-info (str "sx:route client " pathname)))))
|
||||||
|
|||||||
Reference in New Issue
Block a user