release: WRNexusJS 0.2.46

This commit is contained in:
2026-07-15 20:09:47 +05:30
parent 8a8328a308
commit 973d580bc5
58 changed files with 453 additions and 147 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.2.45",
"version": "0.2.46",
"type": "module",
"main": "src/index.ts",
"exports": {
+251 -47
View File
@@ -43,6 +43,34 @@ export const REACTIVE_RUNTIME = String.raw`
});
}
function decodeScopePayload(value) {
var binary = window.atob(value);
var bytes =
new Uint8Array(binary.length);
for (
var index = 0;
index < binary.length;
index++
) {
bytes[index] =
binary.charCodeAt(index);
}
var decoded =
new TextDecoder(
"utf-8",
).decode(bytes);
var parsed =
JSON.parse(decoded);
return parsed &&
typeof parsed === "object"
? parsed
: {};
}
function decodeBehaviorPayload(value) {
var binary = window.atob(value);
var bytes = new Uint8Array(binary.length);
@@ -310,11 +338,43 @@ export const REACTIVE_RUNTIME = String.raw`
}
function setupScope(el) {
if (el.__wrnexusScope) return; // idempotent: safe to call again after an HMR morph
if (el.__wrnexusScope) return;
el.__wrnexusScope = true;
el.__wrnexusHydrated = true; // marks the subtree as client-owned for the HMR morph
var decl = el.getAttribute("data-scope") || "";
var initial = parseScopeDecl(decl);
el.__wrnexusHydrated = true;
var encodedScope =
el.getAttribute(
"data-wrn-scope",
);
var initial;
if (encodedScope) {
try {
initial =
decodeScopePayload(
encodedScope,
);
} catch (error) {
console.error(
"[wrnexus] failed to decode scope payload",
error,
);
initial = {};
}
} else {
var declaration =
el.getAttribute(
"data-scope",
) || "";
initial =
parseScopeDecl(
declaration,
);
}
var signals = {};
Object.keys(initial).forEach(function (k) { signals[k] = signal(initial[k]); });
@@ -374,11 +434,17 @@ export const REACTIVE_RUNTIME = String.raw`
binary.charCodeAt(index);
}
return JSON.parse(
new TextDecoder(
"utf-8",
).decode(bytes),
);
var value =
JSON.parse(
new TextDecoder(
"utf-8",
).decode(bytes),
);
return value &&
typeof value === "object"
? value
: {};
} catch (error) {
console.error(
"[wrnexus] failed to decode loop locals",
@@ -572,7 +638,13 @@ export const REACTIVE_RUNTIME = String.raw`
// so an outer scope never clobbers an inner one's values.
function owns(node) {
var host = node.nodeType === 1 ? node : node.parentNode;
return !!host && host.closest && host.closest("[data-scope]") === el;
return (
!!host &&
host.closest &&
host.closest(
"[data-scope], [data-wrn-scope]",
) === el
);
}
// --- data-for list rendering -------------------------------------------
@@ -925,34 +997,128 @@ export const REACTIVE_RUNTIME = String.raw`
}
}
Array.prototype.slice.call(el.querySelectorAll("[data-for]")).forEach(function (tpl) {
if (!tpl.parentNode || !owns(tpl)) return;
var spec = parseFor(tpl.getAttribute("data-for"));
if (!spec) return;
tpl.removeAttribute("data-for");
var parent = tpl.parentNode;
var marker = document.createComment("wire-for");
parent.insertBefore(marker, tpl);
Array.prototype.slice
.call(
el.querySelectorAll(
"[data-for]",
),
)
.forEach(function (tpl) {
if (
!tpl.parentNode ||
!owns(tpl)
) {
return;
}
var spec =
parseFor(
tpl.getAttribute(
"data-for",
),
);
if (!spec) {
return;
}
var template =
tpl.cloneNode(true);
template.removeAttribute(
"data-for",
);
var parent =
tpl.parentNode;
var marker =
document.createComment(
"wrn-for",
);
parent.insertBefore(
marker,
tpl,
);
parent.removeChild(tpl);
var clones = [];
reactive(function () {
var list = evalExpr(spec.list);
if (!list || typeof list.length !== "number") list = [];
for (var c = 0; c < clones.length; c++) {
if (clones[c].parentNode) clones[c].parentNode.removeChild(clones[c]);
var list =
evalExpr(spec.list);
if (!Array.isArray(list)) {
console.error(
"[wrnexus] data-for expected an array for '" +
spec.list +
"', received",
list,
);
list = [];
}
for (
var cloneIndex = 0;
cloneIndex <
clones.length;
cloneIndex++
) {
var existingClone =
clones[cloneIndex];
if (
existingClone.parentNode
) {
existingClone.parentNode
.removeChild(
existingClone,
);
}
}
clones = [];
var frag = document.createDocumentFragment();
for (var i = 0; i < list.length; i++) {
var clone = tpl.cloneNode(true);
var fragment =
document.createDocumentFragment();
for (
var itemIndex = 0;
itemIndex < list.length;
itemIndex++
) {
var clone =
template.cloneNode(true);
var locals = {};
locals[spec.item] = list[i];
if (spec.index) locals[spec.index] = i;
hydrateItem(clone, locals);
frag.appendChild(clone);
locals[spec.item] =
list[itemIndex];
if (spec.index) {
locals[spec.index] =
itemIndex;
}
hydrateItem(
clone,
locals,
);
fragment.appendChild(
clone,
);
clones.push(clone);
}
parent.insertBefore(frag, marker.nextSibling);
parent.insertBefore(
fragment,
marker.nextSibling,
);
});
});
@@ -1009,11 +1175,14 @@ export const REACTIVE_RUNTIME = String.raw`
expression,
decodeLoopLocals(node),
);
} catch (e) {
} catch (error) {
enabled = false;
}
node.classList.toggle(className, enabled);
node.classList.toggle(
className,
enabled,
);
});
});
});
@@ -1076,7 +1245,7 @@ export const REACTIVE_RUNTIME = String.raw`
try {
raw = evalExpr(
exact[1].trim(),
locals,
decodeLoopLocals(node),
);
} catch (error) {
return;
@@ -1090,10 +1259,10 @@ export const REACTIVE_RUNTIME = String.raw`
) {
try {
var value =
evalExpr(
expression.trim(),
locals,
);
evalExpr(
expression.trim(),
decodeLoopLocals(node),
);
return value == null
? ""
@@ -1125,7 +1294,18 @@ export const REACTIVE_RUNTIME = String.raw`
reactive(function () {
node.nodeValue = tpl.replace(/\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function (_, doubleExpr, singleExpr) {
var expr = doubleExpr || singleExpr;
try { return String(evalExpr(expr.trim())); } catch (err) { return ""; }
try {
var value = evalExpr(
expr.trim(),
decodeLoopLocals(
node.parentNode,
),
);
return value == null
? ""
: String(value);
} catch (err) { return ""; }
});
});
})(textNode, template);
@@ -1152,16 +1332,24 @@ export const REACTIVE_RUNTIME = String.raw`
var stmt = attr.value;
var listener = function (event) {
try {
var locals =
decodeLoopLocals(node);
var locals =
decodeLoopLocals(node);
locals.event = event;
locals.$event = event;
locals.event = event;
locals.$event = event;
runStmt(stmt, locals);
try {
runStmt(
stmt,
locals,
);
} catch (error) {
console.error("[wrnexus] handler error in '" + stmt + "'", error);
console.error(
"[wrnexus] handler error in '" +
stmt +
"'",
error,
);
}
};
@@ -1276,8 +1464,24 @@ export const REACTIVE_RUNTIME = String.raw`
function hydrateScopes(root) {
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);
var scopeSelector =
"[data-scope], [data-wrn-scope]";
if (
host.nodeType === 1 &&
host.matches &&
host.matches(scopeSelector)
) {
setupScope(host);
}
if (host.querySelectorAll) {
host
.querySelectorAll(scopeSelector)
.forEach(setupScope);
}
ensureBehaviorObserver();
}
+46
View File
@@ -1,6 +1,7 @@
import { test, expect, beforeEach } from "bun:test";
import { Window } from "happy-dom";
import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts";
import { mountHtml } from "@wrnexus/test";
// Fresh DOM per test, with the runtime's globals bound.
function mount(html: string): Window {
@@ -164,3 +165,48 @@ test("independent signals in one scope update correctly (dependency tracking)",
click("bb");
expect([ta(), tb()]).toEqual(["2", "101"]);
});
test("data-for preserves arrays of objects from encoded component scope", () => {
const items = [
{
question: "One",
answer: "First",
},
{
question: "Two",
answer: "Second",
},
{
question: "Three",
answer: "Third",
},
{
question: "Four",
answer: "Fourth",
},
];
const encoded = Buffer.from(
JSON.stringify({
items,
}),
"utf8",
).toString("base64");
const dom = mountHtml(`
<div data-wrn-scope="${encoded}">
<article data-for="item, index in items">
<span>{index + 1}</span>
<strong>{item.question}</strong>
</article>
</div>
`);
expect(dom.querySelectorAll("article")).toHaveLength(4);
expect(dom.querySelectorAll("strong").map((element) => element.textContent)).toEqual([
"One",
"Two",
"Three",
"Four",
]);
});