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
+14 -1
View File
@@ -2,6 +2,7 @@
// Same API, same event flow; ESM, consolidated nano* internals in ./lib.
import morph from '@choojs/html/morph'
import hydrate from '@choojs/html/hydrate'
import nanotiming from './lib/timing.js'
import Nanorouter from './lib/router.js'
import Nanobus from './lib/bus.js'
@@ -213,8 +214,20 @@ export class Choo {
self._tree.nodeName.toLowerCase() + '> is not the same type as the new node <' +
newTree.nodeName.toLowerCase() + '>.')
// First render adopts the existing (usually server-rendered) DOM:
// matching nodes are left in place, and any server/client markup
// disagreement is reported before the client render wins.
const morphTiming = nanotiming('choo.morph')
morph(self._tree, newTree)
hydrate(self._tree, newTree, {
onMismatch: function (diff) {
console.warn(
'choo.mount: server and client markup differ at ' + diff.path +
' (' + diff.reason + '): server rendered ' + diff.server +
', client rendered ' + diff.client +
'. The client version wins; fix the view so both sides agree.'
)
}
})
morphTiming()
renderTiming()