release: WRNexusJS 0.2.31

This commit is contained in:
2026-07-14 21:58:19 +05:30
parent 3cfdc747fa
commit 8b4b7ca5ac
58 changed files with 1152 additions and 196 deletions
+335 -25
View File
@@ -15,8 +15,109 @@
* Expressions are evaluated by a tiny parser instead of `eval`/`new Function`,
* so production can use a strong CSP without `unsafe-eval`.
*/
export const REACTIVE_RUNTIME = String.raw`
(function () {
var behaviorInstances = new WeakMap();
var mountedBehaviorRoots = new Set();
var pendingUpdateHooks = new Map();
var updateHooksScheduled = false;
var behaviorObserver;
function scheduleUpdateHook(element, callback) {
pendingUpdateHooks.set(element, callback);
if (updateHooksScheduled) return;
updateHooksScheduled = true;
queueMicrotask(function () {
updateHooksScheduled = false;
var pending = Array.from(pendingUpdateHooks.entries());
pendingUpdateHooks.clear();
pending.forEach(function (entry) {
var root = entry[0];
var hook = entry[1];
if (!root.isConnected) return;
try { hook(); } catch (error) {
console.error("[wrnexus] component update hook failed", error);
}
});
});
}
function parseBehavior(element) {
var raw = element.getAttribute("data-wrn-behavior");
if (!raw) return null;
try {
var parsed = JSON.parse(raw);
return parsed && typeof parsed === "object" ? parsed : null;
} catch (error) {
console.error("[wrnexus] failed to parse component behavior", error);
return null;
}
}
function extractBehaviorFunctions(source) {
var functions = [];
var re = /(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
var match;
while ((match = re.exec(source || ""))) {
var start = re.lastIndex;
var depth = 1;
var quote = "";
var i = start;
for (; i < source.length; i++) {
var ch = source[i];
if (quote) {
if (ch === "\\") i++;
else if (ch === quote) quote = "";
continue;
}
if (ch === '"' || ch === "'" || ch === "\`") {
quote = ch;
continue;
}
if (ch === "{") depth++;
else if (ch === "}" && --depth === 0) break;
}
if (depth !== 0) break;
functions.push({
name: match[1],
args: match[2].split(",").map(function (arg) { return arg.trim(); }).filter(Boolean),
body: source.slice(start, i),
});
re.lastIndex = i + 1;
}
return functions;
}
function splitStatements(input) {
var parts = [];
var start = 0;
var depth = 0;
var quote = "";
for (var i = 0; i < input.length; i++) {
var ch = input[i];
if (quote) {
if (ch === "\\") i++;
else if (ch === quote) quote = "";
continue;
}
if (ch === '"' || ch === "'" || ch === "\`") {
quote = ch;
continue;
}
if (ch === "(" || ch === "[" || ch === "{") depth++;
else if (ch === ")" || ch === "]" || ch === "}") depth--;
else if ((ch === ";" || ch === "\n" || ch === "\r") && depth === 0) {
var part = input.slice(start, i).trim();
if (part) parts.push(part);
start = i + 1;
}
}
var tail = input.slice(start).trim();
if (tail) parts.push(tail);
return parts;
}
function signal(initial) {
var value = initial;
var subs = new Set();
@@ -24,8 +125,9 @@ export const REACTIVE_RUNTIME = String.raw`
get: function () { return value; },
set: function (v) {
if (Object.is(v, value)) return;
var previous = value;
value = v;
subs.forEach(function (f) { f(value); });
subs.forEach(function (f) { f(value, previous); });
},
subscribe: function (f) { subs.add(f); return function () { subs.delete(f); }; }
};
@@ -58,29 +160,117 @@ export const REACTIVE_RUNTIME = String.raw`
}
function renderAll() { renderers.forEach(function (f) { f(); }); }
function readScope(name) {
var sig = signals[name];
if (!sig) return undefined;
if (currentRenderer) sig.subscribe(currentRenderer); // track dependency
return sig.get();
}
function peekScope(name) {
return signals[name] ? signals[name].get() : undefined;
var behaviorFunctions = {};
var stateWatchers = {};
var anyStateListeners = new Set();
var cleanupCallbacks = [];
var disposed = false;
function readGlobal(name) {
if (name === "window") return window;
if (name === "document") return document;
if (name === "console") return console;
if (name === "Math") return Math;
if (name === "JSON") return JSON;
if (name === "Date") return Date;
if (name === "URL") return URL;
if (name === "location") return window.location;
if (name === "history") return window.history;
if (name === "navigator") return window.navigator;
if (name === "setTimeout") return window.setTimeout.bind(window);
if (name === "clearTimeout") return window.clearTimeout.bind(window);
if (name === "requestAnimationFrame") return window.requestAnimationFrame.bind(window);
if (name === "cancelAnimationFrame") return window.cancelAnimationFrame.bind(window);
return undefined;
}
function evalExpr(expr) {
return evaluateExpression(expr, readScope);
function readScope(name) {
var sig = signals[name];
if (sig) {
if (currentRenderer) sig.subscribe(currentRenderer);
return sig.get();
}
if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) {
return behaviorFunctions[name];
}
return readGlobal(name);
}
function runStmt(stmt) {
splitTopLevel(stmt, ";").forEach(function (part) {
runStatement(part, function (e) { return evaluateExpression(e, peekScope); }, peekScope, function (name, value) {
if (!signals[name]) {
signals[name] = signal(value);
renderAll(); // new variable: re-run once so readers pick it up + re-track
} else {
signals[name].set(value);
function peekScope(name) {
if (signals[name]) return signals[name].get();
if (Object.prototype.hasOwnProperty.call(behaviorFunctions, name)) {
return behaviorFunctions[name];
}
return readGlobal(name);
}
function notifyState(name, value, previous) {
var watchers = stateWatchers[name];
if (watchers) {
watchers.slice().forEach(function (watcher) {
try { watcher(value, previous); } catch (error) {
console.error("[wrnexus] watcher for '" + name + "' failed", error);
}
});
}
anyStateListeners.forEach(function (listener) {
try { listener(name, value, previous); } catch (error) {
console.error("[wrnexus] state subscriber failed", error);
}
});
}
function writeScope(name, value) {
if (!signals[name]) {
signals[name] = signal(value);
renderAll();
notifyState(name, value, undefined);
return;
}
var previous = signals[name].get();
if (Object.is(previous, value)) return;
signals[name].set(value);
notifyState(name, value, previous);
}
function evalExpr(expr, locals) {
return evaluateExpression(expr, function (name) {
if (locals && Object.prototype.hasOwnProperty.call(locals, name)) {
return locals[name];
}
return readScope(name);
});
}
function runStmt(stmt, locals) {
splitStatements(stmt).forEach(function (part) {
runStatement(
part,
function (expression) { return evalExpr(expression, locals); },
function (name) {
if (locals && Object.prototype.hasOwnProperty.call(locals, name)) return locals[name];
return peekScope(name);
},
function (name, value) {
if (locals && Object.prototype.hasOwnProperty.call(locals, name)) {
locals[name] = value;
} else {
writeScope(name, value);
}
},
);
});
}
function installBehaviorFunctions(source) {
extractBehaviorFunctions(source || "").forEach(function (definition) {
behaviorFunctions[definition.name] = function () {
var locals = {};
for (var i = 0; i < definition.args.length; i++) {
locals[definition.args[i]] = arguments[i];
}
runStmt(definition.body, locals);
};
});
}
@@ -287,27 +477,146 @@ export const REACTIVE_RUNTIME = String.raw`
})(textNode, template);
}
// data-on-<event> handlers, on the scope element and the descendants it owns.
// Event handlers on elements, window, and document.
var nodes = [el].concat(Array.prototype.slice.call(el.querySelectorAll("*")));
nodes.forEach(function (node) {
if (!owns(node)) return;
Array.prototype.slice.call(node.attributes).forEach(function (attr) {
if (attr.name.indexOf("data-on-") !== 0) return;
var evt = attr.name.slice("data-on-".length);
var rawName = attr.name.slice("data-on-".length);
var target = node;
var evt = rawName;
if (rawName.indexOf("window-") === 0) {
target = window;
evt = rawName.slice("window-".length);
} else if (rawName.indexOf("document-") === 0) {
target = document;
evt = rawName.slice("document-".length);
}
var stmt = attr.value;
node.addEventListener(evt, function () {
try { runStmt(stmt); } catch (e) {
console.error("[wrnexus] handler error in '" + stmt + "'", e);
var listener = function (event) {
try { runStmt(stmt, { event: event, $event: event }); } catch (error) {
console.error("[wrnexus] handler error in '" + stmt + "'", error);
}
};
var options =
target === window && (evt === "scroll" || evt === "touchstart" || evt === "touchmove")
? { passive: true }
: undefined;
target.addEventListener(evt, listener, options);
cleanupCallbacks.push(function () {
target.removeEventListener(evt, listener, options);
});
});
});
var behavior = parseBehavior(el);
if (behavior) {
installBehaviorFunctions(behavior.functions);
(behavior.watches || []).forEach(function (watch) {
if (!watch || typeof watch.state !== "string" || typeof watch.body !== "string") return;
if (!stateWatchers[watch.state]) stateWatchers[watch.state] = [];
stateWatchers[watch.state].push(function (value, previous) {
runStmt(watch.body, { value: value, previous: previous });
});
});
var updateSource =
behavior.lifecycle && typeof behavior.lifecycle.update === "string"
? behavior.lifecycle.update.trim()
: "";
if (updateSource) {
anyStateListeners.add(function () {
scheduleUpdateHook(el, function () {
if (!disposed) runStmt(updateSource);
});
});
}
var mountSource =
behavior.lifecycle && typeof behavior.lifecycle.mount === "string"
? behavior.lifecycle.mount.trim()
: "";
var unmountSource =
behavior.lifecycle && typeof behavior.lifecycle.unmount === "string"
? behavior.lifecycle.unmount.trim()
: "";
var instance = {
dispose: function () {
if (disposed) return;
disposed = true;
cleanupCallbacks.splice(0).reverse().forEach(function (cleanup) {
try { cleanup(); } catch (_) { /* ignore cleanup errors */ }
});
pendingUpdateHooks.delete(el);
if (unmountSource) {
try { runStmt(unmountSource); } catch (error) {
console.error("[wrnexus] component unmount hook failed", error);
}
}
mountedBehaviorRoots.delete(el);
behaviorInstances.delete(el);
el.__wrnexusScope = false;
},
};
behaviorInstances.set(el, instance);
mountedBehaviorRoots.add(el);
if (mountSource) {
queueMicrotask(function () {
if (!disposed && el.isConnected) {
try { runStmt(mountSource); } catch (error) {
console.error("[wrnexus] component mount hook failed", error);
}
}
});
}
}
renderAll();
}
function disposeBehaviors(root) {
var elements = [];
if (root && root.nodeType === 1 && root.hasAttribute("data-wrn-behavior")) {
elements.push(root);
}
if (root && root.querySelectorAll) {
Array.prototype.push.apply(elements, root.querySelectorAll("[data-wrn-behavior]"));
}
elements.reverse().forEach(function (element) {
var instance = behaviorInstances.get(element);
if (instance) instance.dispose();
});
}
function ensureBehaviorObserver() {
if (behaviorObserver || typeof MutationObserver === "undefined") return;
behaviorObserver = new MutationObserver(function (records) {
records.forEach(function (record) {
Array.prototype.forEach.call(record.removedNodes, function (node) {
if (node && node.nodeType === 1) disposeBehaviors(node);
});
});
});
behaviorObserver.observe(document.documentElement, { childList: true, subtree: true });
}
function hydrateScopes(root) {
(root || document).querySelectorAll("[data-scope]").forEach(setupScope);
var host = root || document;
if (host.nodeType === 1 && host.matches && host.matches("[data-scope]")) setupScope(host);
if (host.querySelectorAll) host.querySelectorAll("[data-scope]").forEach(setupScope);
ensureBehaviorObserver();
}
function parseScopeDecl(decl) {
@@ -635,6 +944,7 @@ export const REACTIVE_RUNTIME = String.raw`
window.__wrnexusHydrateScopes = hydrateScopes;
window.__wrnexusHydrateCsrFetches = hydrateCsrFetches;
window.__wrnexusDisposeBehaviors = disposeBehaviors;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
hydrateScopes(document);