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:
co-authored by
Claude Fable 5
parent
37bd8adf81
commit
13155a3469
@@ -0,0 +1,75 @@
|
||||
// Hydration: adoption semantics and mismatch detection.
|
||||
|
||||
import { test, before } from 'node:test'
|
||||
import assert from 'node:assert'
|
||||
import { Window } from 'happy-dom'
|
||||
|
||||
let html, hydrate
|
||||
|
||||
before(async () => {
|
||||
const win = new Window()
|
||||
globalThis.window = win
|
||||
globalThis.document = win.document
|
||||
;({ default: html } = await import('../browser.js'))
|
||||
;({ default: hydrate } = await import('../hydrate.js'))
|
||||
})
|
||||
|
||||
test('agreeing trees hydrate without a mismatch report', () => {
|
||||
const a = html`<div class="x"><h1>hi</h1><p>body</p></div>`
|
||||
const b = html`<div class="x"><h1>hi</h1><p>body</p></div>`
|
||||
let reported = null
|
||||
hydrate(a, b, { onMismatch: (d) => { reported = d } })
|
||||
assert.strictEqual(reported, null)
|
||||
})
|
||||
|
||||
test('adoption: matching child nodes keep their identity through hydrate', () => {
|
||||
const server = html`<div><h1>hi</h1><input value="typed by user" /></div>`
|
||||
const serverH1 = server.querySelector('h1')
|
||||
const serverInput = server.querySelector('input')
|
||||
|
||||
const client = html`<div><h1>hi</h1><input value="typed by user" /></div>`
|
||||
hydrate(server, client)
|
||||
|
||||
assert.strictEqual(server.querySelector('h1'), serverH1, 'h1 adopted in place')
|
||||
assert.strictEqual(server.querySelector('input'), serverInput, 'input adopted in place')
|
||||
})
|
||||
|
||||
test('text mismatches are reported with a path, client wins', () => {
|
||||
const server = html`<div><h1>count is 0</h1></div>`
|
||||
const client = html`<div><h1>count is 7</h1></div>`
|
||||
let reported = null
|
||||
hydrate(server, client, { onMismatch: (d) => { reported = d } })
|
||||
|
||||
assert.strictEqual(reported.reason, 'text')
|
||||
assert.match(reported.path, /div > h1/)
|
||||
assert.strictEqual(reported.server, 'count is 0')
|
||||
assert.strictEqual(reported.client, 'count is 7')
|
||||
assert.strictEqual(server.textContent, 'count is 7', 'client render won')
|
||||
})
|
||||
|
||||
test('attribute mismatches are reported', () => {
|
||||
const server = html`<div><a href="/old">go</a></div>`
|
||||
const client = html`<div><a href="/new">go</a></div>`
|
||||
let reported = null
|
||||
hydrate(server, client, { onMismatch: (d) => { reported = d } })
|
||||
|
||||
assert.strictEqual(reported.reason, 'attribute')
|
||||
assert.match(reported.server, /\/old/)
|
||||
assert.match(reported.client, /\/new/)
|
||||
})
|
||||
|
||||
test('child count mismatches are reported', () => {
|
||||
const server = html`<ul><li>a</li></ul>`
|
||||
const client = html`<ul><li>a</li><li>b</li></ul>`
|
||||
let reported = null
|
||||
hydrate(server, client, { onMismatch: (d) => { reported = d } })
|
||||
|
||||
assert.strictEqual(reported.reason, 'children')
|
||||
})
|
||||
|
||||
test('mismatch detection is optional and hydrate still morphs without it', () => {
|
||||
const server = html`<p>old</p>`
|
||||
const client = html`<p>new</p>`
|
||||
hydrate(server, client)
|
||||
assert.strictEqual(server.textContent, 'new')
|
||||
})
|
||||
Reference in New Issue
Block a user