diff --git a/bun.lock b/bun.lock index 6c84c3be..090127a4 100644 --- a/bun.lock +++ b/bun.lock @@ -320,7 +320,7 @@ }, "packages/compiler": { "name": "@wrnexus/compiler", - "version": "0.8.22", + "version": "0.8.23", "dependencies": { "@wrnexus/csr": "workspace:*", "@wrnexus/store": "workspace:*", diff --git a/packages/compiler/package.json b/packages/compiler/package.json index ed58490d..521be118 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.8.22", + "version": "0.8.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/compiler/src/codegen.ts b/packages/compiler/src/codegen.ts index ad4a127a..28248ad9 100644 --- a/packages/compiler/src/codegen.ts +++ b/packages/compiler/src/codegen.ts @@ -344,10 +344,16 @@ function renderAttrs( exprRefsState(expression, reactive.runtimeStateNames) && dynamicExpressions ) { + const marker = exprRefsState(expression, reactive.stateNames) + ? ` data-wrn-bind-${bindIndex++}="${attrEscape(JSON.stringify([attr.name, attr.value]))}"` + : ""; + if (isHtmlBooleanAttribute(attr.name)) { + dynamicExpressions.push(`\${(${expression}) ? ${JSON.stringify(` ${attr.name}`)} : ""}`); + return `\x00WRNEACH${dynamicExpressions.length - 1}\x00${marker}`; + } 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)}"`; + return ` ${attr.name}="${sentinel}"${marker}`; } const initial = reactiveAttrValue(attr.value, reactive); if (initial === null) return base; @@ -1603,7 +1609,7 @@ function generateInner(ast: PageAst): string { ]; const runtimeStateNames = runtimeComputedNames(orderedStates, orderedComputed, runtimeRoots); const reactive: PageReactive | null = - reactiveNames.length > 0 + reactiveNames.length > 0 || runtimeStateNames.size > 0 ? { stateNames: new Set(reactiveNames), runtimeStateNames, scope: seedScope } : null; const loops: string[] = []; diff --git a/packages/compiler/test/server-load-attributes.test.ts b/packages/compiler/test/server-load-attributes.test.ts new file mode 100644 index 00000000..09830218 --- /dev/null +++ b/packages/compiler/test/server-load-attributes.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "bun:test"; +import { generate, parse } from "../src/index.ts"; + +test("server-only loader data renders native input attributes without client state", async () => { + const code = generate( + parse(`page Profile { + load server account { return {} } + view { + + +
+ } + }`), + ); + const javascript = new Bun.Transpiler({ loader: "ts" }).transformSync(code); + const page = await import( + `data:text/javascript;base64,${Buffer.from(javascript).toString("base64")}` + ); + const html = await page.default({ + account: { name: 'A ""', readonly: false, theme: "dark", identity: { name: "Alice" } }, + }); + expect(html).toContain('value="A "<User>""'); + expect(html).not.toContain(" readonly"); + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain('identity="{"name":"Alice"}"'); + expect(html).not.toContain("data-wrn-bind"); + expect(html).not.toContain("{account."); +});