first commit
This commit is contained in:
@@ -0,0 +1,577 @@
|
||||
/**
|
||||
* Browser reactive runtime (Point 2: reactive directives).
|
||||
*
|
||||
* Served verbatim at `/__wrnexus/reactive.js` for any page that contains a
|
||||
* `data-scope`. It is plain browser JS (no build step) and self-contained: it
|
||||
* inlines a tiny `signal()` so it has no imports to resolve.
|
||||
*
|
||||
* Supported directives (this is exactly what the `.wrn` compiler emits):
|
||||
* 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-wrnexus-csr="id" target for generated CSR fetch bindings
|
||||
* {{expr}} or {expr} interpolation inside text nodes
|
||||
*
|
||||
* 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 () {
|
||||
function signal(initial) {
|
||||
var value = initial;
|
||||
var subs = new Set();
|
||||
return {
|
||||
get: function () { return value; },
|
||||
set: function (v) {
|
||||
if (Object.is(v, value)) return;
|
||||
value = v;
|
||||
subs.forEach(function (f) { f(value); });
|
||||
},
|
||||
subscribe: function (f) { subs.add(f); return function () { subs.delete(f); }; }
|
||||
};
|
||||
}
|
||||
|
||||
function setupScope(el) {
|
||||
if (el.__wrnexusScope) return; // idempotent: safe to call again after an HMR morph
|
||||
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);
|
||||
|
||||
var signals = {};
|
||||
Object.keys(initial).forEach(function (k) { signals[k] = signal(initial[k]); });
|
||||
|
||||
var renderers = [];
|
||||
// Dependency-tracked rendering: while a renderer runs, every signal it reads
|
||||
// subscribes THAT renderer (not a blanket "re-render everything"). A signal
|
||||
// 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;
|
||||
function reactive(fn) {
|
||||
function run() {
|
||||
var prev = currentRenderer;
|
||||
currentRenderer = run;
|
||||
try { fn(); } finally { currentRenderer = prev; }
|
||||
}
|
||||
renderers.push(run);
|
||||
return run;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
function evalExpr(expr) {
|
||||
return evaluateExpression(expr, readScope);
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// A binding belongs to THIS scope only when el is the node's nearest
|
||||
// [data-scope] ancestor. Otherwise a nested scope owns it and we skip it,
|
||||
// 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;
|
||||
}
|
||||
|
||||
// --- data-for list rendering -------------------------------------------
|
||||
// Each [data-for="item in list"] element is a per-item template. On any
|
||||
// change to the list (or a dependency an item reads), the list re-renders.
|
||||
function parseFor(value) {
|
||||
var m = /^\s*([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\s+in\s+([\s\S]+?)\s*$/.exec(
|
||||
value || "",
|
||||
);
|
||||
return m ? { item: m[1], index: m[2], list: m[3] } : null;
|
||||
}
|
||||
function fillMustache(str, itemEval) {
|
||||
return str.replace(/\{\{\s*([^}]+?)\s*\}\}|\{([^{}]+)\}/g, function (_, d, s) {
|
||||
var e = (d || s).trim();
|
||||
try { return String(itemEval(e)); } catch (err) { return ""; }
|
||||
});
|
||||
}
|
||||
function hydrateItem(root, locals) {
|
||||
function localRead(name) {
|
||||
return Object.prototype.hasOwnProperty.call(locals, name) ? locals[name] : peekScope(name);
|
||||
}
|
||||
function itemEval(expr) { return evaluateExpression(expr, localRead); }
|
||||
|
||||
var els = [root];
|
||||
if (root.querySelectorAll) Array.prototype.push.apply(els, root.querySelectorAll("*"));
|
||||
els.forEach(function (n) {
|
||||
if (n.nodeType !== 1) return;
|
||||
Array.prototype.slice.call(n.attributes).forEach(function (attr) {
|
||||
if (attr.name === "data-text") {
|
||||
try { n.textContent = String(itemEval(attr.value)); } catch (e) { /* ignore */ }
|
||||
} else if (attr.name.indexOf("data-on-") === 0) {
|
||||
var evt = attr.name.slice("data-on-".length);
|
||||
var stmt = attr.value;
|
||||
n.addEventListener(evt, function () {
|
||||
try {
|
||||
splitTopLevel(stmt, ";").forEach(function (part) {
|
||||
runStatement(part, itemEval, localRead, function (name, value) {
|
||||
if (Object.prototype.hasOwnProperty.call(locals, name)) locals[name] = value;
|
||||
else if (!signals[name]) { signals[name] = signal(value); renderAll(); }
|
||||
else signals[name].set(value);
|
||||
});
|
||||
});
|
||||
} catch (e) { console.error("[wrnexus] data-for handler error", e); }
|
||||
});
|
||||
} else if (attr.value.indexOf("{") !== -1) {
|
||||
attr.value = fillMustache(attr.value, itemEval);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
||||
var tn;
|
||||
while ((tn = walker.nextNode())) {
|
||||
if (tn.nodeValue.indexOf("{") === -1) continue;
|
||||
tn.nodeValue = fillMustache(tn.nodeValue, itemEval);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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]);
|
||||
}
|
||||
clones = [];
|
||||
var frag = document.createDocumentFragment();
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
var clone = tpl.cloneNode(true);
|
||||
var locals = {};
|
||||
locals[spec.item] = list[i];
|
||||
if (spec.index) locals[spec.index] = i;
|
||||
hydrateItem(clone, locals);
|
||||
frag.appendChild(clone);
|
||||
clones.push(clone);
|
||||
}
|
||||
parent.insertBefore(frag, marker.nextSibling);
|
||||
});
|
||||
});
|
||||
|
||||
// data-text bindings
|
||||
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 */ }
|
||||
});
|
||||
});
|
||||
|
||||
// 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 */ }
|
||||
node.style.display = visible ? "" : "none";
|
||||
});
|
||||
});
|
||||
|
||||
// {{expr}} / {expr} interpolation in text nodes
|
||||
var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
|
||||
var textNode;
|
||||
while ((textNode = walker.nextNode())) {
|
||||
var template = textNode.nodeValue;
|
||||
if (template.indexOf("{") === -1) continue;
|
||||
if (!owns(textNode)) continue;
|
||||
(function (node, tpl) {
|
||||
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 ""; }
|
||||
});
|
||||
});
|
||||
})(textNode, template);
|
||||
}
|
||||
|
||||
// data-on-<event> handlers, on the scope element and the descendants it owns.
|
||||
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 stmt = attr.value;
|
||||
node.addEventListener(evt, function () {
|
||||
try { runStmt(stmt); } catch (e) {
|
||||
console.error("[wrnexus] handler error in '" + stmt + "'", e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function hydrateScopes(root) {
|
||||
(root || document).querySelectorAll("[data-scope]").forEach(setupScope);
|
||||
}
|
||||
|
||||
function parseScopeDecl(decl) {
|
||||
var initial = {};
|
||||
splitTopLevel(decl, ",").forEach(function (part) {
|
||||
var idx = findTopLevel(part, ":");
|
||||
if (idx < 0) return;
|
||||
var name = part.slice(0, idx).trim();
|
||||
var expr = part.slice(idx + 1).trim();
|
||||
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) return;
|
||||
try {
|
||||
initial[name] = evaluateExpression(expr, function () { return undefined; });
|
||||
} catch (e) {
|
||||
console.error("[wrnexus] invalid data-scope value for '" + name + "'", e);
|
||||
}
|
||||
});
|
||||
return initial;
|
||||
}
|
||||
|
||||
function runStatement(stmt, evalExpr, read, write) {
|
||||
stmt = String(stmt || "").trim();
|
||||
if (!stmt) return;
|
||||
|
||||
var inc = stmt.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s*(\+\+|--)$/);
|
||||
if (inc) {
|
||||
write(inc[1], Number(read(inc[1]) || 0) + (inc[2] === "++" ? 1 : -1));
|
||||
return;
|
||||
}
|
||||
|
||||
var assign = stmt.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s*(\+=|-=|\*=|\/=|%=|=)\s*([\s\S]+)$/);
|
||||
if (!assign) {
|
||||
evalExpr(stmt); // bare expression statement (e.g. a method/function call)
|
||||
return;
|
||||
}
|
||||
|
||||
var name = assign[1];
|
||||
var op = assign[2];
|
||||
var next = evalExpr(assign[3]);
|
||||
var current = read(name);
|
||||
if (op === "+=") next = current + next;
|
||||
else if (op === "-=") next = Number(current || 0) - Number(next || 0);
|
||||
else if (op === "*=") next = Number(current || 0) * Number(next || 0);
|
||||
else if (op === "/=") next = Number(current || 0) / Number(next || 0);
|
||||
else if (op === "%=") next = Number(current || 0) % Number(next || 0);
|
||||
write(name, next);
|
||||
}
|
||||
|
||||
// A small, eval-free expression evaluator (so a strict CSP needs no
|
||||
// 'unsafe-eval'). Supports: literals, identifiers, member access (a.b, a[b]),
|
||||
// function/method calls, arrays, objects, arithmetic, comparison, equality,
|
||||
// logical (&& ||), unary (! - +), and the ternary operator.
|
||||
function evaluateExpression(expr, read) {
|
||||
var tokens = tokenizeExpression(String(expr || ""));
|
||||
var index = 0;
|
||||
function peek() { return tokens[index]; }
|
||||
function next() { return tokens[index++]; }
|
||||
function is(v) { return peek() && peek().value === v; }
|
||||
function match(v) { if (is(v)) { index++; return true; } return false; }
|
||||
function expect(v) { if (!match(v)) throw new Error("Expected '" + v + "'"); }
|
||||
|
||||
function parsePrimary() {
|
||||
var t = next();
|
||||
if (!t) throw new Error("Unexpected end of expression");
|
||||
if (t.type === "number" || t.type === "string") return { value: t.value };
|
||||
if (t.type === "ident") {
|
||||
if (t.value === "true") return { value: true };
|
||||
if (t.value === "false") return { value: false };
|
||||
if (t.value === "null") return { value: null };
|
||||
if (t.value === "undefined") return { value: undefined };
|
||||
return { value: read(t.value) };
|
||||
}
|
||||
if (t.value === "(") { var v = parseTernary(); expect(")"); return { value: v }; }
|
||||
if (t.value === "[") {
|
||||
var arr = [];
|
||||
if (!is("]")) { arr.push(parseTernary()); while (match(",")) arr.push(parseTernary()); }
|
||||
expect("]");
|
||||
return { value: arr };
|
||||
}
|
||||
if (t.value === "{") {
|
||||
var obj = {};
|
||||
if (!is("}")) {
|
||||
do {
|
||||
var kt = next();
|
||||
var key = kt.value;
|
||||
expect(":");
|
||||
obj[key] = parseTernary();
|
||||
} while (match(","));
|
||||
}
|
||||
expect("}");
|
||||
return { value: obj };
|
||||
}
|
||||
throw new Error("Unexpected token '" + t.value + "'");
|
||||
}
|
||||
|
||||
function parsePostfix() {
|
||||
var node = parsePrimary();
|
||||
for (;;) {
|
||||
if (match(".")) {
|
||||
var prop = next().value;
|
||||
node = { value: node.value == null ? undefined : node.value[prop], obj: node.value };
|
||||
} else if (match("[")) {
|
||||
var key = parseTernary();
|
||||
expect("]");
|
||||
node = { value: node.value == null ? undefined : node.value[key], obj: node.value };
|
||||
} else if (is("(")) {
|
||||
next();
|
||||
var args = [];
|
||||
if (!is(")")) { args.push(parseTernary()); while (match(",")) args.push(parseTernary()); }
|
||||
expect(")");
|
||||
var fn = node.value;
|
||||
node = { value: typeof fn === "function" ? fn.apply(node.obj, args) : undefined };
|
||||
} else break;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function parseUnary() {
|
||||
if (match("!")) return !parseUnary();
|
||||
if (match("-")) return -parseUnary();
|
||||
if (match("+")) return +parseUnary();
|
||||
return parsePostfix().value;
|
||||
}
|
||||
function parseMul() {
|
||||
var l = parseUnary();
|
||||
while (peek() && (is("*") || is("/") || is("%"))) {
|
||||
var op = next().value, r = parseUnary();
|
||||
l = op === "*" ? l * r : op === "/" ? l / r : l % r;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
function parseAdd() {
|
||||
var l = parseMul();
|
||||
while (peek() && (is("+") || is("-"))) {
|
||||
var op = next().value, r = parseMul();
|
||||
l = op === "+" ? l + r : l - r;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
function parseCmp() {
|
||||
var l = parseAdd();
|
||||
while (peek() && (is("<") || is(">") || is("<=") || is(">="))) {
|
||||
var op = next().value, r = parseAdd();
|
||||
l = op === "<" ? l < r : op === ">" ? l > r : op === "<=" ? l <= r : l >= r;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
function parseEq() {
|
||||
var l = parseCmp();
|
||||
while (peek() && (is("==") || is("!=") || is("===") || is("!=="))) {
|
||||
var op = next().value, r = parseCmp();
|
||||
l = op === "==" ? l == r : op === "!=" ? l != r : op === "===" ? l === r : l !== r;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
function parseAnd() {
|
||||
var l = parseEq();
|
||||
while (match("&&")) l = l && parseEq();
|
||||
return l;
|
||||
}
|
||||
function parseOr() {
|
||||
var l = parseAnd();
|
||||
while (match("||")) l = l || parseAnd();
|
||||
return l;
|
||||
}
|
||||
function parseTernary() {
|
||||
var c = parseOr();
|
||||
if (match("?")) { var a = parseTernary(); expect(":"); var b = parseTernary(); return c ? a : b; }
|
||||
return c;
|
||||
}
|
||||
|
||||
var value = parseTernary();
|
||||
if (index < tokens.length) throw new Error("Unexpected token '" + tokens[index].value + "'");
|
||||
return value;
|
||||
}
|
||||
|
||||
function tokenizeExpression(input) {
|
||||
var tokens = [];
|
||||
var i = 0;
|
||||
while (i < input.length) {
|
||||
var ch = input[i];
|
||||
if (/\s/.test(ch)) { i++; continue; }
|
||||
if (/[0-9]/.test(ch) || (ch === "." && /[0-9]/.test(input[i + 1]))) {
|
||||
var start = i++;
|
||||
while (i < input.length && /[0-9.]/.test(input[i])) i++;
|
||||
tokens.push({ type: "number", value: Number(input.slice(start, i)) });
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
var quote = ch, value = "";
|
||||
i++;
|
||||
while (i < input.length) {
|
||||
ch = input[i++];
|
||||
if (ch === quote) break;
|
||||
if (ch === "\\") {
|
||||
var esc = input[i++];
|
||||
value += esc === "n" ? "\n" : esc === "t" ? "\t" : esc || "";
|
||||
} else value += ch;
|
||||
}
|
||||
tokens.push({ type: "string", value: value });
|
||||
continue;
|
||||
}
|
||||
if (/[A-Za-z_$]/.test(ch)) {
|
||||
var s = i++;
|
||||
while (i < input.length && /[A-Za-z0-9_$]/.test(input[i])) i++;
|
||||
tokens.push({ type: "ident", value: input.slice(s, i) });
|
||||
continue;
|
||||
}
|
||||
var three = input.substr(i, 3);
|
||||
if (three === "===" || three === "!==") { tokens.push({ type: "op", value: three }); i += 3; continue; }
|
||||
var two = input.substr(i, 2);
|
||||
if (["==", "!=", "<=", ">=", "&&", "||"].indexOf(two) !== -1) {
|
||||
tokens.push({ type: "op", value: two });
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if ("()+-*/%!<>.,?:[]{}".indexOf(ch) !== -1) { tokens.push({ type: "op", value: ch }); i++; continue; }
|
||||
throw new Error("Unexpected character '" + ch + "'");
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function splitTopLevel(input, separator) {
|
||||
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 === "'") {
|
||||
quote = ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
||||
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
||||
else if (ch === separator && depth === 0) {
|
||||
parts.push(input.slice(start, i).trim());
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
parts.push(input.slice(start).trim());
|
||||
return parts.filter(Boolean);
|
||||
}
|
||||
|
||||
function findTopLevel(input, needle) {
|
||||
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 === "'") quote = ch;
|
||||
else if (ch === "(" || ch === "[" || ch === "{") depth++;
|
||||
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
||||
else if (ch === needle && depth === 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function safeCsrUrl(id) {
|
||||
try {
|
||||
if (!/^[A-Za-z0-9_-]+$/.test(id)) return null;
|
||||
return "/__wrnexus/csr?route=" + encodeURIComponent(location.pathname) + "&id=" + encodeURIComponent(id);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setupCsrFetch(el) {
|
||||
if (el.__wrnexusCsrFetch) return;
|
||||
el.__wrnexusCsrFetch = true;
|
||||
|
||||
var id = el.getAttribute("data-wrnexus-csr") || "";
|
||||
var url = safeCsrUrl(id);
|
||||
if (!url) {
|
||||
console.error("[wrnexus] blocked unsafe CSR binding '" + id + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
var headers = { "accept": "text/plain" };
|
||||
var storage = localStorageSnapshotHeader();
|
||||
if (storage) headers["x-wrnexus-local-storage"] = storage;
|
||||
|
||||
fetch(url, { headers: headers })
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||
return res.text();
|
||||
})
|
||||
.then(function (text) {
|
||||
el.textContent = text;
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error("[wrnexus] CSR fetch failed for binding '" + id + "'", err);
|
||||
el.textContent = "Failed to load";
|
||||
});
|
||||
}
|
||||
|
||||
function hydrateCsrFetches(root) {
|
||||
(root || document).querySelectorAll("[data-wrnexus-csr]").forEach(setupCsrFetch);
|
||||
}
|
||||
|
||||
function localStorageSnapshotHeader() {
|
||||
try {
|
||||
if (!("localStorage" in window)) return "";
|
||||
var values = {};
|
||||
for (var i = 0; i < window.localStorage.length; i++) {
|
||||
var key = window.localStorage.key(i);
|
||||
if (!key) continue;
|
||||
var value = window.localStorage.getItem(key);
|
||||
if (typeof value === "string") values[key] = value;
|
||||
}
|
||||
var encoded = encodeURIComponent(JSON.stringify(values));
|
||||
return encoded.length <= 12000 ? encoded : "";
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
window.__wrnexusHydrateScopes = hydrateScopes;
|
||||
window.__wrnexusHydrateCsrFetches = hydrateCsrFetches;
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
hydrateScopes(document);
|
||||
hydrateCsrFetches(document);
|
||||
});
|
||||
} else {
|
||||
hydrateScopes(document);
|
||||
hydrateCsrFetches(document);
|
||||
}
|
||||
})();
|
||||
`.trim();
|
||||
Reference in New Issue
Block a user