Fix sx-browser.js navigation bugs: CSS tracking meta tag and stale verb info
Two fixes for sx-browser.js (spec-compiled) vs sx.js (hand-written): 1. CSS meta tag mismatch: initCssTracking read meta[name="sx-css-hash"] but the page template uses meta[name="sx-css-classes"]. This left _cssHash empty, causing the server to send ALL CSS as "new" on every navigation, appending duplicate rules that broke Tailwind responsive ordering (e.g. menu bar layout). 2. Stale verb info after morph: execute-request used captured verbInfo from bind time. After morph updated element attributes (e.g. during OOB nav swap), click handlers still fired with old URLs. Now re-reads verb info from the element first, matching sx.js behavior. Also includes: render-expression dispatch in eval.sx, NIL guard for preload cache in bootstrap_js.py, and helpers.py switched to sx-browser.js. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,8 @@ class JSEmitter:
|
||||
"eval-expr": "evalExpr",
|
||||
"eval-list": "evalList",
|
||||
"eval-call": "evalCall",
|
||||
"is-render-expr?": "isRenderExpr",
|
||||
"render-expr": "renderExpr",
|
||||
"call-lambda": "callLambda",
|
||||
"call-component": "callComponent",
|
||||
"parse-keyword-args": "parseKeywordArgs",
|
||||
@@ -559,7 +561,7 @@ class JSEmitter:
|
||||
|
||||
def _emit_fn(self, expr) -> str:
|
||||
params = expr[1]
|
||||
body = expr[2]
|
||||
body = expr[2:]
|
||||
param_names = []
|
||||
for p in params:
|
||||
if isinstance(p, Symbol):
|
||||
@@ -567,8 +569,16 @@ class JSEmitter:
|
||||
else:
|
||||
param_names.append(str(p))
|
||||
params_str = ", ".join(param_names)
|
||||
body_js = self.emit(body)
|
||||
return f"function({params_str}) {{ return {body_js}; }}"
|
||||
if len(body) == 1:
|
||||
body_js = self.emit(body[0])
|
||||
return f"function({params_str}) {{ return {body_js}; }}"
|
||||
# Multi-expression body: statements then return last
|
||||
parts = []
|
||||
for b in body[:-1]:
|
||||
parts.append(self.emit_statement(b))
|
||||
parts.append(f"return {self.emit(body[-1])};")
|
||||
inner = "\n".join(parts)
|
||||
return f"function({params_str}) {{ {inner} }}"
|
||||
|
||||
def _emit_let(self, expr) -> str:
|
||||
bindings = expr[1]
|
||||
@@ -717,10 +727,10 @@ class JSEmitter:
|
||||
# If fn is an inline lambda, emit a for loop
|
||||
if isinstance(fn_expr, list) and fn_expr[0] == Symbol("fn"):
|
||||
params = fn_expr[1]
|
||||
body = fn_expr[2]
|
||||
body = fn_expr[2:]
|
||||
p = params[0].name if isinstance(params[0], Symbol) else str(params[0])
|
||||
p_js = self._mangle(p)
|
||||
body_js = self.emit_statement(body)
|
||||
body_js = "\n".join(self.emit_statement(b) for b in body)
|
||||
return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ var {p_js} = _c[_i]; {body_js} }} }}"
|
||||
fn = self.emit(fn_expr)
|
||||
return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ {fn}(_c[_i]); }} }}"
|
||||
@@ -978,6 +988,7 @@ PLATFORM_JS = '''
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
if (x._styleValue) return "style-value";
|
||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||
if (Array.isArray(x)) return "list";
|
||||
if (typeof x === "object") return "dict";
|
||||
return "unknown";
|
||||
@@ -1053,6 +1064,29 @@ PLATFORM_JS = '''
|
||||
function dictSet(d, k, v) { d[k] = v; }
|
||||
function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; }
|
||||
|
||||
// Render-expression detection — lets the evaluator delegate to the active adapter.
|
||||
// Matches HTML tags, SVG tags, <>, raw!, ~components, html: prefix, custom elements.
|
||||
function isRenderExpr(expr) {
|
||||
if (!Array.isArray(expr) || !expr.length) return false;
|
||||
var h = expr[0];
|
||||
if (!h || !h._sym) return false;
|
||||
var n = h.name;
|
||||
return !!(n === "<>" || n === "raw!" ||
|
||||
n.charAt(0) === "~" || n.indexOf("html:") === 0 ||
|
||||
(typeof HTML_TAGS !== "undefined" && HTML_TAGS.indexOf(n) >= 0) ||
|
||||
(typeof SVG_TAGS !== "undefined" && SVG_TAGS.indexOf(n) >= 0) ||
|
||||
(n.indexOf("-") > 0 && expr.length > 1 && expr[1] && expr[1]._kw));
|
||||
}
|
||||
|
||||
// Render dispatch — call the active adapter's render function.
|
||||
// Set by each adapter when loaded; defaults to identity (no rendering).
|
||||
var _renderExprFn = null;
|
||||
function renderExpr(expr, env) {
|
||||
if (_renderExprFn) return _renderExprFn(expr, env);
|
||||
// No adapter loaded — just return the expression as-is
|
||||
return expr;
|
||||
}
|
||||
|
||||
function stripPrefix(s, prefix) {
|
||||
return s.indexOf(prefix) === 0 ? s.slice(prefix.length) : s;
|
||||
}
|
||||
@@ -1366,6 +1400,9 @@ PLATFORM_DOM_JS = """
|
||||
|
||||
var _hasDom = typeof document !== "undefined";
|
||||
|
||||
// Register DOM adapter as the render dispatch target for the evaluator.
|
||||
_renderExprFn = function(expr, env) { return renderToDom(expr, env, null); };
|
||||
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var MATH_NS = "http://www.w3.org/1998/Math/MathML";
|
||||
|
||||
@@ -1649,7 +1686,7 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
if (config.body && config.method !== "GET") opts.body = config.body;
|
||||
if (config["cross-origin"]) opts.credentials = "include";
|
||||
|
||||
var p = config.preloaded
|
||||
var p = (config.preloaded && config.preloaded !== NIL)
|
||||
? Promise.resolve({
|
||||
ok: true, status: 200,
|
||||
headers: new Headers({ "Content-Type": config.preloaded["content-type"] || "" }),
|
||||
@@ -2045,23 +2082,25 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
// --- SX API references ---
|
||||
|
||||
function sxRender(source) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
||||
if (SxObj && SxObj.render) return SxObj.render(source);
|
||||
throw new Error("No SX renderer available");
|
||||
}
|
||||
|
||||
function sxProcessScripts(root) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (SxObj && SxObj.processScripts) SxObj.processScripts(root || undefined);
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
||||
var r = (root && root !== NIL) ? root : undefined;
|
||||
if (SxObj && SxObj.processScripts) SxObj.processScripts(r);
|
||||
}
|
||||
|
||||
function sxHydrate(root) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
if (SxObj && SxObj.hydrate) SxObj.hydrate(root || undefined);
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
||||
var r = (root && root !== NIL) ? root : undefined;
|
||||
if (SxObj && SxObj.hydrate) SxObj.hydrate(r);
|
||||
}
|
||||
|
||||
function loadedComponentNames() {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
||||
if (!SxObj) return [];
|
||||
var env = SxObj.componentEnv || (SxObj.getEnv ? SxObj.getEnv() : {});
|
||||
return Object.keys(env).filter(function(k) { return k.charAt(0) === "~"; });
|
||||
@@ -2070,7 +2109,7 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
// --- Response processing ---
|
||||
|
||||
function stripComponentScripts(text) {
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : (typeof SxRef !== "undefined" ? SxRef : null);
|
||||
var SxObj = typeof Sx !== "undefined" ? Sx : null;
|
||||
return text.replace(/<script[^>]*type="text\\/sx"[^>]*data-components[^>]*>([\\s\\S]*?)<\\/script>/gi,
|
||||
function(_, defs) { if (SxObj && SxObj.loadComponents) SxObj.loadComponents(defs); return ""; });
|
||||
}
|
||||
@@ -2269,8 +2308,9 @@ PLATFORM_BOOT_JS = """
|
||||
|
||||
function querySxScripts(root) {
|
||||
if (!_hasDom) return [];
|
||||
var r = (root && root !== NIL) ? root : document;
|
||||
return Array.prototype.slice.call(
|
||||
(root || document).querySelectorAll('script[type="text/sx"]'));
|
||||
r.querySelectorAll('script[type="text/sx"]'));
|
||||
}
|
||||
|
||||
function queryStyleScripts() {
|
||||
@@ -2556,11 +2596,13 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
|
||||
return parts.join("");
|
||||
}''')
|
||||
|
||||
# Build SxRef object
|
||||
# Build Sx object
|
||||
version = f"ref-2.0 ({adapter_label}, bootstrap-compiled)"
|
||||
api_lines.append(f'''
|
||||
var SxRef = {{
|
||||
var Sx = {{
|
||||
VERSION: "ref-2.0",
|
||||
parse: parse,
|
||||
parseAll: parse,
|
||||
eval: function(expr, env) {{ return trampoline(evalExpr(expr, env || merge(componentEnv))); }},
|
||||
loadComponents: loadComponents,
|
||||
render: render,{"" if has_html else ""}
|
||||
@@ -2569,6 +2611,8 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
|
||||
NIL: NIL,
|
||||
Symbol: Symbol,
|
||||
Keyword: Keyword,
|
||||
isTruthy: isSxTruthy,
|
||||
isNil: isNil,
|
||||
componentEnv: componentEnv,''')
|
||||
|
||||
if has_html:
|
||||
@@ -2612,27 +2656,27 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
|
||||
api_lines.append('''
|
||||
// --- Auto-init ---
|
||||
if (typeof document !== "undefined") {
|
||||
var _sxRefInit = function() { bootInit(); };
|
||||
var _sxInit = function() { bootInit(); };
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", _sxRefInit);
|
||||
document.addEventListener("DOMContentLoaded", _sxInit);
|
||||
} else {
|
||||
_sxRefInit();
|
||||
_sxInit();
|
||||
}
|
||||
}''')
|
||||
elif has_orch:
|
||||
api_lines.append('''
|
||||
// --- Auto-init ---
|
||||
if (typeof document !== "undefined") {
|
||||
var _sxRefInit = function() { engineInit(); };
|
||||
var _sxInit = function() { engineInit(); };
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", _sxRefInit);
|
||||
document.addEventListener("DOMContentLoaded", _sxInit);
|
||||
} else {
|
||||
_sxRefInit();
|
||||
_sxInit();
|
||||
}
|
||||
}''')
|
||||
|
||||
api_lines.append(' if (typeof module !== "undefined" && module.exports) module.exports = SxRef;')
|
||||
api_lines.append(' else global.SxRef = SxRef;')
|
||||
api_lines.append(' if (typeof module !== "undefined" && module.exports) module.exports = Sx;')
|
||||
api_lines.append(' else global.Sx = Sx;')
|
||||
|
||||
return "\n".join(api_lines)
|
||||
|
||||
|
||||
@@ -165,6 +165,10 @@
|
||||
(let ((mac (env-get env name)))
|
||||
(make-thunk (expand-macro mac args env) env))
|
||||
|
||||
;; Render expression — delegate to active adapter
|
||||
(is-render-expr? expr)
|
||||
(render-expr expr env)
|
||||
|
||||
;; Fall through to function call
|
||||
:else (eval-call head args env)))
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
(define init-css-tracking
|
||||
(fn ()
|
||||
;; Read initial CSS hash from meta tag
|
||||
(let ((meta (dom-query "meta[name=\"sx-css-hash\"]")))
|
||||
(let ((meta (dom-query "meta[name=\"sx-css-classes\"]")))
|
||||
(when meta
|
||||
(let ((content (dom-get-attr meta "content")))
|
||||
(when content
|
||||
@@ -76,8 +76,9 @@
|
||||
(fn (el verbInfo extraParams)
|
||||
;; Gate checks then delegate to do-fetch.
|
||||
;; verbInfo: dict with "method" and "url" (or nil to read from element).
|
||||
;; Re-read from element in case attributes were morphed since binding.
|
||||
;; Returns a promise.
|
||||
(let ((info (or verbInfo (get-verb-info el))))
|
||||
(let ((info (or (get-verb-info el) verbInfo)))
|
||||
(if (nil? info)
|
||||
(promise-resolve nil)
|
||||
(let ((verb (get info "method"))
|
||||
|
||||
Reference in New Issue
Block a user