release: WRNexusJS 0.8.3
This commit is contained in:
@@ -363,6 +363,26 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function routeSnapshot() {
|
||||
var url = new URL(window.location.href);
|
||||
var params = window.__wrnexusRouteParams || {};
|
||||
var marker = document.querySelector("[data-wrn-route-params]");
|
||||
if (marker) {
|
||||
try { params = JSON.parse(marker.getAttribute("data-wrn-route-params") || "{}"); }
|
||||
catch (_) { params = {}; }
|
||||
window.__wrnexusRouteParams = params;
|
||||
marker.remove();
|
||||
}
|
||||
return {
|
||||
url: url.href,
|
||||
pathname: url.pathname,
|
||||
search: url.search,
|
||||
hash: url.hash,
|
||||
searchParams: url.searchParams,
|
||||
params: params,
|
||||
};
|
||||
}
|
||||
function sanitizeReactiveUrl(value) {
|
||||
var raw = String(value == null ? "" : value);
|
||||
var compact = raw.trim().replace(/[\u0000-\u0020]+/g, "").toLowerCase();
|
||||
@@ -755,6 +775,20 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var anyStateListeners = new Set();
|
||||
var cleanupCallbacks = [];
|
||||
var disposed = false;
|
||||
var routeValue = signal(routeSnapshot());
|
||||
|
||||
function updateRouteValue() {
|
||||
routeValue.set(routeSnapshot());
|
||||
}
|
||||
|
||||
window.addEventListener("wrnexus:navigated", updateRouteValue);
|
||||
window.addEventListener("popstate", updateRouteValue);
|
||||
window.addEventListener("hashchange", updateRouteValue);
|
||||
cleanupCallbacks.push(function () {
|
||||
window.removeEventListener("wrnexus:navigated", updateRouteValue);
|
||||
window.removeEventListener("popstate", updateRouteValue);
|
||||
window.removeEventListener("hashchange", updateRouteValue);
|
||||
});
|
||||
|
||||
function readGlobal(name) {
|
||||
if (declaredEvents.has(name)) {
|
||||
@@ -785,6 +819,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
if (name === "location") return window.location;
|
||||
if (name === "history") return window.history;
|
||||
if (name === "navigator") return window.navigator;
|
||||
if (name === "$route" || name === "route") {
|
||||
if (currentRenderer) routeValue.subscribe(currentRenderer);
|
||||
return routeValue.get();
|
||||
}
|
||||
if (name === "setTimeout") return window.setTimeout.bind(window);
|
||||
if (name === "clearTimeout") return window.clearTimeout.bind(window);
|
||||
if (name === "setInterval") return window.setInterval.bind(window);
|
||||
@@ -965,6 +1003,12 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
function installBehaviorFunctions(source) {
|
||||
extractBehaviorFunctions(source || "").forEach(function (definition) {
|
||||
// A generated browser module contains the real JavaScript implementation
|
||||
// of client/shared functions. Keep that implementation when it has already
|
||||
// been installed instead of replacing it with the small CSP-safe fallback
|
||||
// interpreter. The fallback intentionally supports only a bounded subset of
|
||||
// JavaScript and must never shadow the compiled function.
|
||||
if (typeof behaviorFunctions[definition.name] === "function") return;
|
||||
behaviorFunctions[
|
||||
definition.name
|
||||
] = function () {
|
||||
@@ -3665,15 +3709,8 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
return special.value;
|
||||
}
|
||||
|
||||
// Template literals are evaluated without eval so they remain compatible
|
||||
// with a strict CSP. Each interpolation uses the same bounded expression
|
||||
// evaluator as every other reactive binding.
|
||||
if (
|
||||
expr.length >= 2 &&
|
||||
expr[0] === "\`" &&
|
||||
expr[expr.length - 1] === "\`"
|
||||
) {
|
||||
var template = expr.slice(1, -1);
|
||||
function evaluateTemplateLiteral(source) {
|
||||
var template = source.slice(1, -1);
|
||||
var rendered = "";
|
||||
var cursor = 0;
|
||||
|
||||
@@ -3703,7 +3740,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
else if (character === "}" && --depth === 0) break;
|
||||
}
|
||||
|
||||
if (depth !== 0) throw new Error("Unclosed template interpolation in '" + expr + "'");
|
||||
if (depth !== 0) throw new Error("Unclosed template interpolation in '" + source + "'");
|
||||
rendered += String(evaluateExpression(template.slice(expressionStart, expressionEnd), read));
|
||||
cursor = expressionEnd + 1;
|
||||
continue;
|
||||
@@ -3715,6 +3752,18 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
return rendered;
|
||||
}
|
||||
|
||||
// Template literals are evaluated without eval so they remain compatible
|
||||
// with a strict CSP. Each interpolation uses the same bounded expression
|
||||
// evaluator as every other reactive binding. They are accepted both as a
|
||||
// complete expression and as a primary inside a larger expression.
|
||||
if (
|
||||
expr.length >= 2 &&
|
||||
expr[0] === "\`" &&
|
||||
expr[expr.length - 1] === "\`"
|
||||
) {
|
||||
return evaluateTemplateLiteral(expr);
|
||||
}
|
||||
|
||||
var tokens =
|
||||
tokenizeExpression(expr);
|
||||
|
||||
@@ -3731,6 +3780,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var t = next();
|
||||
if (!t) throw new Error("Unexpected end of expression in '" + expr + "'");
|
||||
if (t.type === "number" || t.type === "string") return { value: t.value };
|
||||
if (t.type === "template") return { value: evaluateTemplateLiteral(t.value) };
|
||||
if (t.type === "ident") {
|
||||
if (t.value === "true") return { value: true };
|
||||
if (t.value === "false") return { value: false };
|
||||
@@ -3874,6 +3924,37 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
tokens.push({ type: "string", value: value });
|
||||
continue;
|
||||
}
|
||||
if (ch === "\`") {
|
||||
var templateStart = i++;
|
||||
var templateDepth = 0;
|
||||
var templateQuote = "";
|
||||
while (i < input.length) {
|
||||
ch = input[i++];
|
||||
if (templateQuote) {
|
||||
if (ch === "\\") i++;
|
||||
else if (ch === templateQuote) templateQuote = "";
|
||||
continue;
|
||||
}
|
||||
if (ch === "\\") {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (ch === "\`" && templateDepth === 0) break;
|
||||
if (ch === "$" && input[i] === "{") {
|
||||
templateDepth++;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (templateDepth > 0) {
|
||||
if (ch === '"' || ch === "'") templateQuote = ch;
|
||||
else if (ch === "{") templateDepth++;
|
||||
else if (ch === "}") templateDepth--;
|
||||
}
|
||||
}
|
||||
if (input[i - 1] !== "\`") throw new Error("Unclosed template literal");
|
||||
tokens.push({ type: "template", value: input.slice(templateStart, i) });
|
||||
continue;
|
||||
}
|
||||
if (/[A-Za-z_$]/.test(ch)) {
|
||||
var s = i++;
|
||||
while (i < input.length && /[A-Za-z0-9_$]/.test(input[i])) i++;
|
||||
@@ -4047,6 +4128,23 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
Array.from(node.childNodes || []).forEach(visit);
|
||||
}
|
||||
visit(root);
|
||||
root.querySelectorAll("*").forEach(function (node) {
|
||||
Array.from(node.attributes || []).forEach(function (attribute) {
|
||||
if (attribute.name.indexOf("data-wrn-async-") === 0) return;
|
||||
if (attribute.value.indexOf("{") === -1) return;
|
||||
var nextValue = String(attribute.value).replace(/\{\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\}/g, function (whole, path) {
|
||||
var parts = path.split(".");
|
||||
if (parts[0] === prefix) parts.shift();
|
||||
if (prefix === "error" && parts[0] !== "message") return whole;
|
||||
var value = asyncValueAt(model, parts.join("."));
|
||||
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
||||
});
|
||||
if (/^(?:href|src|action|formaction)$/i.test(attribute.name)) {
|
||||
nextValue = sanitizeReactiveUrl(nextValue);
|
||||
}
|
||||
node.setAttribute(attribute.name, nextValue);
|
||||
});
|
||||
});
|
||||
root.querySelectorAll("[data-wrn-async-value]").forEach(function (node) {
|
||||
var path = node.getAttribute("data-wrn-async-value") || "";
|
||||
var value = asyncValueAt(model, path);
|
||||
@@ -4058,7 +4156,9 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var content = boundary.querySelector("[data-wrn-async-content]");
|
||||
if (!template || !content) return;
|
||||
var fragment = template.content.cloneNode(true);
|
||||
interpolateAsync(fragment, model, state === "error" ? "error" : boundary.getAttribute("data-wrn-async"));
|
||||
var alias = template.getAttribute("data-wrn-async-alias") ||
|
||||
(state === "error" ? "error" : boundary.getAttribute("data-wrn-async"));
|
||||
interpolateAsync(fragment, model, alias);
|
||||
content.replaceChildren(fragment);
|
||||
boundary.setAttribute("aria-busy", state === "loading" ? "true" : "false");
|
||||
boundary.setAttribute("data-wrn-async-state", state);
|
||||
@@ -4073,7 +4173,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
var name = boundary.getAttribute("data-wrn-async") || "";
|
||||
if (!/^[A-Za-z_$][\w$]{0,63}$/.test(name)) return showAsync(boundary, "error", { message: "Invalid data source" });
|
||||
var url = "/__wrnexus/client-load?route=" + encodeURIComponent(location.pathname) + "&name=" + encodeURIComponent(name);
|
||||
var url = "/__wrnexus/client-load?route=" + encodeURIComponent(location.pathname) + "&search=" + encodeURIComponent(location.search) + "&name=" + encodeURIComponent(name);
|
||||
var attempts = Math.min(5, Math.max(0, Number(boundary.getAttribute("data-wrn-async-retries")) || 0));
|
||||
var controller = new AbortController();
|
||||
boundary.__wrnexusAsyncAbort = controller;
|
||||
@@ -4102,6 +4202,44 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
(root || document).querySelectorAll("[data-wrn-async]").forEach(setupAsyncBoundary);
|
||||
}
|
||||
|
||||
var invalidationRefreshPending = false;
|
||||
function asyncBoundaryTags(boundary) {
|
||||
var raw = boundary.getAttribute("data-wrn-async-tags") || boundary.getAttribute("data-wrn-async") || "";
|
||||
return raw.split(",").map(function (tag) { return tag.trim(); }).filter(Boolean);
|
||||
}
|
||||
function refreshAsyncBoundary(boundary) {
|
||||
if (boundary.__wrnexusAsyncAbort) {
|
||||
try { boundary.__wrnexusAsyncAbort.abort(); } catch (_) {}
|
||||
}
|
||||
boundary.__wrnexusAsync = false;
|
||||
boundary.removeAttribute("data-wrn-async-resolved");
|
||||
setupAsyncBoundary(boundary);
|
||||
}
|
||||
function handleCacheInvalidation(event) {
|
||||
var detail = event && event.detail && typeof event.detail === "object" ? event.detail : {};
|
||||
var tags = Array.isArray(detail.tags) ? detail.tags.map(String) : [];
|
||||
if (!tags.length) return;
|
||||
var matchedServerBoundary = false;
|
||||
document.querySelectorAll("[data-wrn-async]").forEach(function (boundary) {
|
||||
var matches = asyncBoundaryTags(boundary).some(function (tag) { return tags.indexOf(tag) !== -1; });
|
||||
if (!matches) return;
|
||||
if (boundary.getAttribute("data-wrn-async-resolved") === "true") {
|
||||
matchedServerBoundary = true;
|
||||
return;
|
||||
}
|
||||
refreshAsyncBoundary(boundary);
|
||||
});
|
||||
if (matchedServerBoundary && !invalidationRefreshPending) {
|
||||
invalidationRefreshPending = true;
|
||||
Promise.resolve().then(function () {
|
||||
invalidationRefreshPending = false;
|
||||
if (typeof window.__wrnexusRefresh === "function") window.__wrnexusRefresh();
|
||||
else window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
window.addEventListener("wrnexus:cache:invalidate", handleCacheInvalidation);
|
||||
|
||||
function mountClientRoots(root) {
|
||||
(root || document).querySelectorAll("template[data-wrn-client-template]").forEach(function (template) {
|
||||
var id = template.getAttribute("data-wrn-client-template");
|
||||
|
||||
Reference in New Issue
Block a user