feat(html): adoption-style hydration with mismatch reporting

hydrate(oldNode, newNode) normalizes the client tree to parser text-node
granularity, walks both trees to report the first server/client markup
disagreement (node, text, attribute, or child count, with a path), then
morphs — matching nodes are adopted in place, the client render wins.
choo.mount() now hydrates and console.warns on mismatch.

Building this surfaced two real isomorphism divergences, both fixed:
adjacent text nodes from template holes vs the parser's merged runs
(folded via Node.normalize), and the server serializing onclick="" where
the browser sets a property — event handlers now leave no trace in server
markup at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-08 17:49:32 +02:00
co-authored by Claude Fable 5
parent 37bd8adf81
commit 13155a3469
7 changed files with 323 additions and 3 deletions
+7 -2
View File
@@ -45,9 +45,14 @@ test('boolean attributes render when truthy and drop when falsy', () => {
assert.strictEqual(off.toString(), '<input />'.replace(' ', ' '))
})
test('event handler functions are stripped', () => {
test('event handler functions leave no trace in markup', () => {
const res = html`<button onclick=${() => {}}>go</button>`
assert.strictEqual(res.toString(), '<button onclick="">go</button>')
assert.strictEqual(res.toString(), '<button >go</button>')
})
test('handler-shaped attributes with non-function values still render', () => {
const res = html`<a onclick=${'confirm()'}>x</a>`
assert.strictEqual(res.toString(), '<a onclick="confirm()">x</a>')
})
test('spread-style object interpolation renders attributes', () => {