release: WRNexusJS 0.2.47

This commit is contained in:
2026-07-18 13:12:16 +05:30
parent 973d580bc5
commit d22824fa62
57 changed files with 420 additions and 129 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.2.46",
"version": "0.2.47",
"type": "module",
"main": "src/index.ts",
"exports": {
+199 -38
View File
@@ -9,6 +9,7 @@
* data-scope="count: 0, name: 'x'" declare reactive state on a subtree
* data-on-<event>="count++" run a statement in scope on an event
* data-text="expr" element textContent follows an expression
* data-wrn-loop-locals="base64-json" preserves SSR {#each} item/index values
* data-wrnexus-csr="id" target for generated CSR fetch bindings
* {{expr}} or {expr} interpolation inside text nodes
*
@@ -211,16 +212,96 @@ export const REACTIVE_RUNTIME = String.raw`
function signal(initial) {
var value = initial;
var subs = new Set();
var subscribers = new Set();
var notifying = false;
var pendingNotification = false;
var pendingPrevious;
function notify(previous) {
if (notifying) {
pendingNotification = true;
if (pendingPrevious === undefined) {
pendingPrevious = previous;
}
return;
}
notifying = true;
try {
/*
* Iterate a snapshot. Reactive renderers may subscribe again while
* running, but that must not mutate the collection currently being
* iterated.
*/
var snapshot =
Array.from(subscribers);
snapshot.forEach(function (
subscriber,
) {
if (
subscribers.has(
subscriber,
)
) {
subscriber(
value,
previous,
);
}
});
} finally {
notifying = false;
}
if (pendingNotification) {
var nextPrevious =
pendingPrevious;
pendingNotification = false;
pendingPrevious = undefined;
notify(nextPrevious);
}
}
return {
get: function () { return value; },
set: function (v) {
if (Object.is(v, value)) return;
var previous = value;
value = v;
subs.forEach(function (f) { f(value, previous); });
get: function () {
return value;
},
set: function (nextValue) {
if (
Object.is(
nextValue,
value,
)
) {
return;
}
var previous = value;
value = nextValue;
notify(previous);
},
subscribe: function (
subscriber,
) {
subscribers.add(
subscriber,
);
return function () {
subscribers.delete(
subscriber,
);
};
},
subscribe: function (f) { subs.add(f); return function () { subs.delete(f); }; }
};
}
function applyReactiveAttribute(node, name, value) {
@@ -385,21 +466,52 @@ export const REACTIVE_RUNTIME = String.raw`
// change then re-runs only the renderers that actually read it. The Set in
// signal.subscribe dedupes, so re-subscribing each run is cheap and bounded.
var currentRenderer = null;
var currentRenderer = null;
function reactive(fn) {
var running = false;
function run() {
var prev = currentRenderer;
if (running) {
return;
}
running = true;
var previousRenderer =
currentRenderer;
currentRenderer = run;
try { fn(); } finally { currentRenderer = prev; }
try {
fn();
} finally {
currentRenderer =
previousRenderer;
running = false;
}
}
renderers.push(run);
return run;
}
function renderAll() { renderers.forEach(function (f) { f(); }); }
function decodeLoopLocals(node) {
if (!node) {
return {};
}
var element =
node.nodeType === 1
? node
: node.parentElement;
var owner =
node &&
node.closest &&
node.closest(
element &&
element.closest &&
element.closest(
"[data-wrn-loop-locals]",
);
@@ -421,9 +533,7 @@ export const REACTIVE_RUNTIME = String.raw`
window.atob(encoded);
var bytes =
new Uint8Array(
binary.length,
);
new Uint8Array(binary.length);
for (
var index = 0;
@@ -434,16 +544,17 @@ export const REACTIVE_RUNTIME = String.raw`
binary.charCodeAt(index);
}
var value =
JSON.parse(
new TextDecoder(
"utf-8",
).decode(bytes),
var decoded =
new TextDecoder("utf-8").decode(
bytes,
);
return value &&
typeof value === "object"
? value
var parsed =
JSON.parse(decoded);
return parsed &&
typeof parsed === "object"
? parsed
: {};
} catch (error) {
console.error(
@@ -515,17 +626,43 @@ export const REACTIVE_RUNTIME = String.raw`
});
}
function writeScope(name, value) {
function writeScope(
name,
value,
) {
if (!signals[name]) {
signals[name] = signal(value);
signals[name] =
signal(value);
notifyState(
name,
value,
undefined,
);
renderAll();
notifyState(name, value, undefined);
return;
}
var previous = signals[name].get();
if (Object.is(previous, value)) return;
var previous =
signals[name].get();
if (
Object.is(
previous,
value,
)
) {
return;
}
signals[name].set(value);
notifyState(name, value, previous);
notifyState(
name,
value,
previous,
);
}
function evalExpr(expr, locals) {
@@ -1122,22 +1259,46 @@ export const REACTIVE_RUNTIME = String.raw`
});
});
// data-text bindings
// data-text bindings. Server-rendered {#each} nodes recover their item/index
// values from the nearest data-wrn-loop-locals marker.
el.querySelectorAll("[data-text]").forEach(function (node) {
if (!owns(node)) return;
var expr = node.getAttribute("data-text");
reactive(function () {
try { node.textContent = String(evalExpr(expr)); } catch (e) { /* ignore */ }
try {
var value = evalExpr(
expr,
decodeLoopLocals(node),
);
node.textContent =
value == null ? "" : String(value);
} catch (_) {
// Keep the server-rendered value when evaluation is unavailable.
}
});
});
// data-show="expr" — toggle visibility on truthiness.
el.querySelectorAll("[data-show]").forEach(function (node) {
if (!owns(node)) return;
var showExpr = node.getAttribute("data-show");
reactive(function () {
var visible = true;
try { visible = !!evalExpr(showExpr); } catch (e) { /* keep visible */ }
try {
visible = !!evalExpr(
showExpr,
decodeLoopLocals(node),
);
} catch (_) {
// Keep visible when evaluation is unavailable.
}
node.style.display = visible ? "" : "none";
});
});
@@ -1245,7 +1406,7 @@ export const REACTIVE_RUNTIME = String.raw`
try {
raw = evalExpr(
exact[1].trim(),
decodeLoopLocals(node),
locals,
);
} catch (error) {
return;
@@ -1259,10 +1420,10 @@ export const REACTIVE_RUNTIME = String.raw`
) {
try {
var value =
evalExpr(
expression.trim(),
decodeLoopLocals(node),
);
evalExpr(
expression.trim(),
locals,
);
return value == null
? ""