Merge branch 'worktree-iso-phase-4' into macros
# Conflicts: # shared/static/scripts/sx-browser.js
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-07T17:45:49Z";
|
var SX_VERSION = "2026-03-07T17:50:50Z";
|
||||||
|
|
||||||
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); }
|
||||||
@@ -565,6 +565,92 @@
|
|||||||
return makeThunk(componentBody(comp), local);
|
return makeThunk(componentBody(comp), local);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Platform: deps module — component dependency analysis
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function componentDeps(c) {
|
||||||
|
return c.deps ? c.deps.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentSetDeps(c, deps) {
|
||||||
|
c.deps = deps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentCssClasses(c) {
|
||||||
|
return c.cssClasses ? c.cssClasses.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function envComponents(env) {
|
||||||
|
var names = [];
|
||||||
|
for (var k in env) {
|
||||||
|
var v = env[k];
|
||||||
|
if (v && (v._component || v._macro)) names.push(k);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
function regexFindAll(pattern, source) {
|
||||||
|
var re = new RegExp(pattern, "g");
|
||||||
|
var results = [];
|
||||||
|
var m;
|
||||||
|
while ((m = re.exec(source)) !== null) {
|
||||||
|
if (m[1] !== undefined) results.push(m[1]);
|
||||||
|
else results.push(m[0]);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanCssClasses(source) {
|
||||||
|
var classes = {};
|
||||||
|
var result = [];
|
||||||
|
var m;
|
||||||
|
var re1 = /:class\s+"([^"]*)"/g;
|
||||||
|
while ((m = re1.exec(source)) !== null) {
|
||||||
|
var parts = m[1].split(/\s+/);
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
if (parts[i] && !classes[parts[i]]) {
|
||||||
|
classes[parts[i]] = true;
|
||||||
|
result.push(parts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var re2 = /:class\s+\(str\s+((?:"[^"]*"\s*)+)\)/g;
|
||||||
|
while ((m = re2.exec(source)) !== null) {
|
||||||
|
var re3 = /"([^"]*)"/g;
|
||||||
|
var m2;
|
||||||
|
while ((m2 = re3.exec(m[1])) !== null) {
|
||||||
|
var parts2 = m2[1].split(/\s+/);
|
||||||
|
for (var j = 0; j < parts2.length; j++) {
|
||||||
|
if (parts2[j] && !classes[parts2[j]]) {
|
||||||
|
classes[parts2[j]] = true;
|
||||||
|
result.push(parts2[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var re4 = /;;\s*@css\s+(.+)/g;
|
||||||
|
while ((m = re4.exec(source)) !== null) {
|
||||||
|
var parts3 = m[1].split(/\s+/);
|
||||||
|
for (var k = 0; k < parts3.length; k++) {
|
||||||
|
if (parts3[k] && !classes[parts3[k]]) {
|
||||||
|
classes[parts3[k]] = true;
|
||||||
|
result.push(parts3[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentIoRefs(c) {
|
||||||
|
return c.ioRefs ? c.ioRefs.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentSetIoRefs(c, refs) {
|
||||||
|
c.ioRefs = refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Platform interface — Parser
|
// Platform interface — Parser
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -2007,13 +2093,6 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
|
|||||||
// page-data-cache-set
|
// page-data-cache-set
|
||||||
var pageDataCacheSet = function(cacheKey, data) { return dictSet(_pageDataCache, cacheKey, {"data": data, "ts": nowMs()}); };
|
var pageDataCacheSet = function(cacheKey, data) { return dictSet(_pageDataCache, cacheKey, {"data": data, "ts": nowMs()}); };
|
||||||
|
|
||||||
// current-page-layout
|
|
||||||
var currentPageLayout = function() { return (function() {
|
|
||||||
var pathname = urlPathname(browserLocationHref());
|
|
||||||
var match = findMatchingRoute(pathname, _pageRoutes);
|
|
||||||
return (isSxTruthy(isNil(match)) ? "" : sxOr(get(match, "layout"), ""));
|
|
||||||
})(); };
|
|
||||||
|
|
||||||
// swap-rendered-content
|
// swap-rendered-content
|
||||||
var swapRenderedContent = function(target, rendered, pathname) { return (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 (domSetTextContent(target, ""), domAppend(target, rendered), hoistHeadElementsFull(target), processElements(target), sxHydrateElements(target), domDispatch(target, "sx:clientRoute", {["pathname"]: pathname}), logInfo((String("sx:route client ") + String(pathname)))); };
|
||||||
|
|
||||||
@@ -2031,9 +2110,6 @@ return domAppendToHead(link); }, domQueryAll(container, "link[rel=\"stylesheet\"
|
|||||||
var tryClientRoute = function(pathname, targetSel) { return (function() {
|
var tryClientRoute = function(pathname, targetSel) { return (function() {
|
||||||
var match = findMatchingRoute(pathname, _pageRoutes);
|
var match = findMatchingRoute(pathname, _pageRoutes);
|
||||||
return (isSxTruthy(isNil(match)) ? (logInfo((String("sx:route no match (") + String(len(_pageRoutes)) + String(" routes) ") + String(pathname))), false) : (function() {
|
return (isSxTruthy(isNil(match)) ? (logInfo((String("sx:route no match (") + String(len(_pageRoutes)) + String(" routes) ") + String(pathname))), false) : (function() {
|
||||||
var targetLayout = sxOr(get(match, "layout"), "");
|
|
||||||
var curLayout = currentPageLayout();
|
|
||||||
return (isSxTruthy(!isSxTruthy((targetLayout == curLayout))) ? (logInfo((String("sx:route server (layout: ") + String(curLayout) + String(" -> ") + String(targetLayout) + String(") ") + String(pathname))), false) : (function() {
|
|
||||||
var contentSrc = get(match, "content");
|
var contentSrc = get(match, "content");
|
||||||
var closure = sxOr(get(match, "closure"), {});
|
var closure = sxOr(get(match, "closure"), {});
|
||||||
var params = get(match, "params");
|
var params = get(match, "params");
|
||||||
@@ -2071,7 +2147,6 @@ return (function() {
|
|||||||
})()));
|
})()));
|
||||||
})());
|
})());
|
||||||
})());
|
})());
|
||||||
})());
|
|
||||||
})(); };
|
})(); };
|
||||||
|
|
||||||
// bind-client-route-link
|
// bind-client-route-link
|
||||||
@@ -2509,6 +2584,119 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), initStyleDict(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), initStyleDict(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), processElements(NIL)); };
|
||||||
|
|
||||||
|
|
||||||
|
// === Transpiled from deps (component dependency analysis) ===
|
||||||
|
|
||||||
|
// scan-refs
|
||||||
|
var scanRefs = function(node) { return (function() {
|
||||||
|
var refs = [];
|
||||||
|
scanRefsWalk(node, refs);
|
||||||
|
return refs;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// scan-refs-walk
|
||||||
|
var scanRefsWalk = function(node, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
|
||||||
|
var name = symbolName(node);
|
||||||
|
return (isSxTruthy(startsWith(name, "~")) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL);
|
||||||
|
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanRefsWalk(item, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanRefsWalk(dictGet(node, key), refs); }, keys(node)) : NIL))); };
|
||||||
|
|
||||||
|
// transitive-deps-walk
|
||||||
|
var transitiveDepsWalk = function(n, seen, env) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() {
|
||||||
|
var val = envGet(env, n);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(componentBody(val))) : (isSxTruthy((typeOf(val) == "macro")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(macroBody(val))) : NIL));
|
||||||
|
})()) : NIL); };
|
||||||
|
|
||||||
|
// transitive-deps
|
||||||
|
var transitiveDeps = function(name, env) { return (function() {
|
||||||
|
var seen = [];
|
||||||
|
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||||
|
transitiveDepsWalk(key, seen, env);
|
||||||
|
return filter(function(x) { return !isSxTruthy((x == key)); }, seen);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// compute-all-deps
|
||||||
|
var computeAllDeps = function(env) { return forEach(function(name) { return (function() {
|
||||||
|
var val = envGet(env, name);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? componentSetDeps(val, transitiveDeps(name, env)) : NIL);
|
||||||
|
})(); }, envComponents(env)); };
|
||||||
|
|
||||||
|
// scan-components-from-source
|
||||||
|
var scanComponentsFromSource = function(source) { return (function() {
|
||||||
|
var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source);
|
||||||
|
return map(function(m) { return (String("~") + String(m)); }, matches);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// components-needed
|
||||||
|
var componentsNeeded = function(pageSource, env) { return (function() {
|
||||||
|
var direct = scanComponentsFromSource(pageSource);
|
||||||
|
var allNeeded = [];
|
||||||
|
{ var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(allNeeded, name)))) {
|
||||||
|
allNeeded.push(name);
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
var val = envGet(env, name);
|
||||||
|
return (function() {
|
||||||
|
var deps = (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isSxTruthy(isEmpty(componentDeps(val))))) ? componentDeps(val) : transitiveDeps(name, env));
|
||||||
|
return forEach(function(dep) { return (isSxTruthy(!isSxTruthy(contains(allNeeded, dep))) ? append_b(allNeeded, dep) : NIL); }, deps);
|
||||||
|
})();
|
||||||
|
})(); } }
|
||||||
|
return allNeeded;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// page-component-bundle
|
||||||
|
var pageComponentBundle = function(pageSource, env) { return componentsNeeded(pageSource, env); };
|
||||||
|
|
||||||
|
// page-css-classes
|
||||||
|
var pageCssClasses = function(pageSource, env) { return (function() {
|
||||||
|
var needed = componentsNeeded(pageSource, env);
|
||||||
|
var classes = [];
|
||||||
|
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() {
|
||||||
|
var val = envGet(env, name);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!isSxTruthy(contains(classes, cls))) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL);
|
||||||
|
})(); } }
|
||||||
|
{ var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(classes, cls)))) {
|
||||||
|
classes.push(cls);
|
||||||
|
} } }
|
||||||
|
return classes;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// scan-io-refs-walk
|
||||||
|
var scanIoRefsWalk = function(node, ioNames, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
|
||||||
|
var name = symbolName(node);
|
||||||
|
return (isSxTruthy(contains(ioNames, name)) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL);
|
||||||
|
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanIoRefsWalk(item, ioNames, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanIoRefsWalk(dictGet(node, key), ioNames, refs); }, keys(node)) : NIL))); };
|
||||||
|
|
||||||
|
// scan-io-refs
|
||||||
|
var scanIoRefs = function(node, ioNames) { return (function() {
|
||||||
|
var refs = [];
|
||||||
|
scanIoRefsWalk(node, ioNames, refs);
|
||||||
|
return refs;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// transitive-io-refs-walk
|
||||||
|
var transitiveIoRefsWalk = function(n, seen, allRefs, env, ioNames) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() {
|
||||||
|
var val = envGet(env, n);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(componentBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(componentBody(val)))) : (isSxTruthy((typeOf(val) == "macro")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(macroBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(macroBody(val)))) : NIL));
|
||||||
|
})()) : NIL); };
|
||||||
|
|
||||||
|
// transitive-io-refs
|
||||||
|
var transitiveIoRefs = function(name, env, ioNames) { return (function() {
|
||||||
|
var allRefs = [];
|
||||||
|
var seen = [];
|
||||||
|
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||||
|
transitiveIoRefsWalk(key, seen, allRefs, env, ioNames);
|
||||||
|
return allRefs;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// compute-all-io-refs
|
||||||
|
var computeAllIoRefs = function(env, ioNames) { return forEach(function(name) { return (function() {
|
||||||
|
var val = envGet(env, name);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? componentSetIoRefs(val, transitiveIoRefs(name, env, ioNames)) : NIL);
|
||||||
|
})(); }, envComponents(env)); };
|
||||||
|
|
||||||
|
// component-pure?
|
||||||
|
var componentPure_p = function(name, env, ioNames) { return isEmpty(transitiveIoRefs(name, env, ioNames)); };
|
||||||
|
|
||||||
|
|
||||||
// === Transpiled from router (client-side route matching) ===
|
// === Transpiled from router (client-side route matching) ===
|
||||||
|
|
||||||
// split-path-segments
|
// split-path-segments
|
||||||
@@ -4506,6 +4694,12 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); },
|
renderToSx: function(expr, env) { return renderToSx(expr, env || merge(componentEnv)); },
|
||||||
renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,
|
renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,
|
||||||
parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,
|
parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,
|
||||||
|
parseTime: typeof parseTime === "function" ? parseTime : null,
|
||||||
|
defaultTrigger: typeof defaultTrigger === "function" ? defaultTrigger : null,
|
||||||
|
parseSwapSpec: typeof parseSwapSpec === "function" ? parseSwapSpec : null,
|
||||||
|
parseRetrySpec: typeof parseRetrySpec === "function" ? parseRetrySpec : null,
|
||||||
|
nextRetryMs: typeof nextRetryMs === "function" ? nextRetryMs : null,
|
||||||
|
filterParams: typeof filterParams === "function" ? filterParams : null,
|
||||||
morphNode: typeof morphNode === "function" ? morphNode : null,
|
morphNode: typeof morphNode === "function" ? morphNode : null,
|
||||||
morphChildren: typeof morphChildren === "function" ? morphChildren : null,
|
morphChildren: typeof morphChildren === "function" ? morphChildren : null,
|
||||||
swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,
|
swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,
|
||||||
@@ -4520,6 +4714,17 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
getEnv: function() { return componentEnv; },
|
getEnv: function() { return componentEnv; },
|
||||||
resolveSuspense: typeof resolveSuspense === "function" ? resolveSuspense : null,
|
resolveSuspense: typeof resolveSuspense === "function" ? resolveSuspense : null,
|
||||||
init: typeof bootInit === "function" ? bootInit : null,
|
init: typeof bootInit === "function" ? bootInit : null,
|
||||||
|
scanRefs: scanRefs,
|
||||||
|
scanComponentsFromSource: scanComponentsFromSource,
|
||||||
|
transitiveDeps: transitiveDeps,
|
||||||
|
computeAllDeps: computeAllDeps,
|
||||||
|
componentsNeeded: componentsNeeded,
|
||||||
|
pageComponentBundle: pageComponentBundle,
|
||||||
|
pageCssClasses: pageCssClasses,
|
||||||
|
scanIoRefs: scanIoRefs,
|
||||||
|
transitiveIoRefs: transitiveIoRefs,
|
||||||
|
computeAllIoRefs: computeAllIoRefs,
|
||||||
|
componentPure_p: componentPure_p,
|
||||||
splitPathSegments: splitPathSegments,
|
splitPathSegments: splitPathSegments,
|
||||||
parseRoutePattern: parseRoutePattern,
|
parseRoutePattern: parseRoutePattern,
|
||||||
matchRoute: matchRoute,
|
matchRoute: matchRoute,
|
||||||
|
|||||||
@@ -131,6 +131,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadDepsFromBootstrap(env) {
|
||||||
|
if (Sx.scanRefs) {
|
||||||
|
env["scan-refs"] = Sx.scanRefs;
|
||||||
|
env["scan-components-from-source"] = Sx.scanComponentsFromSource;
|
||||||
|
env["transitive-deps"] = Sx.transitiveDeps;
|
||||||
|
env["compute-all-deps"] = Sx.computeAllDeps;
|
||||||
|
env["components-needed"] = Sx.componentsNeeded;
|
||||||
|
env["page-component-bundle"] = Sx.pageComponentBundle;
|
||||||
|
env["page-css-classes"] = Sx.pageCssClasses;
|
||||||
|
env["scan-io-refs"] = Sx.scanIoRefs;
|
||||||
|
env["transitive-io-refs"] = Sx.transitiveIoRefs;
|
||||||
|
env["compute-all-io-refs"] = Sx.computeAllIoRefs;
|
||||||
|
env["component-pure?"] = Sx.componentPure_p;
|
||||||
|
env["test-env"] = function() { return env; };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEngineFromBootstrap(env) {
|
||||||
|
if (Sx.parseTime) {
|
||||||
|
env["parse-time"] = Sx.parseTime;
|
||||||
|
env["parse-trigger-spec"] = Sx.parseTriggerSpec;
|
||||||
|
env["default-trigger"] = Sx.defaultTrigger;
|
||||||
|
env["parse-swap-spec"] = Sx.parseSwapSpec;
|
||||||
|
env["parse-retry-spec"] = Sx.parseRetrySpec;
|
||||||
|
env["next-retry-ms"] = function(cur, cap) { return Math.min(cur * 2, cap); };
|
||||||
|
env["filter-params"] = Sx.filterParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Legacy runner (monolithic test.sx) ---
|
// --- Legacy runner (monolithic test.sx) ---
|
||||||
window.sxRunTests = function(srcId, outId, btnId) {
|
window.sxRunTests = function(srcId, outId, btnId) {
|
||||||
var src = document.getElementById(srcId).textContent;
|
var src = document.getElementById(srcId).textContent;
|
||||||
@@ -169,6 +198,8 @@
|
|||||||
"parser": { needs: ["sx-parse"] },
|
"parser": { needs: ["sx-parse"] },
|
||||||
"router": { needs: [] },
|
"router": { needs: [] },
|
||||||
"render": { needs: ["render-html"] },
|
"render": { needs: ["render-html"] },
|
||||||
|
"deps": { needs: [] },
|
||||||
|
"engine": { needs: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
window.sxRunModularTests = function(specName, outId, btnId) {
|
window.sxRunModularTests = function(specName, outId, btnId) {
|
||||||
@@ -190,8 +221,10 @@
|
|||||||
var sn = specs[si];
|
var sn = specs[si];
|
||||||
if (!SPECS[sn]) continue;
|
if (!SPECS[sn]) continue;
|
||||||
|
|
||||||
// Load router from bootstrap if needed
|
// Load module functions from bootstrap
|
||||||
if (sn === "router") loadRouterFromBootstrap(ctx.env);
|
if (sn === "router") loadRouterFromBootstrap(ctx.env);
|
||||||
|
if (sn === "deps") loadDepsFromBootstrap(ctx.env);
|
||||||
|
if (sn === "engine") loadEngineFromBootstrap(ctx.env);
|
||||||
|
|
||||||
// Find spec source — either per-spec textarea or embedded in overview
|
// Find spec source — either per-spec textarea or embedded in overview
|
||||||
var specEl = document.getElementById("test-spec-" + sn);
|
var specEl = document.getElementById("test-spec-" + sn);
|
||||||
|
|||||||
@@ -4007,6 +4007,12 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
|
|||||||
api_lines.append(' renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,')
|
api_lines.append(' renderToDom: _hasDom ? function(expr, env, ns) { return renderToDom(expr, env || merge(componentEnv), ns || null); } : null,')
|
||||||
if has_engine:
|
if has_engine:
|
||||||
api_lines.append(' parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,')
|
api_lines.append(' parseTriggerSpec: typeof parseTriggerSpec === "function" ? parseTriggerSpec : null,')
|
||||||
|
api_lines.append(' parseTime: typeof parseTime === "function" ? parseTime : null,')
|
||||||
|
api_lines.append(' defaultTrigger: typeof defaultTrigger === "function" ? defaultTrigger : null,')
|
||||||
|
api_lines.append(' parseSwapSpec: typeof parseSwapSpec === "function" ? parseSwapSpec : null,')
|
||||||
|
api_lines.append(' parseRetrySpec: typeof parseRetrySpec === "function" ? parseRetrySpec : null,')
|
||||||
|
api_lines.append(' nextRetryMs: typeof nextRetryMs === "function" ? nextRetryMs : null,')
|
||||||
|
api_lines.append(' filterParams: typeof filterParams === "function" ? filterParams : null,')
|
||||||
api_lines.append(' morphNode: typeof morphNode === "function" ? morphNode : null,')
|
api_lines.append(' morphNode: typeof morphNode === "function" ? morphNode : null,')
|
||||||
api_lines.append(' morphChildren: typeof morphChildren === "function" ? morphChildren : null,')
|
api_lines.append(' morphChildren: typeof morphChildren === "function" ? morphChildren : null,')
|
||||||
api_lines.append(' swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,')
|
api_lines.append(' swapDomNodes: typeof swapDomNodes === "function" ? swapDomNodes : null,')
|
||||||
@@ -4027,6 +4033,7 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_cssx, has
|
|||||||
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
|
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
|
||||||
if has_deps:
|
if has_deps:
|
||||||
api_lines.append(' scanRefs: scanRefs,')
|
api_lines.append(' scanRefs: scanRefs,')
|
||||||
|
api_lines.append(' scanComponentsFromSource: scanComponentsFromSource,')
|
||||||
api_lines.append(' transitiveDeps: transitiveDeps,')
|
api_lines.append(' transitiveDeps: transitiveDeps,')
|
||||||
api_lines.append(' computeAllDeps: computeAllDeps,')
|
api_lines.append(' computeAllDeps: computeAllDeps,')
|
||||||
api_lines.append(' componentsNeeded: componentsNeeded,')
|
api_lines.append(' componentsNeeded: componentsNeeded,')
|
||||||
|
|||||||
@@ -854,6 +854,7 @@ ADAPTER_FILES = {
|
|||||||
SPEC_MODULES = {
|
SPEC_MODULES = {
|
||||||
"deps": ("deps.sx", "deps (component dependency analysis)"),
|
"deps": ("deps.sx", "deps (component dependency analysis)"),
|
||||||
"router": ("router.sx", "router (client-side route matching)"),
|
"router": ("router.sx", "router (client-side route matching)"),
|
||||||
|
"engine": ("engine.sx", "engine (fetch/swap/trigger pure logic)"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -958,7 +959,7 @@ def compile_ref_to_py(
|
|||||||
Valid names: continuations.
|
Valid names: continuations.
|
||||||
None = no extensions.
|
None = no extensions.
|
||||||
spec_modules: List of spec module names to include.
|
spec_modules: List of spec module names to include.
|
||||||
Valid names: deps.
|
Valid names: deps, engine.
|
||||||
None = no spec modules.
|
None = no spec modules.
|
||||||
"""
|
"""
|
||||||
# Determine which primitive modules to include
|
# Determine which primitive modules to include
|
||||||
@@ -1832,10 +1833,15 @@ PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
|
|||||||
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
|
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
|
||||||
|
|
||||||
def _sx_parse_int(v, default=0):
|
def _sx_parse_int(v, default=0):
|
||||||
try:
|
if v is None or v is NIL:
|
||||||
return _b_int(v)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return default
|
return default
|
||||||
|
s = str(v).strip()
|
||||||
|
# Match JS parseInt: extract leading integer portion
|
||||||
|
import re as _re
|
||||||
|
m = _re.match(r'^[+-]?\\d+', s)
|
||||||
|
if m:
|
||||||
|
return _b_int(m.group())
|
||||||
|
return default
|
||||||
''',
|
''',
|
||||||
|
|
||||||
"stdlib.text": '''
|
"stdlib.text": '''
|
||||||
@@ -1976,6 +1982,10 @@ concat = PRIMITIVES["concat"]
|
|||||||
split = PRIMITIVES["split"]
|
split = PRIMITIVES["split"]
|
||||||
length = PRIMITIVES["len"]
|
length = PRIMITIVES["len"]
|
||||||
merge = PRIMITIVES["merge"]
|
merge = PRIMITIVES["merge"]
|
||||||
|
trim = PRIMITIVES["trim"]
|
||||||
|
replace = PRIMITIVES["replace"]
|
||||||
|
parse_int = PRIMITIVES["parse-int"]
|
||||||
|
upper = PRIMITIVES["upper"]
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@@ -2189,7 +2199,7 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--spec-modules",
|
"--spec-modules",
|
||||||
default=None,
|
default=None,
|
||||||
help="Comma-separated spec modules (deps). Default: none.",
|
help="Comma-separated spec modules (deps,engine). Default: none.",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
adapters = args.adapters.split(",") if args.adapters else None
|
adapters = args.adapters.split(",") if args.adapters else None
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# WARNING: special-forms.sx declares forms not in eval.sx: reset, shift
|
||||||
"""
|
"""
|
||||||
sx_ref.py -- Generated from reference SX evaluator specification.
|
sx_ref.py -- Generated from reference SX evaluator specification.
|
||||||
|
|
||||||
@@ -777,10 +778,15 @@ PRIMITIVES["parse-int"] = lambda v, d=0: _sx_parse_int(v, d)
|
|||||||
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
|
PRIMITIVES["parse-datetime"] = lambda s: str(s) if s else NIL
|
||||||
|
|
||||||
def _sx_parse_int(v, default=0):
|
def _sx_parse_int(v, default=0):
|
||||||
try:
|
if v is None or v is NIL:
|
||||||
return _b_int(v)
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return default
|
return default
|
||||||
|
s = str(v).strip()
|
||||||
|
# Match JS parseInt: extract leading integer portion
|
||||||
|
import re as _re
|
||||||
|
m = _re.match(r'^[+-]?\d+', s)
|
||||||
|
if m:
|
||||||
|
return _b_int(m.group())
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
# stdlib.text
|
# stdlib.text
|
||||||
@@ -879,6 +885,10 @@ concat = PRIMITIVES["concat"]
|
|||||||
split = PRIMITIVES["split"]
|
split = PRIMITIVES["split"]
|
||||||
length = PRIMITIVES["len"]
|
length = PRIMITIVES["len"]
|
||||||
merge = PRIMITIVES["merge"]
|
merge = PRIMITIVES["merge"]
|
||||||
|
trim = PRIMITIVES["trim"]
|
||||||
|
replace = PRIMITIVES["replace"]
|
||||||
|
parse_int = PRIMITIVES["parse-int"]
|
||||||
|
upper = PRIMITIVES["upper"]
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -1252,38 +1262,133 @@ compute_all_io_refs = lambda env, io_names: for_each(lambda name: (lambda val: (
|
|||||||
component_pure_p = lambda name, env, io_names: empty_p(transitive_io_refs(name, env, io_names))
|
component_pure_p = lambda name, env, io_names: empty_p(transitive_io_refs(name, env, io_names))
|
||||||
|
|
||||||
|
|
||||||
# === Transpiled from router (client-side route matching) ===
|
# === Transpiled from engine (fetch/swap/trigger pure logic) ===
|
||||||
|
|
||||||
# split-path-segments
|
# ENGINE_VERBS
|
||||||
split_path_segments = lambda path: (lambda trimmed: (lambda trimmed2: ([] if sx_truthy(empty_p(trimmed2)) else split(trimmed2, '/')))((slice(trimmed, 0, (length(trimmed) - 1)) if sx_truthy(((not sx_truthy(empty_p(trimmed))) if not sx_truthy((not sx_truthy(empty_p(trimmed)))) else ends_with_p(trimmed, '/'))) else trimmed)))((slice(path, 1) if sx_truthy(starts_with_p(path, '/')) else path))
|
ENGINE_VERBS = ['get', 'post', 'put', 'delete', 'patch']
|
||||||
|
|
||||||
# make-route-segment
|
# DEFAULT_SWAP
|
||||||
make_route_segment = lambda seg: ((lambda param_name: (lambda d: _sx_begin(_sx_dict_set(d, 'type', 'param'), _sx_dict_set(d, 'value', param_name), d))({}))(slice(seg, 1, (length(seg) - 1))) if sx_truthy((starts_with_p(seg, '<') if not sx_truthy(starts_with_p(seg, '<')) else ends_with_p(seg, '>'))) else (lambda d: _sx_begin(_sx_dict_set(d, 'type', 'literal'), _sx_dict_set(d, 'value', seg), d))({}))
|
DEFAULT_SWAP = 'outerHTML'
|
||||||
|
|
||||||
# parse-route-pattern
|
# parse-time
|
||||||
parse_route_pattern = lambda pattern: (lambda segments: map(make_route_segment, segments))(split_path_segments(pattern))
|
parse_time = lambda s: (0 if sx_truthy(is_nil(s)) else (parse_int(s, 0) if sx_truthy(ends_with_p(s, 'ms')) else ((parse_int(replace(s, 's', ''), 0) * 1000) if sx_truthy(ends_with_p(s, 's')) else parse_int(s, 0))))
|
||||||
|
|
||||||
# match-route-segments
|
# parse-trigger-spec
|
||||||
def match_route_segments(path_segs, parsed_segs):
|
parse_trigger_spec = lambda spec: (NIL if sx_truthy(is_nil(spec)) else (lambda raw_parts: filter(lambda x: (not sx_truthy(is_nil(x))), map(lambda part: (lambda tokens: (NIL if sx_truthy(empty_p(tokens)) else ({'event': 'every', 'modifiers': {'interval': parse_time(nth(tokens, 1))}} if sx_truthy(((first(tokens) == 'every') if not sx_truthy((first(tokens) == 'every')) else (len(tokens) >= 2))) else (lambda mods: _sx_begin(for_each(lambda tok: (_sx_dict_set(mods, 'once', True) if sx_truthy((tok == 'once')) else (_sx_dict_set(mods, 'changed', True) if sx_truthy((tok == 'changed')) else (_sx_dict_set(mods, 'delay', parse_time(slice(tok, 6))) if sx_truthy(starts_with_p(tok, 'delay:')) else (_sx_dict_set(mods, 'from', slice(tok, 5)) if sx_truthy(starts_with_p(tok, 'from:')) else NIL)))), rest(tokens)), {'event': first(tokens), 'modifiers': mods}))({}))))(split(trim(part), ' ')), raw_parts)))(split(spec, ',')))
|
||||||
|
|
||||||
|
# default-trigger
|
||||||
|
default_trigger = lambda tag_name: ([{'event': 'submit', 'modifiers': {}}] if sx_truthy((tag_name == 'FORM')) else ([{'event': 'change', 'modifiers': {}}] if sx_truthy(((tag_name == 'INPUT') if sx_truthy((tag_name == 'INPUT')) else ((tag_name == 'SELECT') if sx_truthy((tag_name == 'SELECT')) else (tag_name == 'TEXTAREA')))) else [{'event': 'click', 'modifiers': {}}]))
|
||||||
|
|
||||||
|
# get-verb-info
|
||||||
|
get_verb_info = lambda el: some(lambda verb: (lambda url: ({'method': upper(verb), 'url': url} if sx_truthy(url) else NIL))(dom_get_attr(el, sx_str('sx-', verb))), ENGINE_VERBS)
|
||||||
|
|
||||||
|
# build-request-headers
|
||||||
|
build_request_headers = lambda el, loaded_components, css_hash: (lambda headers: _sx_begin((lambda target_sel: (_sx_dict_set(headers, 'SX-Target', target_sel) if sx_truthy(target_sel) else NIL))(dom_get_attr(el, 'sx-target')), (_sx_dict_set(headers, 'SX-Components', join(',', loaded_components)) if sx_truthy((not sx_truthy(empty_p(loaded_components)))) else NIL), (_sx_dict_set(headers, 'SX-Css', css_hash) if sx_truthy(css_hash) else NIL), (lambda extra_h: ((lambda parsed: (for_each(lambda key: _sx_dict_set(headers, key, sx_str(get(parsed, key))), keys(parsed)) if sx_truthy(parsed) else NIL))(parse_header_value(extra_h)) if sx_truthy(extra_h) else NIL))(dom_get_attr(el, 'sx-headers')), headers))({'SX-Request': 'true', 'SX-Current-URL': browser_location_href()})
|
||||||
|
|
||||||
|
# process-response-headers
|
||||||
|
process_response_headers = lambda get_header: {'redirect': get_header('SX-Redirect'), 'refresh': get_header('SX-Refresh'), 'trigger': get_header('SX-Trigger'), 'retarget': get_header('SX-Retarget'), 'reswap': get_header('SX-Reswap'), 'location': get_header('SX-Location'), 'replace-url': get_header('SX-Replace-Url'), 'css-hash': get_header('SX-Css-Hash'), 'trigger-swap': get_header('SX-Trigger-After-Swap'), 'trigger-settle': get_header('SX-Trigger-After-Settle'), 'content-type': get_header('Content-Type')}
|
||||||
|
|
||||||
|
# parse-swap-spec
|
||||||
|
def parse_swap_spec(raw_swap, global_transitions_p):
|
||||||
_cells = {}
|
_cells = {}
|
||||||
return (NIL if sx_truthy((not sx_truthy((length(path_segs) == length(parsed_segs))))) else (lambda params: _sx_begin(_sx_cell_set(_cells, 'matched', True), _sx_begin(for_each_indexed(lambda i, parsed_seg: ((lambda path_seg: (lambda seg_type: ((_sx_cell_set(_cells, 'matched', False) if sx_truthy((not sx_truthy((path_seg == get(parsed_seg, 'value'))))) else NIL) if sx_truthy((seg_type == 'literal')) else (_sx_dict_set(params, get(parsed_seg, 'value'), path_seg) if sx_truthy((seg_type == 'param')) else _sx_cell_set(_cells, 'matched', False))))(get(parsed_seg, 'type')))(nth(path_segs, i)) if sx_truthy(_cells['matched']) else NIL), parsed_segs), (params if sx_truthy(_cells['matched']) else NIL))))({}))
|
parts = split((raw_swap if sx_truthy(raw_swap) else DEFAULT_SWAP), ' ')
|
||||||
|
style = first(parts)
|
||||||
|
_cells['use_transition'] = global_transitions_p
|
||||||
|
for p in rest(parts):
|
||||||
|
if sx_truthy((p == 'transition:true')):
|
||||||
|
_cells['use_transition'] = True
|
||||||
|
elif sx_truthy((p == 'transition:false')):
|
||||||
|
_cells['use_transition'] = False
|
||||||
|
return {'style': style, 'transition': _cells['use_transition']}
|
||||||
|
|
||||||
# match-route
|
# parse-retry-spec
|
||||||
match_route = lambda path, pattern: (lambda path_segs: (lambda parsed_segs: match_route_segments(path_segs, parsed_segs))(parse_route_pattern(pattern)))(split_path_segments(path))
|
parse_retry_spec = lambda retry_attr: (NIL if sx_truthy(is_nil(retry_attr)) else (lambda parts: {'strategy': first(parts), 'start-ms': parse_int(nth(parts, 1), 1000), 'cap-ms': parse_int(nth(parts, 2), 30000)})(split(retry_attr, ':')))
|
||||||
|
|
||||||
# find-matching-route
|
# next-retry-ms
|
||||||
def find_matching_route(path, routes):
|
next_retry_ms = lambda current_ms, cap_ms: min((current_ms * 2), cap_ms)
|
||||||
|
|
||||||
|
# filter-params
|
||||||
|
filter_params = lambda params_spec, all_params: (all_params if sx_truthy(is_nil(params_spec)) else ([] if sx_truthy((params_spec == 'none')) else (all_params if sx_truthy((params_spec == '*')) else ((lambda excluded: filter(lambda p: (not sx_truthy(contains_p(excluded, first(p)))), all_params))(map(trim, split(slice(params_spec, 4), ','))) if sx_truthy(starts_with_p(params_spec, 'not ')) else (lambda allowed: filter(lambda p: contains_p(allowed, first(p)), all_params))(map(trim, split(params_spec, ',')))))))
|
||||||
|
|
||||||
|
# resolve-target
|
||||||
|
resolve_target = lambda el: (lambda sel: (el if sx_truthy((is_nil(sel) if sx_truthy(is_nil(sel)) else (sel == 'this'))) else (dom_parent(el) if sx_truthy((sel == 'closest')) else dom_query(sel))))(dom_get_attr(el, 'sx-target'))
|
||||||
|
|
||||||
|
# apply-optimistic
|
||||||
|
apply_optimistic = lambda el: (lambda directive: (NIL if sx_truthy(is_nil(directive)) else (lambda target: (lambda state: _sx_begin((_sx_begin(_sx_dict_set(state, 'opacity', dom_get_style(target, 'opacity')), dom_set_style(target, 'opacity', '0'), dom_set_style(target, 'pointer-events', 'none')) if sx_truthy((directive == 'remove')) else (_sx_begin(_sx_dict_set(state, 'disabled', dom_get_prop(target, 'disabled')), dom_set_prop(target, 'disabled', True)) if sx_truthy((directive == 'disable')) else ((lambda cls: _sx_begin(_sx_dict_set(state, 'add-class', cls), dom_add_class(target, cls)))(slice(directive, 10)) if sx_truthy(starts_with_p(directive, 'add-class:')) else NIL))), state))({'target': target, 'directive': directive}))((resolve_target(el) if sx_truthy(resolve_target(el)) else el))))(dom_get_attr(el, 'sx-optimistic'))
|
||||||
|
|
||||||
|
# revert-optimistic
|
||||||
|
revert_optimistic = lambda state: ((lambda target: (lambda directive: (_sx_begin(dom_set_style(target, 'opacity', (get(state, 'opacity') if sx_truthy(get(state, 'opacity')) else '')), dom_set_style(target, 'pointer-events', '')) if sx_truthy((directive == 'remove')) else (dom_set_prop(target, 'disabled', (get(state, 'disabled') if sx_truthy(get(state, 'disabled')) else False)) if sx_truthy((directive == 'disable')) else (dom_remove_class(target, get(state, 'add-class')) if sx_truthy(get(state, 'add-class')) else NIL))))(get(state, 'directive')))(get(state, 'target')) if sx_truthy(state) else NIL)
|
||||||
|
|
||||||
|
# find-oob-swaps
|
||||||
|
find_oob_swaps = lambda container: (lambda results: _sx_begin(for_each(lambda attr: (lambda oob_els: for_each(lambda oob: (lambda swap_type: (lambda target_id: _sx_begin(dom_remove_attr(oob, attr), (_sx_append(results, {'element': oob, 'swap-type': swap_type, 'target-id': target_id}) if sx_truthy(target_id) else NIL)))(dom_id(oob)))((dom_get_attr(oob, attr) if sx_truthy(dom_get_attr(oob, attr)) else 'outerHTML')), oob_els))(dom_query_all(container, sx_str('[', attr, ']'))), ['sx-swap-oob', 'hx-swap-oob']), results))([])
|
||||||
|
|
||||||
|
# morph-node
|
||||||
|
morph_node = lambda old_node, new_node: (NIL if sx_truthy((dom_has_attr_p(old_node, 'sx-preserve') if sx_truthy(dom_has_attr_p(old_node, 'sx-preserve')) else dom_has_attr_p(old_node, 'sx-ignore'))) else (dom_replace_child(dom_parent(old_node), dom_clone(new_node), old_node) if sx_truthy(((not sx_truthy((dom_node_type(old_node) == dom_node_type(new_node)))) if sx_truthy((not sx_truthy((dom_node_type(old_node) == dom_node_type(new_node))))) else (not sx_truthy((dom_node_name(old_node) == dom_node_name(new_node)))))) else ((dom_set_text_content(old_node, dom_text_content(new_node)) if sx_truthy((not sx_truthy((dom_text_content(old_node) == dom_text_content(new_node))))) else NIL) if sx_truthy(((dom_node_type(old_node) == 3) if sx_truthy((dom_node_type(old_node) == 3)) else (dom_node_type(old_node) == 8))) else (_sx_begin(sync_attrs(old_node, new_node), (morph_children(old_node, new_node) if sx_truthy((not sx_truthy((dom_is_active_element_p(old_node) if not sx_truthy(dom_is_active_element_p(old_node)) else dom_is_input_element_p(old_node))))) else NIL)) if sx_truthy((dom_node_type(old_node) == 1)) else NIL))))
|
||||||
|
|
||||||
|
# sync-attrs
|
||||||
|
sync_attrs = _sx_fn(lambda old_el, new_el: (
|
||||||
|
for_each(lambda attr: (lambda name: (lambda val: (dom_set_attr(old_el, name, val) if sx_truthy((not sx_truthy((dom_get_attr(old_el, name) == val)))) else NIL))(nth(attr, 1)))(first(attr)), dom_attr_list(new_el)),
|
||||||
|
for_each(lambda attr: (dom_remove_attr(old_el, first(attr)) if sx_truthy((not sx_truthy(dom_has_attr_p(new_el, first(attr))))) else NIL), dom_attr_list(old_el))
|
||||||
|
)[-1])
|
||||||
|
|
||||||
|
# morph-children
|
||||||
|
def morph_children(old_parent, new_parent):
|
||||||
_cells = {}
|
_cells = {}
|
||||||
path_segs = split_path_segments(path)
|
old_kids = dom_child_list(old_parent)
|
||||||
_cells['result'] = NIL
|
new_kids = dom_child_list(new_parent)
|
||||||
for route in routes:
|
old_by_id = reduce(lambda acc, kid: (lambda id: (_sx_begin(_sx_dict_set(acc, id, kid), acc) if sx_truthy(id) else acc))(dom_id(kid)), {}, old_kids)
|
||||||
if sx_truthy(is_nil(_cells['result'])):
|
_cells['oi'] = 0
|
||||||
params = match_route_segments(path_segs, get(route, 'parsed'))
|
for new_child in new_kids:
|
||||||
if sx_truthy((not sx_truthy(is_nil(params)))):
|
match_id = dom_id(new_child)
|
||||||
matched = merge(route, {})
|
match_by_id = (dict_get(old_by_id, match_id) if sx_truthy(match_id) else NIL)
|
||||||
matched['params'] = params
|
if sx_truthy((match_by_id if not sx_truthy(match_by_id) else (not sx_truthy(is_nil(match_by_id))))):
|
||||||
_cells['result'] = matched
|
if sx_truthy(((_cells['oi'] < len(old_kids)) if not sx_truthy((_cells['oi'] < len(old_kids))) else (not sx_truthy((match_by_id == nth(old_kids, _cells['oi'])))))):
|
||||||
return _cells['result']
|
dom_insert_before(old_parent, match_by_id, (nth(old_kids, _cells['oi']) if sx_truthy((_cells['oi'] < len(old_kids))) else NIL))
|
||||||
|
morph_node(match_by_id, new_child)
|
||||||
|
_cells['oi'] = (_cells['oi'] + 1)
|
||||||
|
elif sx_truthy((_cells['oi'] < len(old_kids))):
|
||||||
|
old_child = nth(old_kids, _cells['oi'])
|
||||||
|
if sx_truthy((dom_id(old_child) if not sx_truthy(dom_id(old_child)) else (not sx_truthy(match_id)))):
|
||||||
|
dom_insert_before(old_parent, dom_clone(new_child), old_child)
|
||||||
|
else:
|
||||||
|
morph_node(old_child, new_child)
|
||||||
|
_cells['oi'] = (_cells['oi'] + 1)
|
||||||
|
else:
|
||||||
|
dom_append(old_parent, dom_clone(new_child))
|
||||||
|
return for_each(lambda i: ((lambda leftover: (dom_remove_child(old_parent, leftover) if sx_truthy((dom_is_child_of_p(leftover, old_parent) if not sx_truthy(dom_is_child_of_p(leftover, old_parent)) else ((not sx_truthy(dom_has_attr_p(leftover, 'sx-preserve'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(leftover, 'sx-preserve')))) else (not sx_truthy(dom_has_attr_p(leftover, 'sx-ignore')))))) else NIL))(nth(old_kids, i)) if sx_truthy((i >= _cells['oi'])) else NIL), range(_cells['oi'], len(old_kids)))
|
||||||
|
|
||||||
|
# swap-dom-nodes
|
||||||
|
swap_dom_nodes = lambda target, new_nodes, strategy: _sx_case(strategy, [('innerHTML', lambda: (morph_children(target, new_nodes) if sx_truthy(dom_is_fragment_p(new_nodes)) else (lambda wrapper: _sx_begin(dom_append(wrapper, new_nodes), morph_children(target, wrapper)))(dom_create_element('div', NIL)))), ('outerHTML', lambda: (lambda parent: _sx_begin(((lambda fc: (_sx_begin(morph_node(target, fc), (lambda sib: insert_remaining_siblings(parent, target, sib))(dom_next_sibling(fc))) if sx_truthy(fc) else dom_remove_child(parent, target)))(dom_first_child(new_nodes)) if sx_truthy(dom_is_fragment_p(new_nodes)) else morph_node(target, new_nodes)), parent))(dom_parent(target))), ('afterend', lambda: dom_insert_after(target, new_nodes)), ('beforeend', lambda: dom_append(target, new_nodes)), ('afterbegin', lambda: dom_prepend(target, new_nodes)), ('beforebegin', lambda: dom_insert_before(dom_parent(target), new_nodes, target)), ('delete', lambda: dom_remove_child(dom_parent(target), target)), ('none', lambda: NIL), (None, lambda: (morph_children(target, new_nodes) if sx_truthy(dom_is_fragment_p(new_nodes)) else (lambda wrapper: _sx_begin(dom_append(wrapper, new_nodes), morph_children(target, wrapper)))(dom_create_element('div', NIL))))])
|
||||||
|
|
||||||
|
# insert-remaining-siblings
|
||||||
|
insert_remaining_siblings = lambda parent, ref_node, sib: ((lambda next: _sx_begin(dom_insert_after(ref_node, sib), insert_remaining_siblings(parent, sib, next)))(dom_next_sibling(sib)) if sx_truthy(sib) else NIL)
|
||||||
|
|
||||||
|
# swap-html-string
|
||||||
|
swap_html_string = lambda target, html, strategy: _sx_case(strategy, [('innerHTML', lambda: dom_set_inner_html(target, html)), ('outerHTML', lambda: (lambda parent: _sx_begin(dom_insert_adjacent_html(target, 'afterend', html), dom_remove_child(parent, target), parent))(dom_parent(target))), ('afterend', lambda: dom_insert_adjacent_html(target, 'afterend', html)), ('beforeend', lambda: dom_insert_adjacent_html(target, 'beforeend', html)), ('afterbegin', lambda: dom_insert_adjacent_html(target, 'afterbegin', html)), ('beforebegin', lambda: dom_insert_adjacent_html(target, 'beforebegin', html)), ('delete', lambda: dom_remove_child(dom_parent(target), target)), ('none', lambda: NIL), (None, lambda: dom_set_inner_html(target, html))])
|
||||||
|
|
||||||
|
# handle-history
|
||||||
|
handle_history = lambda el, url, resp_headers: (lambda push_url: (lambda replace_url: (lambda hdr_replace: (browser_replace_state(hdr_replace) if sx_truthy(hdr_replace) else (browser_push_state((url if sx_truthy((push_url == 'true')) else push_url)) if sx_truthy((push_url if not sx_truthy(push_url) else (not sx_truthy((push_url == 'false'))))) else (browser_replace_state((url if sx_truthy((replace_url == 'true')) else replace_url)) if sx_truthy((replace_url if not sx_truthy(replace_url) else (not sx_truthy((replace_url == 'false'))))) else NIL))))(get(resp_headers, 'replace-url')))(dom_get_attr(el, 'sx-replace-url')))(dom_get_attr(el, 'sx-push-url'))
|
||||||
|
|
||||||
|
# PRELOAD_TTL
|
||||||
|
PRELOAD_TTL = 30000
|
||||||
|
|
||||||
|
# preload-cache-get
|
||||||
|
preload_cache_get = lambda cache, url: (lambda entry: (NIL if sx_truthy(is_nil(entry)) else (_sx_begin(dict_delete(cache, url), NIL) if sx_truthy(((now_ms() - get(entry, 'timestamp')) > PRELOAD_TTL)) else _sx_begin(dict_delete(cache, url), entry))))(dict_get(cache, url))
|
||||||
|
|
||||||
|
# preload-cache-set
|
||||||
|
preload_cache_set = lambda cache, url, text, content_type: _sx_dict_set(cache, url, {'text': text, 'content-type': content_type, 'timestamp': now_ms()})
|
||||||
|
|
||||||
|
# classify-trigger
|
||||||
|
classify_trigger = lambda trigger: (lambda event: ('poll' if sx_truthy((event == 'every')) else ('intersect' if sx_truthy((event == 'intersect')) else ('load' if sx_truthy((event == 'load')) else ('revealed' if sx_truthy((event == 'revealed')) else 'event')))))(get(trigger, 'event'))
|
||||||
|
|
||||||
|
# should-boost-link?
|
||||||
|
should_boost_link_p = lambda link: (lambda href: (href if not sx_truthy(href) else ((not sx_truthy(starts_with_p(href, '#'))) if not sx_truthy((not sx_truthy(starts_with_p(href, '#')))) else ((not sx_truthy(starts_with_p(href, 'javascript:'))) if not sx_truthy((not sx_truthy(starts_with_p(href, 'javascript:')))) else ((not sx_truthy(starts_with_p(href, 'mailto:'))) if not sx_truthy((not sx_truthy(starts_with_p(href, 'mailto:')))) else (browser_same_origin_p(href) if not sx_truthy(browser_same_origin_p(href)) else ((not sx_truthy(dom_has_attr_p(link, 'sx-get'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(link, 'sx-get')))) else ((not sx_truthy(dom_has_attr_p(link, 'sx-post'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(link, 'sx-post')))) else (not sx_truthy(dom_has_attr_p(link, 'sx-disable')))))))))))(dom_get_attr(link, 'href'))
|
||||||
|
|
||||||
|
# should-boost-form?
|
||||||
|
should_boost_form_p = lambda form: ((not sx_truthy(dom_has_attr_p(form, 'sx-get'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(form, 'sx-get')))) else ((not sx_truthy(dom_has_attr_p(form, 'sx-post'))) if not sx_truthy((not sx_truthy(dom_has_attr_p(form, 'sx-post')))) else (not sx_truthy(dom_has_attr_p(form, 'sx-disable')))))
|
||||||
|
|
||||||
|
# parse-sse-swap
|
||||||
|
parse_sse_swap = lambda el: (dom_get_attr(el, 'sx-sse-swap') if sx_truthy(dom_get_attr(el, 'sx-sse-swap')) else 'message')
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -1318,64 +1423,6 @@ def _wrap_aser_outputs():
|
|||||||
aser_fragment = _aser_fragment_wrapped
|
aser_fragment = _aser_fragment_wrapped
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# Extension: delimited continuations (shift/reset)
|
|
||||||
# =========================================================================
|
|
||||||
|
|
||||||
_RESET_RESUME = [] # stack of resume values; empty = not resuming
|
|
||||||
|
|
||||||
_SPECIAL_FORM_NAMES = _SPECIAL_FORM_NAMES | frozenset(["reset", "shift"])
|
|
||||||
|
|
||||||
def sf_reset(args, env):
|
|
||||||
"""(reset body) -- establish a continuation delimiter."""
|
|
||||||
body = first(args)
|
|
||||||
try:
|
|
||||||
return trampoline(eval_expr(body, env))
|
|
||||||
except _ShiftSignal as sig:
|
|
||||||
def cont_fn(value=NIL):
|
|
||||||
_RESET_RESUME.append(value)
|
|
||||||
try:
|
|
||||||
return trampoline(eval_expr(body, env))
|
|
||||||
finally:
|
|
||||||
_RESET_RESUME.pop()
|
|
||||||
k = Continuation(cont_fn)
|
|
||||||
sig_env = dict(sig.env)
|
|
||||||
sig_env[sig.k_name] = k
|
|
||||||
return trampoline(eval_expr(sig.body, sig_env))
|
|
||||||
|
|
||||||
def sf_shift(args, env):
|
|
||||||
"""(shift k body) -- capture continuation to nearest reset."""
|
|
||||||
if _RESET_RESUME:
|
|
||||||
return _RESET_RESUME[-1]
|
|
||||||
k_name = symbol_name(first(args))
|
|
||||||
body = nth(args, 1)
|
|
||||||
raise _ShiftSignal(k_name, body, env)
|
|
||||||
|
|
||||||
# Wrap eval_list to inject shift/reset dispatch
|
|
||||||
_base_eval_list = eval_list
|
|
||||||
def _eval_list_with_continuations(expr, env):
|
|
||||||
head = first(expr)
|
|
||||||
if type_of(head) == "symbol":
|
|
||||||
name = symbol_name(head)
|
|
||||||
args = rest(expr)
|
|
||||||
if name == "reset":
|
|
||||||
return sf_reset(args, env)
|
|
||||||
if name == "shift":
|
|
||||||
return sf_shift(args, env)
|
|
||||||
return _base_eval_list(expr, env)
|
|
||||||
eval_list = _eval_list_with_continuations
|
|
||||||
|
|
||||||
# Inject into aser_special
|
|
||||||
_base_aser_special = aser_special
|
|
||||||
def _aser_special_with_continuations(name, expr, env):
|
|
||||||
if name == "reset":
|
|
||||||
return sf_reset(expr[1:], env)
|
|
||||||
if name == "shift":
|
|
||||||
return sf_shift(expr[1:], env)
|
|
||||||
return _base_aser_special(name, expr, env)
|
|
||||||
aser_special = _aser_special_with_continuations
|
|
||||||
|
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Public API
|
# Public API
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
225
shared/sx/ref/test-deps.sx
Normal file
225
shared/sx/ref/test-deps.sx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
;; ==========================================================================
|
||||||
|
;; test-deps.sx — Tests for component dependency analysis (deps.sx)
|
||||||
|
;;
|
||||||
|
;; Requires: test-framework.sx loaded first.
|
||||||
|
;; Platform functions: scan-refs, transitive-deps, components-needed,
|
||||||
|
;; component-pure?, scan-io-refs, transitive-io-refs,
|
||||||
|
;; scan-components-from-source, test-env
|
||||||
|
;; (loaded from bootstrapped output by test runners)
|
||||||
|
;; ==========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; Test component definitions — these exist in the test env for dep analysis
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defcomp ~dep-leaf ()
|
||||||
|
(span "leaf"))
|
||||||
|
|
||||||
|
(defcomp ~dep-branch ()
|
||||||
|
(div (~dep-leaf)))
|
||||||
|
|
||||||
|
(defcomp ~dep-trunk ()
|
||||||
|
(div (~dep-branch) (~dep-leaf)))
|
||||||
|
|
||||||
|
(defcomp ~dep-conditional (&key show?)
|
||||||
|
(if show?
|
||||||
|
(~dep-leaf)
|
||||||
|
(~dep-branch)))
|
||||||
|
|
||||||
|
(defcomp ~dep-nested-cond (&key mode)
|
||||||
|
(cond
|
||||||
|
(= mode "a") (~dep-leaf)
|
||||||
|
(= mode "b") (~dep-branch)
|
||||||
|
:else (~dep-trunk)))
|
||||||
|
|
||||||
|
(defcomp ~dep-island ()
|
||||||
|
(div "no deps"))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 1. scan-refs — finds component references in AST nodes
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "scan-refs"
|
||||||
|
|
||||||
|
(deftest "empty for string literal"
|
||||||
|
(assert-equal (list) (scan-refs "hello")))
|
||||||
|
|
||||||
|
(deftest "empty for number"
|
||||||
|
(assert-equal (list) (scan-refs 42)))
|
||||||
|
|
||||||
|
(deftest "finds component symbol"
|
||||||
|
(let ((refs (scan-refs (quote (~dep-leaf)))))
|
||||||
|
(assert-contains "~dep-leaf" refs)))
|
||||||
|
|
||||||
|
(deftest "finds in nested list"
|
||||||
|
(let ((refs (scan-refs (quote (div (span (~dep-leaf)))))))
|
||||||
|
(assert-contains "~dep-leaf" refs)))
|
||||||
|
|
||||||
|
(deftest "finds multiple refs"
|
||||||
|
(let ((refs (scan-refs (quote (div (~dep-leaf) (~dep-branch))))))
|
||||||
|
(assert-contains "~dep-leaf" refs)
|
||||||
|
(assert-contains "~dep-branch" refs)))
|
||||||
|
|
||||||
|
(deftest "deduplicates"
|
||||||
|
(let ((refs (scan-refs (quote (div (~dep-leaf) (~dep-leaf))))))
|
||||||
|
(assert-equal 1 (len refs))))
|
||||||
|
|
||||||
|
(deftest "walks if branches"
|
||||||
|
(let ((refs (scan-refs (quote (if true (~dep-leaf) (~dep-branch))))))
|
||||||
|
(assert-contains "~dep-leaf" refs)
|
||||||
|
(assert-contains "~dep-branch" refs)))
|
||||||
|
|
||||||
|
(deftest "walks cond branches"
|
||||||
|
(let ((refs (scan-refs (quote (cond (= x 1) (~dep-leaf) :else (~dep-trunk))))))
|
||||||
|
(assert-contains "~dep-leaf" refs)
|
||||||
|
(assert-contains "~dep-trunk" refs)))
|
||||||
|
|
||||||
|
(deftest "ignores non-component symbols"
|
||||||
|
(let ((refs (scan-refs (quote (div class "foo")))))
|
||||||
|
(assert-equal 0 (len refs)))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 2. scan-components-from-source — regex-based source string scanning
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "scan-components-from-source"
|
||||||
|
|
||||||
|
(deftest "finds single component"
|
||||||
|
(let ((refs (scan-components-from-source "(~dep-leaf)")))
|
||||||
|
(assert-contains "~dep-leaf" refs)))
|
||||||
|
|
||||||
|
(deftest "finds multiple components"
|
||||||
|
(let ((refs (scan-components-from-source "(div (~dep-leaf) (~dep-branch))")))
|
||||||
|
(assert-contains "~dep-leaf" refs)
|
||||||
|
(assert-contains "~dep-branch" refs)))
|
||||||
|
|
||||||
|
(deftest "no false positives on plain text"
|
||||||
|
(let ((refs (scan-components-from-source "(div \"hello world\")")))
|
||||||
|
(assert-equal 0 (len refs))))
|
||||||
|
|
||||||
|
(deftest "handles hyphenated names"
|
||||||
|
(let ((refs (scan-components-from-source "(~my-component :key val)")))
|
||||||
|
(assert-contains "~my-component" refs))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 3. transitive-deps — transitive dependency closure
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "transitive-deps"
|
||||||
|
|
||||||
|
(deftest "leaf has no deps"
|
||||||
|
(let ((deps (transitive-deps "~dep-leaf" (test-env))))
|
||||||
|
(assert-equal 0 (len deps))))
|
||||||
|
|
||||||
|
(deftest "direct dependency"
|
||||||
|
(let ((deps (transitive-deps "~dep-branch" (test-env))))
|
||||||
|
(assert-contains "~dep-leaf" deps)))
|
||||||
|
|
||||||
|
(deftest "transitive closure"
|
||||||
|
(let ((deps (transitive-deps "~dep-trunk" (test-env))))
|
||||||
|
(assert-contains "~dep-branch" deps)
|
||||||
|
(assert-contains "~dep-leaf" deps)))
|
||||||
|
|
||||||
|
(deftest "excludes self"
|
||||||
|
(let ((deps (transitive-deps "~dep-trunk" (test-env))))
|
||||||
|
(assert-false (contains? deps "~dep-trunk"))))
|
||||||
|
|
||||||
|
(deftest "walks conditional branches"
|
||||||
|
(let ((deps (transitive-deps "~dep-conditional" (test-env))))
|
||||||
|
(assert-contains "~dep-leaf" deps)
|
||||||
|
(assert-contains "~dep-branch" deps)))
|
||||||
|
|
||||||
|
(deftest "walks all cond branches"
|
||||||
|
(let ((deps (transitive-deps "~dep-nested-cond" (test-env))))
|
||||||
|
(assert-contains "~dep-leaf" deps)
|
||||||
|
(assert-contains "~dep-branch" deps)
|
||||||
|
(assert-contains "~dep-trunk" deps)))
|
||||||
|
|
||||||
|
(deftest "island has no deps"
|
||||||
|
(let ((deps (transitive-deps "~dep-island" (test-env))))
|
||||||
|
(assert-equal 0 (len deps))))
|
||||||
|
|
||||||
|
(deftest "accepts name without tilde"
|
||||||
|
(let ((deps (transitive-deps "dep-branch" (test-env))))
|
||||||
|
(assert-contains "~dep-leaf" deps))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 4. components-needed — page bundle computation
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "components-needed"
|
||||||
|
|
||||||
|
(deftest "finds direct and transitive"
|
||||||
|
(let ((needed (components-needed "(~dep-trunk)" (test-env))))
|
||||||
|
(assert-contains "~dep-trunk" needed)
|
||||||
|
(assert-contains "~dep-branch" needed)
|
||||||
|
(assert-contains "~dep-leaf" needed)))
|
||||||
|
|
||||||
|
(deftest "deduplicates"
|
||||||
|
(let ((needed (components-needed "(div (~dep-leaf) (~dep-leaf))" (test-env))))
|
||||||
|
;; ~dep-leaf should appear only once
|
||||||
|
(assert-true (contains? needed "~dep-leaf"))))
|
||||||
|
|
||||||
|
(deftest "handles leaf page"
|
||||||
|
(let ((needed (components-needed "(~dep-island)" (test-env))))
|
||||||
|
(assert-contains "~dep-island" needed)
|
||||||
|
(assert-equal 1 (len needed))))
|
||||||
|
|
||||||
|
(deftest "handles multiple top-level components"
|
||||||
|
(let ((needed (components-needed "(div (~dep-leaf) (~dep-island))" (test-env))))
|
||||||
|
(assert-contains "~dep-leaf" needed)
|
||||||
|
(assert-contains "~dep-island" needed))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 5. IO detection — scan-io-refs, component-pure?
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
;; Define components that reference "io" functions for testing
|
||||||
|
(defcomp ~dep-pure ()
|
||||||
|
(div (~dep-leaf) "static"))
|
||||||
|
|
||||||
|
(defcomp ~dep-io ()
|
||||||
|
(div (fetch-data "/api")))
|
||||||
|
|
||||||
|
(defcomp ~dep-io-indirect ()
|
||||||
|
(div (~dep-io)))
|
||||||
|
|
||||||
|
(defsuite "scan-io-refs"
|
||||||
|
|
||||||
|
(deftest "no IO in pure AST"
|
||||||
|
(let ((refs (scan-io-refs (quote (div "hello" (span "world"))) (list "fetch-data"))))
|
||||||
|
(assert-equal 0 (len refs))))
|
||||||
|
|
||||||
|
(deftest "finds IO reference"
|
||||||
|
(let ((refs (scan-io-refs (quote (div (fetch-data "/api"))) (list "fetch-data"))))
|
||||||
|
(assert-contains "fetch-data" refs)))
|
||||||
|
|
||||||
|
(deftest "multiple IO refs"
|
||||||
|
(let ((refs (scan-io-refs (quote (div (fetch-data "/a") (query-db "x"))) (list "fetch-data" "query-db"))))
|
||||||
|
(assert-contains "fetch-data" refs)
|
||||||
|
(assert-contains "query-db" refs)))
|
||||||
|
|
||||||
|
(deftest "ignores non-IO symbols"
|
||||||
|
(let ((refs (scan-io-refs (quote (div (map str items))) (list "fetch-data"))))
|
||||||
|
(assert-equal 0 (len refs)))))
|
||||||
|
|
||||||
|
|
||||||
|
(defsuite "component-pure?"
|
||||||
|
|
||||||
|
(deftest "pure component is pure"
|
||||||
|
(assert-true (component-pure? "~dep-pure" (test-env) (list "fetch-data"))))
|
||||||
|
|
||||||
|
(deftest "IO component is not pure"
|
||||||
|
(assert-false (component-pure? "~dep-io" (test-env) (list "fetch-data"))))
|
||||||
|
|
||||||
|
(deftest "indirect IO is not pure"
|
||||||
|
(assert-false (component-pure? "~dep-io-indirect" (test-env) (list "fetch-data"))))
|
||||||
|
|
||||||
|
(deftest "leaf component is pure"
|
||||||
|
(assert-true (component-pure? "~dep-leaf" (test-env) (list "fetch-data")))))
|
||||||
212
shared/sx/ref/test-engine.sx
Normal file
212
shared/sx/ref/test-engine.sx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
;; ==========================================================================
|
||||||
|
;; test-engine.sx — Tests for SxEngine pure logic (engine.sx)
|
||||||
|
;;
|
||||||
|
;; Requires: test-framework.sx loaded first.
|
||||||
|
;; Platform functions: parse-time, parse-trigger-spec, default-trigger,
|
||||||
|
;; parse-swap-spec, parse-retry-spec, next-retry-ms, filter-params
|
||||||
|
;; (loaded from bootstrapped output by test runners)
|
||||||
|
;; ==========================================================================
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 1. parse-time — time string parsing
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "parse-time"
|
||||||
|
|
||||||
|
(deftest "seconds to ms"
|
||||||
|
(assert-equal 2000 (parse-time "2s")))
|
||||||
|
|
||||||
|
(deftest "milliseconds"
|
||||||
|
(assert-equal 500 (parse-time "500ms")))
|
||||||
|
|
||||||
|
(deftest "nil returns 0"
|
||||||
|
(assert-equal 0 (parse-time nil)))
|
||||||
|
|
||||||
|
(deftest "plain number string"
|
||||||
|
(assert-equal 100 (parse-time "100")))
|
||||||
|
|
||||||
|
(deftest "one second"
|
||||||
|
(assert-equal 1000 (parse-time "1s")))
|
||||||
|
|
||||||
|
(deftest "large seconds"
|
||||||
|
(assert-equal 30000 (parse-time "30s"))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 2. parse-trigger-spec — trigger attribute parsing
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "parse-trigger-spec"
|
||||||
|
|
||||||
|
(deftest "nil returns nil"
|
||||||
|
(assert-nil (parse-trigger-spec nil)))
|
||||||
|
|
||||||
|
(deftest "single event"
|
||||||
|
(let ((triggers (parse-trigger-spec "click")))
|
||||||
|
(assert-equal 1 (len triggers))
|
||||||
|
(assert-equal "click" (get (first triggers) "event"))))
|
||||||
|
|
||||||
|
(deftest "event with once modifier"
|
||||||
|
(let ((triggers (parse-trigger-spec "click once")))
|
||||||
|
(assert-equal 1 (len triggers))
|
||||||
|
(assert-equal "click" (get (first triggers) "event"))
|
||||||
|
(assert-true (get (get (first triggers) "modifiers") "once"))))
|
||||||
|
|
||||||
|
(deftest "event with delay modifier"
|
||||||
|
(let ((triggers (parse-trigger-spec "click delay:500ms")))
|
||||||
|
(assert-equal 1 (len triggers))
|
||||||
|
(assert-equal 500 (get (get (first triggers) "modifiers") "delay"))))
|
||||||
|
|
||||||
|
(deftest "multiple triggers comma-separated"
|
||||||
|
(let ((triggers (parse-trigger-spec "click,change")))
|
||||||
|
(assert-equal 2 (len triggers))
|
||||||
|
(assert-equal "click" (get (first triggers) "event"))
|
||||||
|
(assert-equal "change" (get (nth triggers 1) "event"))))
|
||||||
|
|
||||||
|
(deftest "polling trigger"
|
||||||
|
(let ((triggers (parse-trigger-spec "every 3s")))
|
||||||
|
(assert-equal 1 (len triggers))
|
||||||
|
(assert-equal "every" (get (first triggers) "event"))
|
||||||
|
(assert-equal 3000 (get (get (first triggers) "modifiers") "interval"))))
|
||||||
|
|
||||||
|
(deftest "event with from modifier"
|
||||||
|
(let ((triggers (parse-trigger-spec "click from:body")))
|
||||||
|
(assert-equal "body" (get (get (first triggers) "modifiers") "from"))))
|
||||||
|
|
||||||
|
(deftest "event with changed modifier"
|
||||||
|
(let ((triggers (parse-trigger-spec "keyup changed")))
|
||||||
|
(assert-equal "keyup" (get (first triggers) "event"))
|
||||||
|
(assert-true (get (get (first triggers) "modifiers") "changed")))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 3. default-trigger — default trigger by element tag
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "default-trigger"
|
||||||
|
|
||||||
|
(deftest "form submits"
|
||||||
|
(let ((triggers (default-trigger "FORM")))
|
||||||
|
(assert-equal "submit" (get (first triggers) "event"))))
|
||||||
|
|
||||||
|
(deftest "input changes"
|
||||||
|
(let ((triggers (default-trigger "INPUT")))
|
||||||
|
(assert-equal "change" (get (first triggers) "event"))))
|
||||||
|
|
||||||
|
(deftest "select changes"
|
||||||
|
(let ((triggers (default-trigger "SELECT")))
|
||||||
|
(assert-equal "change" (get (first triggers) "event"))))
|
||||||
|
|
||||||
|
(deftest "textarea changes"
|
||||||
|
(let ((triggers (default-trigger "TEXTAREA")))
|
||||||
|
(assert-equal "change" (get (first triggers) "event"))))
|
||||||
|
|
||||||
|
(deftest "div clicks"
|
||||||
|
(let ((triggers (default-trigger "DIV")))
|
||||||
|
(assert-equal "click" (get (first triggers) "event"))))
|
||||||
|
|
||||||
|
(deftest "button clicks"
|
||||||
|
(let ((triggers (default-trigger "BUTTON")))
|
||||||
|
(assert-equal "click" (get (first triggers) "event")))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 4. parse-swap-spec — swap specification parsing
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "parse-swap-spec"
|
||||||
|
|
||||||
|
(deftest "default swap"
|
||||||
|
(let ((spec (parse-swap-spec nil false)))
|
||||||
|
(assert-equal "outerHTML" (get spec "style"))
|
||||||
|
(assert-false (get spec "transition"))))
|
||||||
|
|
||||||
|
(deftest "innerHTML"
|
||||||
|
(let ((spec (parse-swap-spec "innerHTML" false)))
|
||||||
|
(assert-equal "innerHTML" (get spec "style"))))
|
||||||
|
|
||||||
|
(deftest "with transition true"
|
||||||
|
(let ((spec (parse-swap-spec "innerHTML transition:true" false)))
|
||||||
|
(assert-equal "innerHTML" (get spec "style"))
|
||||||
|
(assert-true (get spec "transition"))))
|
||||||
|
|
||||||
|
(deftest "transition false overrides global"
|
||||||
|
(let ((spec (parse-swap-spec "outerHTML transition:false" true)))
|
||||||
|
(assert-equal "outerHTML" (get spec "style"))
|
||||||
|
(assert-false (get spec "transition"))))
|
||||||
|
|
||||||
|
(deftest "global transition when not overridden"
|
||||||
|
(let ((spec (parse-swap-spec "innerHTML" true)))
|
||||||
|
(assert-equal "innerHTML" (get spec "style"))
|
||||||
|
(assert-true (get spec "transition")))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 5. parse-retry-spec — retry specification parsing
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "parse-retry-spec"
|
||||||
|
|
||||||
|
(deftest "nil returns nil"
|
||||||
|
(assert-nil (parse-retry-spec nil)))
|
||||||
|
|
||||||
|
(deftest "exponential backoff"
|
||||||
|
(let ((spec (parse-retry-spec "exponential:1000:30000")))
|
||||||
|
(assert-equal "exponential" (get spec "strategy"))
|
||||||
|
(assert-equal 1000 (get spec "start-ms"))
|
||||||
|
(assert-equal 30000 (get spec "cap-ms"))))
|
||||||
|
|
||||||
|
(deftest "linear strategy"
|
||||||
|
(let ((spec (parse-retry-spec "linear:2000:60000")))
|
||||||
|
(assert-equal "linear" (get spec "strategy"))
|
||||||
|
(assert-equal 2000 (get spec "start-ms"))
|
||||||
|
(assert-equal 60000 (get spec "cap-ms")))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 6. next-retry-ms — exponential backoff calculation
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "next-retry-ms"
|
||||||
|
|
||||||
|
(deftest "doubles current"
|
||||||
|
(assert-equal 2000 (next-retry-ms 1000 30000)))
|
||||||
|
|
||||||
|
(deftest "caps at maximum"
|
||||||
|
(assert-equal 30000 (next-retry-ms 20000 30000)))
|
||||||
|
|
||||||
|
(deftest "exact cap"
|
||||||
|
(assert-equal 30000 (next-retry-ms 15000 30000)))
|
||||||
|
|
||||||
|
(deftest "small initial"
|
||||||
|
(assert-equal 200 (next-retry-ms 100 30000))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; 7. filter-params — form parameter filtering
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "filter-params"
|
||||||
|
|
||||||
|
(deftest "nil passes all through"
|
||||||
|
(let ((params (list (list "a" "1") (list "b" "2"))))
|
||||||
|
(assert-equal 2 (len (filter-params nil params)))))
|
||||||
|
|
||||||
|
(deftest "none returns empty"
|
||||||
|
(let ((params (list (list "a" "1") (list "b" "2"))))
|
||||||
|
(assert-equal 0 (len (filter-params "none" params)))))
|
||||||
|
|
||||||
|
(deftest "star passes all"
|
||||||
|
(let ((params (list (list "a" "1") (list "b" "2"))))
|
||||||
|
(assert-equal 2 (len (filter-params "*" params)))))
|
||||||
|
|
||||||
|
(deftest "whitelist"
|
||||||
|
(let ((params (list (list "name" "Jo") (list "age" "30") (list "secret" "x"))))
|
||||||
|
(let ((filtered (filter-params "name,age" params)))
|
||||||
|
(assert-equal 2 (len filtered)))))
|
||||||
|
|
||||||
|
(deftest "blacklist with not"
|
||||||
|
(let ((params (list (list "name" "Jo") (list "csrf" "tok") (list "age" "30"))))
|
||||||
|
(let ((filtered (filter-params "not csrf" params)))
|
||||||
|
(assert-equal 2 (len filtered))))))
|
||||||
@@ -159,6 +159,8 @@ var SPECS = {
|
|||||||
"parser": { file: "test-parser.sx", needs: ["sx-parse"] },
|
"parser": { file: "test-parser.sx", needs: ["sx-parse"] },
|
||||||
"router": { file: "test-router.sx", needs: [] },
|
"router": { file: "test-router.sx", needs: [] },
|
||||||
"render": { file: "test-render.sx", needs: ["render-html"] },
|
"render": { file: "test-render.sx", needs: ["render-html"] },
|
||||||
|
"deps": { file: "test-deps.sx", needs: [] },
|
||||||
|
"engine": { file: "test-engine.sx", needs: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
function evalFile(filename) {
|
function evalFile(filename) {
|
||||||
@@ -215,9 +217,6 @@ if (args[0] === "--legacy") {
|
|||||||
|
|
||||||
// Load prerequisite spec modules
|
// Load prerequisite spec modules
|
||||||
if (specName === "router") {
|
if (specName === "router") {
|
||||||
// Use bootstrapped router functions from sx-browser.js.
|
|
||||||
// The bare evaluator can't run router.sx faithfully because set!
|
|
||||||
// inside lambda closures doesn't propagate (dict copies, not cells).
|
|
||||||
if (Sx.splitPathSegments) {
|
if (Sx.splitPathSegments) {
|
||||||
env["split-path-segments"] = Sx.splitPathSegments;
|
env["split-path-segments"] = Sx.splitPathSegments;
|
||||||
env["parse-route-pattern"] = Sx.parseRoutePattern;
|
env["parse-route-pattern"] = Sx.parseRoutePattern;
|
||||||
@@ -230,6 +229,35 @@ if (args[0] === "--legacy") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (specName === "deps") {
|
||||||
|
if (Sx.scanRefs) {
|
||||||
|
env["scan-refs"] = Sx.scanRefs;
|
||||||
|
env["scan-components-from-source"] = Sx.scanComponentsFromSource;
|
||||||
|
env["transitive-deps"] = Sx.transitiveDeps;
|
||||||
|
env["compute-all-deps"] = Sx.computeAllDeps;
|
||||||
|
env["components-needed"] = Sx.componentsNeeded;
|
||||||
|
env["page-component-bundle"] = Sx.pageComponentBundle;
|
||||||
|
env["page-css-classes"] = Sx.pageCssClasses;
|
||||||
|
env["scan-io-refs"] = Sx.scanIoRefs;
|
||||||
|
env["transitive-io-refs"] = Sx.transitiveIoRefs;
|
||||||
|
env["compute-all-io-refs"] = Sx.computeAllIoRefs;
|
||||||
|
env["component-pure?"] = Sx.componentPure_p;
|
||||||
|
env["test-env"] = function() { return env; };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (specName === "engine") {
|
||||||
|
if (Sx.parseTime) {
|
||||||
|
env["parse-time"] = Sx.parseTime;
|
||||||
|
env["parse-trigger-spec"] = Sx.parseTriggerSpec;
|
||||||
|
env["default-trigger"] = Sx.defaultTrigger;
|
||||||
|
env["parse-swap-spec"] = Sx.parseSwapSpec;
|
||||||
|
env["parse-retry-spec"] = Sx.parseRetrySpec;
|
||||||
|
env["next-retry-ms"] = function(cur, cap) { return Math.min(cur * 2, cap); };
|
||||||
|
env["filter-params"] = Sx.filterParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log("# --- " + specName + " ---");
|
console.log("# --- " + specName + " ---");
|
||||||
evalFile(spec.file);
|
evalFile(spec.file);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ SPECS = {
|
|||||||
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
||||||
"router": {"file": "test-router.sx", "needs": []},
|
"router": {"file": "test-router.sx", "needs": []},
|
||||||
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
||||||
|
"deps": {"file": "test-deps.sx", "needs": []},
|
||||||
|
"engine": {"file": "test-engine.sx", "needs": []},
|
||||||
}
|
}
|
||||||
|
|
||||||
REF_DIR = os.path.join(_HERE, "..", "ref")
|
REF_DIR = os.path.join(_HERE, "..", "ref")
|
||||||
@@ -269,6 +271,62 @@ def _load_router_from_bootstrap(env):
|
|||||||
eval_file("router.sx", env)
|
eval_file("router.sx", env)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_deps_from_bootstrap(env):
|
||||||
|
"""Load deps functions from the bootstrapped sx_ref.py."""
|
||||||
|
try:
|
||||||
|
from shared.sx.ref.sx_ref import (
|
||||||
|
scan_refs,
|
||||||
|
scan_components_from_source,
|
||||||
|
transitive_deps,
|
||||||
|
compute_all_deps,
|
||||||
|
components_needed,
|
||||||
|
page_component_bundle,
|
||||||
|
page_css_classes,
|
||||||
|
scan_io_refs,
|
||||||
|
transitive_io_refs,
|
||||||
|
compute_all_io_refs,
|
||||||
|
component_pure_p,
|
||||||
|
)
|
||||||
|
env["scan-refs"] = scan_refs
|
||||||
|
env["scan-components-from-source"] = scan_components_from_source
|
||||||
|
env["transitive-deps"] = transitive_deps
|
||||||
|
env["compute-all-deps"] = compute_all_deps
|
||||||
|
env["components-needed"] = components_needed
|
||||||
|
env["page-component-bundle"] = page_component_bundle
|
||||||
|
env["page-css-classes"] = page_css_classes
|
||||||
|
env["scan-io-refs"] = scan_io_refs
|
||||||
|
env["transitive-io-refs"] = transitive_io_refs
|
||||||
|
env["compute-all-io-refs"] = compute_all_io_refs
|
||||||
|
env["component-pure?"] = component_pure_p
|
||||||
|
env["test-env"] = lambda: env
|
||||||
|
except ImportError:
|
||||||
|
eval_file("deps.sx", env)
|
||||||
|
env["test-env"] = lambda: env
|
||||||
|
|
||||||
|
|
||||||
|
def _load_engine_from_bootstrap(env):
|
||||||
|
"""Load engine pure functions from the bootstrapped sx_ref.py."""
|
||||||
|
try:
|
||||||
|
from shared.sx.ref.sx_ref import (
|
||||||
|
parse_time,
|
||||||
|
parse_trigger_spec,
|
||||||
|
default_trigger,
|
||||||
|
parse_swap_spec,
|
||||||
|
parse_retry_spec,
|
||||||
|
next_retry_ms,
|
||||||
|
filter_params,
|
||||||
|
)
|
||||||
|
env["parse-time"] = parse_time
|
||||||
|
env["parse-trigger-spec"] = parse_trigger_spec
|
||||||
|
env["default-trigger"] = default_trigger
|
||||||
|
env["parse-swap-spec"] = parse_swap_spec
|
||||||
|
env["parse-retry-spec"] = parse_retry_spec
|
||||||
|
env["next-retry-ms"] = next_retry_ms
|
||||||
|
env["filter-params"] = filter_params
|
||||||
|
except ImportError:
|
||||||
|
eval_file("engine.sx", env)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global passed, failed, test_num
|
global passed, failed, test_num
|
||||||
|
|
||||||
@@ -306,6 +364,10 @@ def main():
|
|||||||
# Load prerequisite spec modules
|
# Load prerequisite spec modules
|
||||||
if spec_name == "router":
|
if spec_name == "router":
|
||||||
_load_router_from_bootstrap(env)
|
_load_router_from_bootstrap(env)
|
||||||
|
if spec_name == "deps":
|
||||||
|
_load_deps_from_bootstrap(env)
|
||||||
|
if spec_name == "engine":
|
||||||
|
_load_engine_from_bootstrap(env)
|
||||||
|
|
||||||
print(f"# --- {spec_name} ---")
|
print(f"# --- {spec_name} ---")
|
||||||
eval_file(spec["file"], env)
|
eval_file(spec["file"], env)
|
||||||
|
|||||||
@@ -115,6 +115,8 @@
|
|||||||
(dict :label "Parser" :href "/testing/parser")
|
(dict :label "Parser" :href "/testing/parser")
|
||||||
(dict :label "Router" :href "/testing/router")
|
(dict :label "Router" :href "/testing/router")
|
||||||
(dict :label "Renderer" :href "/testing/render")
|
(dict :label "Renderer" :href "/testing/render")
|
||||||
|
(dict :label "Dependencies" :href "/testing/deps")
|
||||||
|
(dict :label "Engine" :href "/testing/engine")
|
||||||
(dict :label "Runners" :href "/testing/runners")))
|
(dict :label "Runners" :href "/testing/runners")))
|
||||||
|
|
||||||
(define isomorphism-nav-items (list
|
(define isomorphism-nav-items (list
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
;; Overview page
|
;; Overview page
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
(defcomp ~testing-overview-content (&key server-results framework-source eval-source parser-source router-source render-source)
|
(defcomp ~testing-overview-content (&key server-results framework-source eval-source parser-source router-source render-source deps-source engine-source)
|
||||||
(~doc-page :title "Testing"
|
(~doc-page :title "Testing"
|
||||||
(div :class "space-y-8"
|
(div :class "space-y-8"
|
||||||
|
|
||||||
@@ -35,6 +35,8 @@
|
|||||||
+-- test-parser.sx 39 tests: tokenizer, parser, serializer
|
+-- test-parser.sx 39 tests: tokenizer, parser, serializer
|
||||||
+-- test-router.sx 18 tests: route matching + param extraction
|
+-- test-router.sx 18 tests: route matching + param extraction
|
||||||
+-- test-render.sx 23 tests: HTML rendering + components
|
+-- test-render.sx 23 tests: HTML rendering + components
|
||||||
|
+-- test-deps.sx 33 tests: dependency analysis + IO detection
|
||||||
|
+-- test-engine.sx 37 tests: trigger/swap/retry parsing
|
||||||
|
|
||||||
Runners:
|
Runners:
|
||||||
run.js Node.js — injects platform fns, runs specs
|
run.js Node.js — injects platform fns, runs specs
|
||||||
@@ -51,7 +53,9 @@ Platform functions (5 total):
|
|||||||
Per-spec platform functions:
|
Per-spec platform functions:
|
||||||
parser: sx-parse, sx-serialize, make-symbol, make-keyword, ...
|
parser: sx-parse, sx-serialize, make-symbol, make-keyword, ...
|
||||||
router: (none — pure spec, uses bootstrapped functions)
|
router: (none — pure spec, uses bootstrapped functions)
|
||||||
render: render-html (wraps parse + render-to-html)")))
|
render: render-html (wraps parse + render-to-html)
|
||||||
|
deps: test-env (returns current evaluation environment)
|
||||||
|
engine: (none — pure spec, uses bootstrapped functions)")))
|
||||||
|
|
||||||
;; Server results
|
;; Server results
|
||||||
(when server-results
|
(when server-results
|
||||||
@@ -86,6 +90,8 @@ Per-spec platform functions:
|
|||||||
(textarea :id "test-spec-parser" :style "display:none" parser-source)
|
(textarea :id "test-spec-parser" :style "display:none" parser-source)
|
||||||
(textarea :id "test-spec-router" :style "display:none" router-source)
|
(textarea :id "test-spec-router" :style "display:none" router-source)
|
||||||
(textarea :id "test-spec-render" :style "display:none" render-source)
|
(textarea :id "test-spec-render" :style "display:none" render-source)
|
||||||
|
(textarea :id "test-spec-deps" :style "display:none" deps-source)
|
||||||
|
(textarea :id "test-spec-engine" :style "display:none" engine-source)
|
||||||
(script :src (asset-url "/scripts/sx-test-runner.js")))
|
(script :src (asset-url "/scripts/sx-test-runner.js")))
|
||||||
|
|
||||||
;; Test spec index
|
;; Test spec index
|
||||||
@@ -107,7 +113,15 @@ Per-spec platform functions:
|
|||||||
(a :href "/testing/render" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
(a :href "/testing/render" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
||||||
(h3 :class "font-semibold text-stone-800" "Renderer")
|
(h3 :class "font-semibold text-stone-800" "Renderer")
|
||||||
(p :class "text-sm text-stone-500" "23 tests — elements, attributes, void elements, fragments, escaping, control flow, components")
|
(p :class "text-sm text-stone-500" "23 tests — elements, attributes, void elements, fragments, escaping, control flow, components")
|
||||||
(p :class "text-xs text-violet-600 mt-1" "test-render.sx"))))
|
(p :class "text-xs text-violet-600 mt-1" "test-render.sx"))
|
||||||
|
(a :href "/testing/deps" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
||||||
|
(h3 :class "font-semibold text-stone-800" "Dependencies")
|
||||||
|
(p :class "text-sm text-stone-500" "33 tests — scan-refs, transitive-deps, components-needed, IO detection, purity classification")
|
||||||
|
(p :class "text-xs text-violet-600 mt-1" "test-deps.sx"))
|
||||||
|
(a :href "/testing/engine" :class "block rounded-lg border border-stone-200 p-5 hover:border-violet-300 hover:bg-violet-50 transition-colors"
|
||||||
|
(h3 :class "font-semibold text-stone-800" "Engine")
|
||||||
|
(p :class "text-sm text-stone-500" "37 tests — parse-time, trigger specs, swap specs, retry logic, param filtering")
|
||||||
|
(p :class "text-xs text-violet-600 mt-1" "test-engine.sx"))))
|
||||||
|
|
||||||
;; What it proves
|
;; What it proves
|
||||||
(div :class "rounded-lg border border-blue-200 bg-blue-50 p-5 space-y-3"
|
(div :class "rounded-lg border border-blue-200 bg-blue-50 p-5 space-y-3"
|
||||||
|
|||||||
@@ -555,7 +555,9 @@
|
|||||||
:eval-source eval-source
|
:eval-source eval-source
|
||||||
:parser-source parser-source
|
:parser-source parser-source
|
||||||
:router-source router-source
|
:router-source router-source
|
||||||
:render-source render-source))
|
:render-source render-source
|
||||||
|
:deps-source deps-source
|
||||||
|
:engine-source engine-source))
|
||||||
|
|
||||||
(defpage testing-page
|
(defpage testing-page
|
||||||
:path "/testing/<slug>"
|
:path "/testing/<slug>"
|
||||||
@@ -572,6 +574,8 @@
|
|||||||
"parser" (run-modular-tests "parser")
|
"parser" (run-modular-tests "parser")
|
||||||
"router" (run-modular-tests "router")
|
"router" (run-modular-tests "router")
|
||||||
"render" (run-modular-tests "render")
|
"render" (run-modular-tests "render")
|
||||||
|
"deps" (run-modular-tests "deps")
|
||||||
|
"engine" (run-modular-tests "engine")
|
||||||
:else (dict))
|
:else (dict))
|
||||||
:content (case slug
|
:content (case slug
|
||||||
"eval" (~testing-spec-content
|
"eval" (~testing-spec-content
|
||||||
@@ -602,6 +606,20 @@
|
|||||||
:spec-source spec-source
|
:spec-source spec-source
|
||||||
:framework-source framework-source
|
:framework-source framework-source
|
||||||
:server-results server-results)
|
:server-results server-results)
|
||||||
|
"deps" (~testing-spec-content
|
||||||
|
:spec-name "deps"
|
||||||
|
:spec-title "Dependency Analysis Tests"
|
||||||
|
:spec-desc "33 tests covering component dependency analysis — scan-refs, scan-components-from-source, transitive-deps, components-needed, scan-io-refs, and component-pure? classification."
|
||||||
|
:spec-source spec-source
|
||||||
|
:framework-source framework-source
|
||||||
|
:server-results server-results)
|
||||||
|
"engine" (~testing-spec-content
|
||||||
|
:spec-name "engine"
|
||||||
|
:spec-title "Engine Tests"
|
||||||
|
:spec-desc "37 tests covering engine pure functions — parse-time, parse-trigger-spec, default-trigger, parse-swap-spec, parse-retry-spec, next-retry-ms, and filter-params."
|
||||||
|
:spec-source spec-source
|
||||||
|
:framework-source framework-source
|
||||||
|
:server-results server-results)
|
||||||
"runners" (~testing-runners-content)
|
"runners" (~testing-runners-content)
|
||||||
:else (~testing-overview-content
|
:else (~testing-overview-content
|
||||||
:server-results server-results)))
|
:server-results server-results)))
|
||||||
|
|||||||
@@ -707,6 +707,8 @@ def _run_modular_tests(spec_name: str) -> dict:
|
|||||||
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
"parser": {"file": "test-parser.sx", "needs": ["sx-parse"]},
|
||||||
"router": {"file": "test-router.sx", "needs": []},
|
"router": {"file": "test-router.sx", "needs": []},
|
||||||
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
"render": {"file": "test-render.sx", "needs": ["render-html"]},
|
||||||
|
"deps": {"file": "test-deps.sx", "needs": []},
|
||||||
|
"engine": {"file": "test-engine.sx", "needs": []},
|
||||||
}
|
}
|
||||||
|
|
||||||
specs_to_run = list(SPECS.keys()) if spec_name == "all" else [spec_name]
|
specs_to_run = list(SPECS.keys()) if spec_name == "all" else [spec_name]
|
||||||
@@ -721,7 +723,7 @@ def _run_modular_tests(spec_name: str) -> dict:
|
|||||||
if not spec:
|
if not spec:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Load router from bootstrap if needed
|
# Load module functions from bootstrap
|
||||||
if sn == "router":
|
if sn == "router":
|
||||||
try:
|
try:
|
||||||
from shared.sx.ref.sx_ref import (
|
from shared.sx.ref.sx_ref import (
|
||||||
@@ -740,6 +742,46 @@ def _run_modular_tests(spec_name: str) -> dict:
|
|||||||
env["make-route-segment"] = make_route_segment
|
env["make-route-segment"] = make_route_segment
|
||||||
except ImportError:
|
except ImportError:
|
||||||
eval_file("router.sx")
|
eval_file("router.sx")
|
||||||
|
elif sn == "deps":
|
||||||
|
try:
|
||||||
|
from shared.sx.ref.sx_ref import (
|
||||||
|
scan_refs, scan_components_from_source,
|
||||||
|
transitive_deps, compute_all_deps,
|
||||||
|
components_needed, page_component_bundle,
|
||||||
|
page_css_classes, scan_io_refs,
|
||||||
|
transitive_io_refs, compute_all_io_refs,
|
||||||
|
component_pure_p,
|
||||||
|
)
|
||||||
|
env["scan-refs"] = scan_refs
|
||||||
|
env["scan-components-from-source"] = scan_components_from_source
|
||||||
|
env["transitive-deps"] = transitive_deps
|
||||||
|
env["compute-all-deps"] = compute_all_deps
|
||||||
|
env["components-needed"] = components_needed
|
||||||
|
env["page-component-bundle"] = page_component_bundle
|
||||||
|
env["page-css-classes"] = page_css_classes
|
||||||
|
env["scan-io-refs"] = scan_io_refs
|
||||||
|
env["transitive-io-refs"] = transitive_io_refs
|
||||||
|
env["compute-all-io-refs"] = compute_all_io_refs
|
||||||
|
env["component-pure?"] = component_pure_p
|
||||||
|
env["test-env"] = lambda: env
|
||||||
|
except ImportError:
|
||||||
|
eval_file("deps.sx")
|
||||||
|
env["test-env"] = lambda: env
|
||||||
|
elif sn == "engine":
|
||||||
|
try:
|
||||||
|
from shared.sx.ref.sx_ref import (
|
||||||
|
parse_time, parse_trigger_spec, default_trigger,
|
||||||
|
parse_swap_spec, parse_retry_spec, filter_params,
|
||||||
|
)
|
||||||
|
env["parse-time"] = parse_time
|
||||||
|
env["parse-trigger-spec"] = parse_trigger_spec
|
||||||
|
env["default-trigger"] = default_trigger
|
||||||
|
env["parse-swap-spec"] = parse_swap_spec
|
||||||
|
env["parse-retry-spec"] = parse_retry_spec
|
||||||
|
env["next-retry-ms"] = lambda cur, cap: min(cur * 2, cap)
|
||||||
|
env["filter-params"] = filter_params
|
||||||
|
except ImportError:
|
||||||
|
eval_file("engine.sx")
|
||||||
|
|
||||||
eval_file(spec["file"])
|
eval_file(spec["file"])
|
||||||
|
|
||||||
@@ -763,6 +805,8 @@ def _run_modular_tests(spec_name: str) -> dict:
|
|||||||
result["parser-source"] = _read_spec_file("test-parser.sx")
|
result["parser-source"] = _read_spec_file("test-parser.sx")
|
||||||
result["router-source"] = _read_spec_file("test-router.sx")
|
result["router-source"] = _read_spec_file("test-router.sx")
|
||||||
result["render-source"] = _read_spec_file("test-render.sx")
|
result["render-source"] = _read_spec_file("test-render.sx")
|
||||||
|
result["deps-source"] = _read_spec_file("test-deps.sx")
|
||||||
|
result["engine-source"] = _read_spec_file("test-engine.sx")
|
||||||
else:
|
else:
|
||||||
spec = SPECS.get(spec_name)
|
spec = SPECS.get(spec_name)
|
||||||
if spec:
|
if spec:
|
||||||
|
|||||||
Reference in New Issue
Block a user