fix: hydrate attribute-bound each values and guard style directives

Addresses two cursor review findings:

- {#each} bodies whose bound values appear in attributes (href={link.href},
  src={item.img}) now record attr slots; the browser hydrates them from the
  rendered attribute so component previews no longer mount with empty links.
  Single-expression attributes hydrate exactly; mixed values stay unhydrated
  as before. A new slot classifier also refuses shapes that would crash a
  shallow hydration item (deep paths, method calls, bare item renders) and
  routes them to source-preview mode instead.
- Style directives now run the mixed loop/outer identifier check before the
  free-identifier param check, so style:width={base + r.pct} falls back
  instead of minting a broken param.

Tests: attr-slot analysis units, crashy/lossy fallback units, an attribute-
bound anchor in the stateful SvelteKit fixture asserted through accept, and
a mountedDomProbe e2e hook that reads the hydrated href off the mounted
variant DOM (verified to fail when hydration is disabled).

AI-assisted (Claude Code).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-28 15:33:16 -07:00
co-authored by Claude Code
parent 6997e4bdb5
commit 39f233ac24
6 changed files with 320 additions and 43 deletions
+10
View File
@@ -5446,6 +5446,16 @@
const texts = collectVisibleTexts(itemEl).filter((t) => !statics.has(t));
const item = {};
slots.forEach((slot, i) => { item[slot.key] = texts[i] != null ? texts[i] : ''; });
// Attribute-bound values (href={link.href}) hydrate from the
// rendered attribute on the live item element or a descendant.
for (const slot of entry.item.attrSlots || []) {
if (item[slot.key] != null || !slot.tag) continue;
const sel = slot.tag + (slot.classes || []).map((c) => '.' + cssEscapeIdent(c)).join('');
let el = null;
try { el = itemEl.matches(sel) ? itemEl : itemEl.querySelector(sel); } catch { el = null; }
const value = el ? el.getAttribute(slot.attr) : null;
if (value != null) item[slot.key] = value;
}
// Keyed each: the key field is never rendered, so hydrate it with a
// unique per-index value or Svelte throws each_key_duplicate.
if (entry.item.keyField && item[entry.item.keyField] == null) {