fix: conditional classes, dynamic params, HMR and formatter
This commit is contained in:
@@ -521,8 +521,8 @@ export function generate(ast: PageAst): string {
|
||||
const needsClientRuntime = ast.states.length > 0 || hasClientBehavior(ast.view);
|
||||
|
||||
if (needsClientRuntime) {
|
||||
const scope = ast.states.map((s) => `${s.name}: ${s.expr}`).join(", ");
|
||||
html = `<div data-scope="${attrEscape(scope)}">${html}</div>`;
|
||||
const scopePlaceholder = "__WRNEXUS_DYNAMIC_SCOPE__";
|
||||
html = `<div data-scope="${scopePlaceholder}">${html}</div>`;
|
||||
}
|
||||
|
||||
if (styles.length > 0) {
|
||||
@@ -536,6 +536,12 @@ export function generate(ast: PageAst): string {
|
||||
// Escape the static HTML for the template literal, then swap loop sentinels for
|
||||
// their real `${…}` code (which must NOT be escaped).
|
||||
let body = templateEscape(html);
|
||||
const dynamicStateScope = ast.states
|
||||
.map(
|
||||
(state) =>
|
||||
`${JSON.stringify(state.name)}: (() => { try { return (${state.expr}); } catch { return undefined; } })()`,
|
||||
)
|
||||
.join(", ");
|
||||
loops.forEach((code, idx) => {
|
||||
body = body.replace(`\x00WRNEACH${idx}\x00`, () => code);
|
||||
});
|
||||
@@ -554,16 +560,68 @@ export function generate(ast: PageAst): string {
|
||||
}
|
||||
}
|
||||
|
||||
const needsSsrRuntime = ssrBindings.length > 0 || loops.length > 0;
|
||||
const needsSsrRuntime =
|
||||
ssrBindings.length > 0 ||
|
||||
loops.length > 0 ||
|
||||
ast.states.some((state) => /\bctx\b/.test(state.expr));
|
||||
if (needsSsrRuntime) {
|
||||
out.push(ssrRuntimeSource());
|
||||
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
||||
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
|
||||
out.push(
|
||||
`export default async function ${ast.name}(ctx: any) {\n${decls} const html = \`${body}\`;\n return await __wrnexusRenderSsrBindings(html, ctx);\n}`,
|
||||
`export default async function ${ast.name}(ctx: any) {
|
||||
${decls}
|
||||
const __state = { ${dynamicStateScope} };
|
||||
|
||||
const __scopeValue = Object.entries(__state)
|
||||
.map(([key, value]) => {
|
||||
const encoded =
|
||||
typeof value === "number" || typeof value === "boolean"
|
||||
? String(value)
|
||||
: JSON.stringify(value == null ? "" : String(value));
|
||||
|
||||
return key + ": " + encoded;
|
||||
})
|
||||
.join(", ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
const html = \`${body}\`.replace(
|
||||
"__WRNEXUS_DYNAMIC_SCOPE__",
|
||||
__scopeValue,
|
||||
);
|
||||
|
||||
return await __wrnexusRenderSsrBindings(html, ctx);
|
||||
}`,
|
||||
);
|
||||
} else {
|
||||
out.push(`export default function ${ast.name}() {\n return \`${body}\`;\n}`);
|
||||
out.push(
|
||||
`export default function ${ast.name}(ctx: any) {
|
||||
const __state = { ${dynamicStateScope} };
|
||||
|
||||
const __scopeValue = Object.entries(__state)
|
||||
.map(([key, value]) => {
|
||||
const encoded =
|
||||
typeof value === "number" || typeof value === "boolean"
|
||||
? String(value)
|
||||
: JSON.stringify(value == null ? "" : String(value));
|
||||
|
||||
return key + ": " + encoded;
|
||||
})
|
||||
.join(", ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
return \`${body}\`.replace(
|
||||
"__WRNEXUS_DYNAMIC_SCOPE__",
|
||||
__scopeValue,
|
||||
);
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
// --- API blocks -> method handlers ---
|
||||
@@ -773,25 +831,81 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
}
|
||||
|
||||
let bindIndex = 0;
|
||||
const staticClasses: string[] = [];
|
||||
const conditionalClasses: Array<{
|
||||
className: string;
|
||||
expression: string;
|
||||
}> = [];
|
||||
|
||||
for (const attr of node.attrs) {
|
||||
if (!attr.event && attr.name === "class") {
|
||||
staticClasses.push(attr.value);
|
||||
}
|
||||
|
||||
if (!attr.event && attr.name.startsWith("class:")) {
|
||||
conditionalClasses.push({
|
||||
className: attr.name.slice("class:".length),
|
||||
expression: attr.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const attrs = node.attrs
|
||||
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
||||
.map((a) => {
|
||||
if (a.event) return ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"`;
|
||||
if (a.boolean) return ` ${a.name}`;
|
||||
if (a.event) {
|
||||
return ` ${eventAttribute(a.name)}="${compileAttrValue(a.value, ctx)}"`;
|
||||
}
|
||||
|
||||
if (a.boolean) {
|
||||
return ` ${a.name}`;
|
||||
}
|
||||
|
||||
const rendered = ` ${a.name}="${compileAttrValue(a.value, ctx)}"`;
|
||||
if (!a.value.includes("{") || !exprRefsState(a.value, ctx.stateNames)) return rendered;
|
||||
|
||||
if (!a.value.includes("{") || !exprRefsState(a.value, ctx.stateNames)) {
|
||||
return rendered;
|
||||
}
|
||||
|
||||
const marker = attrEscape(JSON.stringify([a.name, a.value]));
|
||||
|
||||
return `${rendered} data-wrn-bind-${bindIndex++}="${escLit(marker)}"`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const initialConditionalClasses = conditionalClasses
|
||||
.map(({ className, expression }) => {
|
||||
return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
const classAttribute =
|
||||
staticClasses.length > 0 || conditionalClasses.length > 0
|
||||
? ` class="${compileAttrValue(staticClasses.join(" "), ctx)}${initialConditionalClasses}"`
|
||||
: "";
|
||||
|
||||
const classBindings = conditionalClasses
|
||||
.map(({ className, expression }, index) => {
|
||||
const marker = attrEscape(JSON.stringify([className, expression]));
|
||||
|
||||
return ` data-wrn-class-${index}="${escLit(marker)}"`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
// A `data-for` element introduces loop variables for its subtree.
|
||||
const loops = loopVarsOf(node);
|
||||
const childCtx =
|
||||
loops.length > 0 ? { ...ctx, loopVars: new Set([...(ctx.loopVars ?? []), ...loops]) } : ctx;
|
||||
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) return `<${node.tag}${attrs}>`;
|
||||
const inner = node.children.map((c) => renderComponentNode(c, childCtx)).join("");
|
||||
return `<${node.tag}${attrs}>${inner}</${node.tag}>`;
|
||||
const allAttrs = `${classAttribute}${classBindings}${attrs}`;
|
||||
|
||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||
return `<${node.tag}${allAttrs}>`;
|
||||
}
|
||||
|
||||
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join("");
|
||||
|
||||
return `<${node.tag}${allAttrs}>${inner}</${node.tag}>`;
|
||||
}
|
||||
|
||||
function generateComponent(ast: PageAst): string {
|
||||
|
||||
Reference in New Issue
Block a user