release: WRNexusJS 0.2.36

This commit is contained in:
2026-07-15 10:04:14 +05:30
parent afb14c2fbf
commit 17a535d4dc
65 changed files with 615 additions and 111 deletions
+78 -7
View File
@@ -15,6 +15,8 @@
* realtime {..} -> export const websocket = { evt(ws, ...args) { b } }
*/
import { Buffer } from "node:buffer";
import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts";
interface RenderBinding {
@@ -804,6 +806,8 @@ interface CompCtx {
/** `data-for` loop variables in scope — their mustaches stay literal for the
* client's list renderer (never baked server-side, since they have no value). */
loopVars?: Set<string>;
/** Local identifiers introduced by server-rendered `{#each}` blocks. */
serverLocals?: Set<string>;
}
interface ComponentBehavior {
@@ -940,9 +944,19 @@ function exprRefsState(expr: string, stateNames: Set<string>): boolean {
}
function viewHasEvents(nodes: ViewNode[]): boolean {
return nodes.some(
(n) => n.type === "element" && (n.attrs.some((a) => a.event) || viewHasEvents(n.children)),
);
return nodes.some((node) => {
if (node.type === "text") return false;
if (node.type === "each") {
return viewHasEvents(node.body) || viewHasEvents(node.empty);
}
if (node.type === "if") {
return node.branches.some((branch) => viewHasEvents(branch.body));
}
return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children);
});
}
/**
@@ -1004,14 +1018,65 @@ function compileAttrValue(raw: string, ctx: CompCtx): string {
return out + escLit(attrEscape(raw.slice(last)));
}
function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
let expression = "``";
for (let index = node.branches.length - 1; index >= 0; index--) {
const branch = node.branches[index]!;
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
const bodyExpression = "`" + body + "`";
expression =
branch.cond === null
? bodyExpression
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
}
return "${" + expression + "}";
}
function renderComponentEachNode(node: EachNode, ctx: CompCtx): string {
const item = node.item;
const index = node.index ?? "__wi";
const list = ctx.resolveExpr(node.list);
const childCtx: CompCtx = {
...ctx,
serverLocals: new Set([...(ctx.serverLocals ?? []), item, index]),
};
const body = node.body.map((child) => renderComponentNode(child, childCtx)).join("");
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
return (
"${(() => { const __wl = Array.isArray(" +
list +
") ? (" +
list +
") : []; return __wl.length ? __wl.map((" +
item +
", " +
index +
") => `" +
body +
'`).join("") : `' +
empty +
"`; })()}"
);
}
/** Render a component view node into template-literal-ready source. */
function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
if (node.type === "text") return compileText(node.value, ctx);
if (node.type === "each" || node.type === "if") {
throw new Error(
"Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client).",
);
if (node.type === "each") {
return renderComponentEachNode(node, ctx);
}
if (node.type === "if") {
return renderComponentIfNode(node, ctx);
}
if (isComponentTag(node.tag)) {
return renderNestedComponentInvocation(node, ctx);
}
@@ -1080,6 +1145,9 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
: "";
const classBindings = conditionalClasses
.filter(({ expression }) => {
return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals);
})
.map(({ className, expression }, index) => {
const marker = attrEscape(JSON.stringify([className, expression]));
@@ -1200,6 +1268,9 @@ function __wireAttr(v: any): string {
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;",
);
}
function __wireRaw(v: any): string {
return String(v == null ? "" : v);
}`);
if (needsScope) {