feat: close application architecture gaps
This commit is contained in:
1 parent
d87b197224
commit
2e12656060
20 files changed
+548
-23
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.8.17",
|
||||
"version": "0.8.19",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -283,6 +283,32 @@ function componentEventAttribute(name: string): string {
|
||||
return `data-wrn-out-${name}`;
|
||||
}
|
||||
|
||||
/** Expand bind:value/bind:checked into a reactive prop plus assignment handler. */
|
||||
function expandBindings(attrs: Attr[]): Attr[] {
|
||||
const output: Attr[] = [];
|
||||
for (const attr of attrs) {
|
||||
if (attr.event || !attr.name.startsWith("bind:")) {
|
||||
output.push(attr);
|
||||
continue;
|
||||
}
|
||||
const target = attr.name.slice("bind:".length);
|
||||
if (target !== "value" && target !== "checked") {
|
||||
throw new Error(`Unknown .wrn binding '${attr.name}'. Use bind:value or bind:checked.`);
|
||||
}
|
||||
const expression = wholeAttributeExpression(attr.value) ?? attr.value.trim();
|
||||
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) {
|
||||
throw new Error(`${attr.name} requires a writable state name, received '${expression}'.`);
|
||||
}
|
||||
output.push({ name: target, value: `{${expression}}`, event: false });
|
||||
output.push({
|
||||
name: target === "checked" ? "change" : "input",
|
||||
value: `${expression} = payload.${target}`,
|
||||
event: true,
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function reactiveAttrValue(raw: string, reactive: PageReactive): string | null {
|
||||
let found = false;
|
||||
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
|
||||
@@ -305,6 +331,7 @@ function renderAttrs(
|
||||
reactive: PageReactive | null = null,
|
||||
dynamicExpressions?: string[],
|
||||
): string {
|
||||
attrs = expandBindings(attrs);
|
||||
let bindIndex = 0;
|
||||
const rendered = attrs
|
||||
.map((attr) => {
|
||||
@@ -463,7 +490,7 @@ function renderLoopBody(node: ViewNode, locals: string[] = []): string {
|
||||
|
||||
const componentTag = isComponentTag(node.tag);
|
||||
|
||||
const attrs = node.attrs
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => {
|
||||
const name = attr.event
|
||||
@@ -805,7 +832,7 @@ function renderPageComponentInvocation(
|
||||
const island = islandMarkerFor(node);
|
||||
if (island) return island;
|
||||
|
||||
const attrs = node.attrs
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => renderPageComponentAttr(attr, loops))
|
||||
.join("");
|
||||
@@ -827,7 +854,7 @@ function renderNestedComponentInvocation(
|
||||
|
||||
let bindIndex = 0;
|
||||
|
||||
const attrs = node.attrs
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => {
|
||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
||||
@@ -2251,7 +2278,7 @@ function renderClientControlTemplate(nodes: ViewNode[]): string {
|
||||
|
||||
const componentTag = isComponentTag(node.tag);
|
||||
let bindIndex = 0;
|
||||
const attrs = node.attrs
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.map((attribute) => {
|
||||
const name = attribute.event
|
||||
? componentTag
|
||||
@@ -2293,24 +2320,27 @@ function encodeClientControl(value: unknown): string {
|
||||
|
||||
function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
|
||||
let expression = "``";
|
||||
const clientBranches: string[] = [];
|
||||
|
||||
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 + "`";
|
||||
|
||||
clientBranches.unshift(
|
||||
`{ cond: ${branch.cond === null ? "null" : JSON.stringify(branch.cond)}, body: ${bodyExpression} }`,
|
||||
);
|
||||
|
||||
expression =
|
||||
branch.cond === null
|
||||
? bodyExpression
|
||||
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
||||
}
|
||||
|
||||
const definition = encodeClientControl(
|
||||
node.branches.map((branch) => ({
|
||||
cond: branch.cond,
|
||||
body: renderClientControlTemplate(branch.body),
|
||||
})),
|
||||
);
|
||||
// Render all branches on the server into an inert, encoded definition.
|
||||
// A branch first selected in the browser therefore contains the real nested
|
||||
// component HTML and scope metadata, not a data-component placeholder.
|
||||
const definition = `\${__wrnexusEncodeControl([${clientBranches.join(",")}])}`;
|
||||
|
||||
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
|
||||
}
|
||||
@@ -2489,7 +2519,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
||||
(attribute) => attribute.name === "data-component",
|
||||
);
|
||||
|
||||
const attrs = node.attrs
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
||||
.map((a) => {
|
||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
||||
@@ -3036,6 +3066,9 @@ function __wrnRaw(v: unknown): string {
|
||||
}
|
||||
|
||||
if (needsScope) {
|
||||
out.push(`function __wrnexusEncodeControl(value: unknown): string {
|
||||
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||
}`);
|
||||
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||
if (value === undefined) {
|
||||
return "undefined";
|
||||
|
||||
@@ -211,6 +211,10 @@ function __wrnRaw(v: unknown): string {
|
||||
return String(v == null ? "" : v);
|
||||
}
|
||||
|
||||
function __wrnexusEncodeControl(value: unknown): string {
|
||||
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||
}
|
||||
|
||||
function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||
if (value === undefined) {
|
||||
return "undefined";
|
||||
|
||||
@@ -1070,6 +1070,25 @@ component Banner {
|
||||
expect(output).toContain("visible");
|
||||
expect(output).toContain("Visible");
|
||||
expect(output).toContain("Hidden");
|
||||
expect(output).toContain("__wrnexusEncodeControl");
|
||||
});
|
||||
|
||||
test("bind directives compile to reactive values and assignment handlers", () => {
|
||||
const output = generate(
|
||||
parse(`page FormPage {
|
||||
state name = ""
|
||||
state active = false
|
||||
view {
|
||||
<Input bind:value={name} />
|
||||
<input bind:value={name} />
|
||||
<Switch bind:checked={active} />
|
||||
}
|
||||
}`),
|
||||
);
|
||||
expect(output).not.toContain("bind:value");
|
||||
expect(output).not.toContain("bind:checked");
|
||||
expect(output).toContain("name = payload.value");
|
||||
expect(output).toContain("active = payload.checked");
|
||||
});
|
||||
|
||||
test("if and each blocks emit browser control metadata while preserving SSR", () => {
|
||||
|
||||
Reference in new issue
Block a user