release: WRNexusJS 0.3.5

This commit is contained in:
2026-07-24 12:46:44 +05:30
parent 44ba847210
commit c81dedff17
2114 changed files with 65790 additions and 150559 deletions
+209 -18
View File
@@ -30,6 +30,36 @@ const types_ts_1 = require("./types.js");
function isComponentTag(tag) {
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
}
const HTML_BOOLEAN_ATTRIBUTES = new Set([
"allowfullscreen",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"hidden",
"inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
]);
function isHtmlBooleanAttribute(name) {
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
}
/** Escape a value placed inside a double-quoted HTML attribute. */
function attrEscape(value) {
return value
@@ -94,13 +124,22 @@ function reactiveAttrValue(raw, reactive) {
});
return found ? value : null;
}
function renderAttrs(attrs, csrId, reactive = null) {
function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) {
let bindIndex = 0;
const rendered = attrs
.map((attr) => {
const base = renderAttr(attr);
if (!reactive || attr.event || attr.boolean || !base || !attr.value.includes("{"))
return base;
const expression = wholeAttributeExpression(attr.value);
if (expression &&
exprRefsState(expression, reactive.runtimeStateNames) &&
dynamicExpressions) {
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
const sentinel = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
const marker = JSON.stringify([attr.name, attr.value]);
return ` ${attr.name}="${sentinel}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`;
}
const initial = reactiveAttrValue(attr.value, reactive);
if (initial === null)
return base;
@@ -332,7 +371,7 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
: undefined;
// Void elements (<br>, <img>, …) have no closing tag and no children.
if (parser_ts_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>`;
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>`;
}
const inner = apiBinding?.mode === "ssr"
? ssrMarker(ssrBindings, renderBinding(apiBinding))
@@ -346,7 +385,7 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
: node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
.join("");
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive, loops)}>${inner}</${node.tag}>`;
}
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
const attrs = node.attrs
@@ -363,6 +402,10 @@ function renderNestedComponentInvocation(node, ctx) {
const attrs = node.attrs
.filter((attr) => attr.name !== "data-component")
.map((attr) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
if (spread) {
return `\${__wireSpreadAttrs(${ctx.resolveExpr(spread[1])})}`;
}
if (attr.event) {
return (escLit(` ${eventAttribute(attr.name)}="`) + escLit(attrEscape(attr.value)) + escLit(`"`));
}
@@ -387,11 +430,14 @@ function renderNestedComponentInvocation(node, ctx) {
const childCtx = loops.length > 0
? {
...ctx,
forwardRestAttrs: false,
loopVars: new Set([...(ctx.loopVars ?? []), ...loops]),
}
: ctx;
: { ...ctx, forwardRestAttrs: false };
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
return `<div data-component="${attrEscape(node.tag)}"${attrs}>${inner}</div>`;
return (`<div data-component="${attrEscape(node.tag)}"` +
`${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
`${attrs}>${inner}</div>`);
}
function ssrMarker(bindings, binding) {
const marker = `<!--wrnexus-ssr:${bindings.length}-->`;
@@ -549,12 +595,23 @@ function hydrationId(ast) {
kind: ast.kind,
name: ast.name,
props: ast.props.map((entry) => entry.name),
events: ast.events.map((entry) => entry.name),
states: ast.states.map((entry) => entry.name),
computed: ast.computed.map((entry) => entry.name),
view: ast.view,
});
return `${ast.name}:${stableHash(shape)}`;
}
function isSafeGeneratedIdentifier(name) {
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name);
}
function generateSsrStateAliases(stateNames) {
const names = [...new Set(stateNames)].filter(isSafeGeneratedIdentifier);
if (!names.length) {
return "";
}
return `const { ${names.join(", ")} } = __state;\n`;
}
function hydrationAttribute(ast) {
const strategy = ast.hydrate ?? "load";
return ` data-wrn-hydration="${attrEscape(hydrationId(ast))}" data-wrn-hydrate="${attrEscape(strategy)}" data-wrn-runtime="${attrEscape(ast.runtime ?? "universal")}"`;
@@ -606,7 +663,10 @@ function generate(ast) {
...ast.states.map((entry) => entry.name),
...ast.computed.map((entry) => entry.name),
];
const reactive = reactiveNames.length > 0 ? { stateNames: new Set(reactiveNames), scope: seedScope } : null;
const runtimeStateNames = new Set(ast.states.filter((entry) => /\bctx\b/.test(entry.expr)).map((entry) => entry.name));
const reactive = reactiveNames.length > 0
? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope }
: null;
const loops = [];
let html = ast.view
.map((node) => renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive))
@@ -643,6 +703,7 @@ function generate(ast) {
.map((state) => `${JSON.stringify(state.name)}: ${state.valueType ?? "unknown"}`)
.join("; ")} }`
: "Record<string, never>";
const ssrStateAliases = generateSsrStateAliases(ast.states.map((state) => state.name));
loops.forEach((code, idx) => {
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
});
@@ -669,7 +730,7 @@ function generate(ast) {
out.push(`export default async function ${ast.name}(ctx: any) {
${decls}
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
const __scopeValue = Object.entries(__state)
.map(([key, value]) => {
const encoded =
@@ -696,7 +757,7 @@ function generate(ast) {
else {
out.push(`export default function ${ast.name}(ctx: any) {
const __state: ${stateType} = { ${dynamicStateScope} };
${ssrStateAliases}
const __scopeValue = Object.entries(__state)
.map(([key, value]) => {
const encoded =
@@ -901,6 +962,20 @@ function viewHasServerEach(nodes) {
return viewHasServerEach(node.children);
});
}
function viewHasRestAttributeSpread(nodes) {
return nodes.some((node) => {
if (node.type === "text")
return false;
if (node.type === "each") {
return viewHasRestAttributeSpread(node.body) || viewHasRestAttributeSpread(node.empty);
}
if (node.type === "if") {
return node.branches.some((branch) => viewHasRestAttributeSpread(branch.body));
}
return (node.attrs.some((attr) => /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.test(attr.name)) ||
viewHasRestAttributeSpread(node.children));
});
}
/**
* Compile a text node. Interpolations that reference state stay as client
* mustaches (`{expr}`, hydrated by the reactive runtime); interpolations of
@@ -1060,12 +1135,13 @@ function renderComponentNode(node, ctx) {
return renderNestedComponentInvocation(node, ctx);
}
const loopVariables = loopVarsOf(node);
const elementContext = loopVariables.length > 0
? {
...ctx,
loopVars: new Set([...(ctx.loopVars ?? []), ...loopVariables]),
}
: ctx;
const elementContext = {
...ctx,
forwardRestAttrs: false,
...(loopVariables.length > 0
? { loopVars: new Set([...(ctx.loopVars ?? []), ...loopVariables]) }
: {}),
};
let bindIndex = 0;
const staticClasses = [];
const conditionalClasses = [];
@@ -1083,12 +1159,36 @@ function renderComponentNode(node, ctx) {
const attrs = node.attrs
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
.map((a) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
if (spread) {
return `\${__wireSpreadAttrs(${elementContext.resolveExpr(spread[1])})}`;
}
if (a.event) {
return ` ${eventAttribute(a.name)}="${escLit(attrEscape(a.value))}"`;
}
if (a.boolean) {
return ` ${a.name}`;
}
if (isHtmlBooleanAttribute(a.name)) {
const expression = wholeAttributeExpression(a.value);
if (expression) {
const referencesState = exprRefsState(a.value, ctx.stateNames);
const referencesLoopVariable = elementContext.loopVars
? exprRefsState(a.value, elementContext.loopVars)
: false;
const referencesServerLocal = ctx.serverLocals
? exprRefsState(a.value, ctx.serverLocals)
: false;
const marker = referencesState || referencesLoopVariable || referencesServerLocal
? ` data-wrn-bind-${bindIndex++}="${escLit(attrEscape(JSON.stringify([a.name, a.value])))}"`
: "";
return `\${__wireBooleanAttr(${JSON.stringify(a.name)}, ${elementContext.resolveExpr(expression)})}${marker}`;
}
if (a.value === "false")
return "";
if (a.value === "true" || a.value === "")
return ` ${a.name}`;
}
const rendered = ` ${a.name}="${compileAttrValue(a.value, elementContext)}"`;
const referencesState = exprRefsState(a.value, ctx.stateNames);
const referencesLoopVariable = elementContext.loopVars
@@ -1142,6 +1242,10 @@ function renderComponentNode(node, ctx) {
.join("");
const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);
const allAttrs = `${loopLocalsAttribute}` +
`${ctx.forwardRestAttrs ? "${__wireSpreadAttrs(__attrs)}" : ""}` +
`${ctx.forwardRestAttrs && ctx.eventNames?.length
? ` data-wrn-events="${attrEscape(ctx.eventNames.join(","))}"`
: ""}` +
`${classAttribute}` +
`${classReactiveBinding}` +
`${classBindings}` +
@@ -1179,6 +1283,9 @@ function generateComponent(ast) {
for (const p of effectiveProps) {
nameRefs.set(p.name, safeRef(p.name));
}
if (!nameRefs.has("attrs")) {
nameRefs.set("attrs", "__attrs");
}
for (const s of ast.states)
nameRefs.set(s.name, safeRef(s.name));
for (const entry of ast.computed)
@@ -1191,12 +1298,25 @@ function generateComponent(ast) {
}
return result;
};
const ctx = { stateNames, resolveExpr };
const ctx = {
stateNames,
resolveExpr,
eventNames: ast.events.map((event) => event.name),
};
const serverFunctions = ast.functions
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
const viewCode = ast.view.map((node) => renderComponentNode(node, ctx)).join("");
const hasExplicitRestSpread = viewHasRestAttributeSpread(ast.view);
const rootElementIndex = ast.view.findIndex((node) => node.type === "element");
const automaticallyForwardRootAttrs = !hasExplicitRestSpread &&
!effectiveProps.some((prop) => prop.name === "attrs") &&
rootElementIndex >= 0;
const viewCode = ast.view
.map((node, index) => renderComponentNode(node, automaticallyForwardRootAttrs && index === rootElementIndex
? { ...ctx, forwardRestAttrs: true }
: ctx))
.join("");
const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
const styleTag = styles.length > 0
? escLit(`<style data-wrnexus-style="${attrEscape(ast.name)}">\n${styles.map(styleEscape).join("\n")}\n</style>`)
@@ -1222,6 +1342,9 @@ function generateComponent(ast) {
}
decls.push(` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify((0, types_ts_1.runtimeTypeOf)(prop.valueType))});`);
}
if (!effectiveProps.some((prop) => prop.name === "attrs")) {
decls.push(` const __attrs = __restProps(__p, new Set(${JSON.stringify(effectiveProps.map((prop) => prop.name))}));`);
}
for (const state of ast.states) {
decls.push(` let ${nameRefs.get(state.name)}${state.valueType ? `: ${state.valueType}` : ""} = (${resolveExpr(state.expr)});`);
}
@@ -1264,7 +1387,7 @@ function generateComponent(ast) {
if (typeSource)
out.push(typeSource);
if (effectiveProps.length > 0) {
out.push(`export interface ${ast.name}Props {\n${effectiveProps
out.push(`export interface ${ast.name}Props {\n [attribute: string]: unknown;\n${effectiveProps
.map((prop) => ` ${JSON.stringify(prop.name)}${prop.required ? "" : "?"}: ${prop.valueType ?? "unknown"};`)
.join("\n")}\n}`);
}
@@ -1339,6 +1462,15 @@ function generateComponent(ast) {
return declared === "unknown" && def === undefined ? v : String(v);
}
function __restProps(
props: Record<string, any>,
declared: Set<string>,
): Record<string, any> {
return Object.fromEntries(
Object.entries(props).filter(([name]) => !declared.has(name)),
);
}
function __wireHtml(v: any): string {
return String(v == null ? "" : v).replace(
/[&<>]/g,
@@ -1365,6 +1497,48 @@ function __wireAttr(v: any): string {
);
}
function __wireBooleanAttr(name: string, value: any): string {
return value === true ||
value === "true" ||
value === "" ||
value === 1 ||
value === "1" ||
value === name
? " " + name
: "";
}
function __wireSpreadAttrs(value: any): string {
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
const attributes: string[] = [];
for (const [name, raw] of Object.entries(value)) {
const lowerName = name.toLowerCase();
if (
!/^[A-Za-z_:][A-Za-z0-9_.:-]*$/.test(name) ||
lowerName.startsWith("on") ||
lowerName === "style" ||
lowerName === "slot" ||
lowerName === "data-component" ||
lowerName.startsWith("data-wrn")
) {
continue;
}
if (booleanAttributes.has(lowerName)) {
attributes.push(__wireBooleanAttr(name, raw));
continue;
}
if (raw === false || raw === null || raw === undefined) continue;
attributes.push(" " + name + '="' + __wireAttr(raw) + '"');
}
return attributes.join("");
}
function __wireProp(v: any): string {
const value =
v !== null && typeof v === "object"
@@ -2192,6 +2366,7 @@ function parse(source) {
let runtime;
let hydrate;
const props = [];
const events = [];
const types = [];
const states = [];
const computed = [];
@@ -2241,13 +2416,28 @@ function parse(source) {
break;
}
case "props": {
// props { name: Type = <default> } — omit the default for required props.
// props { name: Type = <default>; @event name = function }
lx.next();
expect("lbrace");
while (lx.peek().type !== "rbrace") {
const t = lx.peek();
if (t.type === "eof")
throw new ParseError("Unexpected end of input inside props");
if (t.type === "at") {
lx.next();
const declarationKind = expect("ident");
if (declarationKind.value !== "event") {
throw new ParseError(`Expected '@event' but got '@${declarationKind.value}' at offset ${declarationKind.pos}`);
}
const eventName = expect("ident").value;
expect("eq");
const marker = lx.readPropInitializer();
if (marker !== "function") {
throw new ParseError(`Event '${eventName}' must be declared as '@event ${eventName} = function'`);
}
events.push({ name: eventName });
continue;
}
if (t.type !== "ident") {
throw new ParseError(`Expected a prop name at offset ${t.pos}`);
}
@@ -2510,6 +2700,7 @@ function parse(source) {
runtime,
hydrate,
props,
events,
types,
states,
computed,