release: WRNexusJS 0.7.0

This commit is contained in:
2026-08-01 10:04:42 +05:30
parent c54144f2e4
commit 87507edf59
207 changed files with 12607 additions and 679 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.6.0",
"version": "0.7.0",
"type": "module",
"main": "src/index.ts",
"exports": {
+78
View File
@@ -0,0 +1,78 @@
import type { PageAst, ViewNode } from "@wrnexus/syntax";
export type RouteExecutionKind =
| "static"
| "static-interactive"
| "request-ssr"
| "authenticated-ssr"
| "streaming-ssr"
| "dynamic";
export interface RuntimeRequirements {
kind: RouteExecutionKind;
canPrerender: boolean;
needsClientRuntime: boolean;
needsServerRuntime: boolean;
hydrationStrategy: string | null;
reasons: string[];
}
function hasEvent(nodes: ViewNode[]): boolean {
for (const node of nodes) {
if (node.type === "element") {
if (node.attrs.some((attribute) => attribute.event)) return true;
if (hasEvent(node.children)) return true;
} else if (node.type === "each") {
if (hasEvent(node.body) || hasEvent(node.empty)) return true;
} else if (node.type === "if") {
if (node.branches.some((branch) => hasEvent(branch.body))) return true;
}
}
return false;
}
export function analyzeRuntimeRequirements(ast: PageAst): RuntimeRequirements {
const reasons: string[] = [];
const clientFunctions = ast.runtimeFunctions.some((fn) => fn.runtime !== "server");
const clientState = ast.states.some((state) => state.runtime !== "server");
const interactive =
clientFunctions ||
clientState ||
ast.effects.length > 0 ||
ast.watches.length > 0 ||
hasEvent(ast.view);
if (interactive) reasons.push("client interactivity");
const requestData =
ast.loads.length > 0 ||
ast.actions.length > 0 ||
ast.dataApis.length > 0 ||
ast.apis.length > 0 ||
ast.realtimes.length > 0 ||
ast.runtimeFunctions.some((fn) => fn.runtime === "server") ||
ast.states.some((state) => state.runtime === "server");
if (requestData) reasons.push("server/request data");
const authenticated = /^(?:required|true)$/i.test(ast.security.auth ?? "");
if (authenticated) reasons.push("authentication required");
const streaming = /^(?:true|required)$/i.test(ast.security.streaming ?? "");
if (streaming) reasons.push("streaming enabled");
let kind: RouteExecutionKind;
if (streaming) kind = "streaming-ssr";
else if (authenticated) kind = "authenticated-ssr";
else if (requestData && interactive) kind = "dynamic";
else if (requestData) kind = "request-ssr";
else if (interactive) kind = "static-interactive";
else kind = "static";
return {
kind,
canPrerender: kind === "static" || kind === "static-interactive",
needsClientRuntime: interactive && ast.hydrate !== "none" && ast.runtime !== "server",
needsServerRuntime: requestData || authenticated || streaming || ast.runtime === "server",
hydrationStrategy: interactive ? (ast.hydrate ?? "load") : null,
reasons,
};
}
+37 -3
View File
@@ -77,6 +77,38 @@ function isHtmlBooleanAttribute(name: string): boolean {
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
}
const URL_ATTRIBUTES = new Set([
"href",
"src",
"action",
"formaction",
"poster",
"cite",
"background",
"xlink:href",
]);
function stripAsciiControlAndSpace(value: string): string {
let result = "";
for (const character of value) {
if (character.charCodeAt(0) > 0x20) result += character;
}
return result;
}
function sanitizeUrlAttribute(value: string): string {
const compact = stripAsciiControlAndSpace(value.trim());
const lower = compact.toLowerCase();
if (/^(?:javascript|vbscript|file):/.test(lower)) return "about:blank";
if (/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(lower)) return "about:blank";
return value;
}
function safeAttributeValue(name: string, value: string): string {
if (!URL_ATTRIBUTES.has(name.toLowerCase()) || value.includes("{")) return value;
return sanitizeUrlAttribute(value);
}
/** Escape a value placed inside a double-quoted HTML attribute. */
function attrEscape(value: string): string {
return value
@@ -110,7 +142,9 @@ function renderAttr(attr: Attr): string {
case "csrText":
return "";
default:
return attr.boolean ? ` ${attr.name}` : ` ${attr.name}="${attrEscape(attr.value)}"`;
return attr.boolean
? ` ${attr.name}`
: ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
}
}
@@ -176,7 +210,7 @@ function renderAttrs(
const initial = reactiveAttrValue(attr.value, reactive);
if (initial === null) return base;
const marker = JSON.stringify([attr.name, attr.value]);
return ` ${attr.name}="${attrEscape(initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
return ` ${attr.name}="${attrEscape(URL_ATTRIBUTES.has(attr.name.toLowerCase()) ? sanitizeUrlAttribute(initial) : initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
})
.join("");
return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered;
@@ -2148,7 +2182,7 @@ function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): stri
const expression = wholeAttributeExpression(attr.value);
if (!expression) {
return ` ${attr.name}="${attrEscape(attr.value)}"`;
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
}
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
+2
View File
@@ -35,6 +35,8 @@ export { generateStoreBrowserModule, generateStoreModule } from "./store-codegen
export { createComponentContract } from "./component-contract.ts";
export { resolveWrnImport, resolveWrnImports } from "./import-resolver.ts";
export { createWrnSourceMap } from "./source-map.ts";
export { analyzeRuntimeRequirements } from "./analysis.ts";
export type { RouteExecutionKind, RuntimeRequirements } from "./analysis.ts";
export { generateNative, NativeCompileError } from "./native-codegen.ts";
export { Lexer, LexError } from "@wrnexus/syntax";
export { eraseFunctionTypes, inferredRuntimeType, runtimeTypeOf } from "@wrnexus/syntax";
+2 -2
View File
@@ -204,7 +204,7 @@ function __diagnostic(code, message, details) {
}
function __csrfToken() {
if (typeof document === "undefined") return undefined;
const match = /(?:^|;\\s*)wrnexus_csrf=([^;]+)/.exec(document.cookie || "");
const match = /(?:^|;\\s*)wire-csrf=([^;]+)/.exec(document.cookie || "");
return match ? decodeURIComponent(match[1]) : undefined;
}
async function __callServerFunction(storeName, functionName, args, options) {
@@ -215,7 +215,7 @@ async function __callServerFunction(storeName, functionName, args, options) {
method: "POST",
credentials: "same-origin",
signal: options.signal,
headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-wrnexus-csrf": csrf } : {}, options.headers || {}),
headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-csrf-token": csrf } : {}, options.headers || {}),
body: JSON.stringify({ component: storeName, function: functionName, args: args }),
});
const payload = await response.json().catch(function () { return null; });