Fix duplicate sx-cssx-live style tags
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled

Cache the style element reference in _cssx-style-el so flush-cssx-to-dom
never creates more than one. Previous code called dom-query on every
flush, which could miss the element during rapid successive calls,
creating duplicates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 17:16:13 +00:00
parent 41f4772ba7
commit 8b24b6b585
7 changed files with 515 additions and 100 deletions

View File

@@ -952,7 +952,7 @@
(dom-set-attr el k (str (dict-get attrs k))))
extra-keys)
;; Flush any newly collected CSS rules to live stylesheet
(flush-cssx-to-dom))
(run-post-render-hooks))
;; No longer a spread — clear tracked state
(do
(set! prev-classes (list))

View File

@@ -88,7 +88,7 @@
(process-elements el)
(sx-hydrate-elements el)
(sx-hydrate-islands el)
(flush-cssx-to-dom))))))
(run-post-render-hooks))))))
;; --------------------------------------------------------------------------
@@ -120,7 +120,7 @@
(process-elements el)
(sx-hydrate-elements el)
(sx-hydrate-islands el)
(flush-cssx-to-dom)
(run-post-render-hooks)
(dom-dispatch el "sx:resolved" {:id id})))
(log-warn (str "resolveSuspense: no element for id=" id))))))
@@ -418,29 +418,30 @@
;; --------------------------------------------------------------------------
;; CSSX live flush — inject collected CSS rules into the DOM
;; Render hooks — generic pre/post callbacks for hydration, swap, mount.
;; The spec calls these at render boundaries; the app decides what to do.
;; Pre-render: setup before DOM changes (e.g. prepare state).
;; Post-render: cleanup after DOM changes (e.g. flush collected CSS).
;; --------------------------------------------------------------------------
;;
;; ~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]
(define *pre-render-hooks* (list))
(define *post-render-hooks* (list))
(define register-pre-render-hook :effects [mutation]
(fn ((hook-fn :as lambda))
(append! *pre-render-hooks* hook-fn)))
(define register-post-render-hook :effects [mutation]
(fn ((hook-fn :as lambda))
(append! *post-render-hooks* hook-fn)))
(define run-pre-render-hooks :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")))))
(for-each (fn (hook) (hook)) *pre-render-hooks*)))
(define run-post-render-hooks :effects [mutation io]
(fn ()
(for-each (fn (hook) (hook)) *post-render-hooks*)))
;; --------------------------------------------------------------------------
@@ -464,7 +465,7 @@
(process-sx-scripts nil)
(sx-hydrate-elements nil)
(sx-hydrate-islands nil)
(flush-cssx-to-dom)
(run-post-render-hooks)
(process-elements nil))))

View File

@@ -460,7 +460,7 @@
(sx-process-scripts root)
(sx-hydrate root)
(sx-hydrate-islands root)
(flush-cssx-to-dom)
(run-post-render-hooks)
(process-elements root)))
@@ -871,7 +871,7 @@
(hoist-head-elements-full target)
(process-elements target)
(sx-hydrate-elements target)
(flush-cssx-to-dom)
(run-post-render-hooks)
(dom-dispatch target "sx:clientRoute"
(dict "pathname" pathname))
(log-info (str "sx:route client " pathname)))))

View File

@@ -1509,33 +1509,9 @@ CEK_FIXUPS_JS = '''
return cekValue(state);
};
// Expose spec functions so evaluated SX code can use them.
// Type inspection (platform interface from boundary.sx)
PRIMITIVES["type-of"] = typeOf;
PRIMITIVES["symbol-name"] = symbolName;
PRIMITIVES["keyword-name"] = keywordName;
PRIMITIVES["callable?"] = isCallable;
PRIMITIVES["lambda?"] = isLambda;
PRIMITIVES["lambda-name"] = lambdaName;
PRIMITIVES["component?"] = isComponent;
PRIMITIVES["island?"] = isIsland;
PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); };
// Parser (from parser.sx)
PRIMITIVES["sx-serialize"] = sxSerialize;
// CEK machine (from cek.sx/frames.sx)
PRIMITIVES["make-cek-state"] = makeCekState;
PRIMITIVES["cek-step"] = cekStep;
PRIMITIVES["cek-run"] = cekRun;
PRIMITIVES["cek-terminal?"] = cekTerminal_p;
PRIMITIVES["cek-value"] = cekValue;
PRIMITIVES["eval-expr-cek"] = evalExprCek;
// Render (from adapter-html.sx / render.sx)
// Synthetic primitives — not direct spec defines but needed by evaluated code
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
// Environment (for creating eval contexts)
PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); };
PRIMITIVES["make-env"] = function() { return merge(componentEnv, PRIMITIVES); };
'''

View File

@@ -20,8 +20,21 @@ _PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
if _PROJECT not in sys.path:
sys.path.insert(0, _PROJECT)
import re
from shared.sx.parser import parse_all
from shared.sx.types import Symbol
def _js_mangle(name: str) -> str:
"""Convert SX name to JS identifier (mirrors js-mangle in js.sx)."""
if name.endswith("?"):
name = name[:-1] + "_p"
elif name.endswith("!"):
name = name[:-1] + "_b"
parts = name.split("-")
result = parts[0] + "".join(p.capitalize() for p in parts[1:])
result = result.replace("*", "_")
return result
from shared.sx.ref.platform_js import (
extract_defines,
ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, SPEC_MODULE_ORDER, EXTENSION_NAMES,
@@ -195,6 +208,7 @@ def compile_ref_to_js(
parts.append(PLATFORM_CEK_JS)
# Translate each spec file using js.sx
all_spec_defines: list[str] = [] # collect all defined names for auto-registration
for filename, label in sx_files:
filepath = os.path.join(ref_dir, filename)
if not os.path.exists(filepath):
@@ -204,6 +218,7 @@ def compile_ref_to_js(
defines = extract_defines(src)
sx_defines = [[name, expr] for name, expr in defines]
all_spec_defines.extend(name for name, _ in defines)
parts.append(f"\n // === Transpiled from {label} ===\n")
env["_defines"] = sx_defines
@@ -222,6 +237,15 @@ def compile_ref_to_js(
if has_cek:
parts.append(CEK_FIXUPS_JS)
# Auto-register all spec defines as PRIMITIVES so evaluated SX code
# (islands, data-init scripts, runtime eval) can call any spec function.
reg_lines = ["\n // === Auto-registered spec defines ==="]
for sx_name in all_spec_defines:
js_name = _js_mangle(sx_name)
reg_lines.append(f' if (typeof {js_name} !== "undefined") PRIMITIVES["{sx_name}"] = {js_name};')
reg_lines.append("")
parts.append("\n".join(reg_lines))
for name in ("dom", "engine", "orchestration", "boot"):
if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name])