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 toundefined→ 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 byexpressionUsesOnlyContextVarsbefore 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 inexpressionUsesOnlyContextVars(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 clientsplit 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.