fix(compiler): preserve server-only component props during hydration

This commit is contained in:
Clintchiz committed 2026-08-26 12:47:21 +05:30
1 parent bfa72d5679
commit 26af30cfe6
4 files changed
+14 -4

No files matched your search

+1 -1
View File
@@ -320,7 +320,7 @@
}, },
"packages/compiler": { "packages/compiler": {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.8.24", "version": "0.8.25",
"dependencies": { "dependencies": {
"@wrnexus/csr": "workspace:*", "@wrnexus/csr": "workspace:*",
"@wrnexus/store": "workspace:*", "@wrnexus/store": "workspace:*",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.8.24", "version": "0.8.25",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+8 -1
View File
@@ -3196,7 +3196,7 @@ function __wrnProp(v: unknown): string {
function renderPageComponentAttr( function renderPageComponentAttr(
attr: Attr, attr: Attr,
dynamicExpressions: string[], dynamicExpressions: string[],
_reactive: PageReactive | null, reactive: PageReactive | null,
): string { ): string {
if (attr.event) { if (attr.event) {
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`; return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
@@ -3215,6 +3215,13 @@ function renderPageComponentAttr(
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`); dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`; const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
if (
reactive &&
exprRefsState(expression, reactive.runtimeStateNames) &&
!exprRefsState(expression, reactive.stateNames)
) {
return ` ${attr.name}="${marker}"`;
}
// The mount disappears during SSR. Preserve every dynamic component prop // The mount disappears during SSR. Preserve every dynamic component prop
// as a parent-owned binding; server-only expressions simply fail closed in // as a parent-owned binding; server-only expressions simply fail closed in
// the browser, while page state can continue driving the mounted child. // the browser, while page state can continue driving the mounted child.
@@ -3,10 +3,12 @@ import { generate, parse } from "../src/index.ts";
test("server-only loader data renders native input attributes without client state", async () => { test("server-only loader data renders native input attributes without client state", async () => {
const code = generate( const code = generate(
parse(`page Profile { parse(`import UserSelect from "./UserSelect.wrn"
page Profile {
load server account { return {} } load server account { return {} }
view { view {
<h1>{account.name}</h1> <h1>{account.name}</h1>
<UserSelect value="{account.name}" options="{account.items}" />
{#if account.readonly}<p>Read only</p>{:else}<p>Editable</p>{/if} {#if account.readonly}<p>Read only</p>{:else}<p>Editable</p>{/if}
{#each account.items as item}<strong>{item}</strong>{/each} {#each account.items as item}<strong>{item}</strong>{/each}
<input value="{account.name}" readonly="{account.readonly}" /> <input value="{account.name}" readonly="{account.readonly}" />
@@ -34,6 +36,7 @@ test("server-only loader data renders native input attributes without client sta
expect(html).toContain('<option value="dark" selected>Dark</option>'); expect(html).toContain('<option value="dark" selected>Dark</option>');
expect(html).toContain('identity="{&quot;name&quot;:&quot;Alice&quot;}"'); expect(html).toContain('identity="{&quot;name&quot;:&quot;Alice&quot;}"');
expect(html).not.toContain("data-wrn-bind"); expect(html).not.toContain("data-wrn-bind");
expect(html).not.toContain("data-wrn-prop-bind");
expect(html).not.toContain("{account."); expect(html).not.toContain("{account.");
expect(html).toContain("<h1>A &quot;&lt;User&gt;&quot;</h1>"); expect(html).toContain("<h1>A &quot;&lt;User&gt;&quot;</h1>");
expect(html).toContain("<p>Editable</p>"); expect(html).toContain("<p>Editable</p>");