ReviewOS

also looking at this

stacks/stx

fix(client): preserve client-signal {⁠{ }} + bind SVG classes via setAttribute

#1774
Closed glennmichael123 wants to merge fix/client-signal-interpolation-and-svg-bindclass into main
4 files +354 -293
9 days ago

Two client-side binding bugs surfaced while running a real signal-heavy app. Both manifested as content silently rendering as the literal text undefined or as a whole page failing to hydrate — with no build error.

1. Compound / template-literal over client-only signals baked to undefined

The SSG interpolator (expressions.ts) only preserved a for client hydration when the entire expression server-evaluated to exactly undefined. Client-only signals aren't in the server context, so:

  • Bare → evaluates to undefined → preserved → client binds. ✅
  • Compound → evaluates to a defined string "undefined people…" → baked into static HTML, braces stripped → client never re-binds → frozen at the undefined-substituted text. ❌
  • Template literal undefined/5 → same, and additionally its ${…} body was stripped by expressionUsesOnlyContextVars before the "references a client var?" check, so the signal was never even seen.

Fix:

  • Preserve any expression that references a non-context (client-only) identifier, regardless of its server-evaluated value (mirrors the existing catch-block behaviour).
  • Stop stripping ${…} expression bodies in expressionUsesOnlyContextVars (keep them, with a separator so ${a}${b} don't fuse into one bogus identifier), so template-literal signals are detected.
  • Companion (signals.ts): the client split regex used [^}]+, which stops at the first inner } — so a preserved template literal / object literal would be dropped on hydration. Made it brace-tolerant ([\s\S]+?).

Pure server-context expressions (3, Hello undefined) still render server-side — no over-preservation.

2. x-class / :class on an SVG aborts hydration

bindClass did el.className = …. On an SVGElement, className is a read-only SVGAnimatedString, so the assignment throws TypeError: Cannot set property className… — uncaught, inside the processElement loop — which aborts hydration for the rest of the page (following stay raw, x-cloak nodes never un-hide). The object-form classList branch was already SVG-safe; only the string/array assignment branches weren't.

Fix: route SVG through el.setAttribute('class', …).

Tests

Adds test/expressions/client-signal-preserve.test.ts (compound/ternary/template-literal preserve, no-over-preserve, SVG setAttribute guard). Full suite: 7816 pass, 0 fail.

4 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.