Server pages legitimately carry scripts (state serialization, analytics)
that are spent by hydration time and that client views never render —
they are noise to the detector, and morph dropping them is harmless.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
The server tag now builds a parts list (string runs + async holes) instead
of an eager string. Sync templates collapse to one string and toString()
is byte-identical to before. Promises and async iterables in child
position make the result streamable: iterating it yields everything before
a hole immediately, then each hole as it resolves, in document order —
resolved values get full child semantics (templates, arrays, raw,
escaping). toString() on async content throws with streaming guidance;
async values in attribute position throw immediately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
Three end-to-end scenarios against a static+SSR test server: the
zero-build page (import map + native ESM, no bundler), the SSR page
(rendered content in the raw response, live after hydration, no mismatch
warnings), and adoption (an expando on the server-rendered node survives
a real render). Renders are raf-batched so assertions poll.
Real Chromium flushed out three fixes happy-dom couldn't see:
- nanoraf called an extracted requestAnimationFrame bare — Illegal
invocation under strict-mode ESM (sloppy CJS had masked it); wrapped.
- the counter example only routed '/', so serving it from any subpath
threw; it now has a wildcard fallback.
- hydration mismatch detection is now whitespace-insensitive (the parser
reparents whitespace, e.g. text after </body>), and page scripts belong
in <head> when a view owns <body> — same convention bankai v9 used.
CI gets an e2e job with chromium-headless-shell.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
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
Each template literal parses once, keyed by its strings array in a
WeakMap: static parts become a <template> with hole markers, renders
clone and fill. Interpolated values never pass through innerHTML (child
values become text nodes or adopted DOM; attributes go through
setAttribute). Event handlers set as properties so nanomorph copies them.
Document-level roots (<body>/<head>/<html>) parse via DOMParser because
<template> drops them. Replaces the v7 browserify transform entirely —
production speed is now a runtime property.
Tests run in happy-dom (new devDependency), including a check that server
and browser renderers serialize identical markup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
ESM ports with attribution: nanohtml 1.10.0 server tag (browserify/babel
transform branches removed — pure runtime), raw-server, and nanomorph 5.4.3
consolidated into one module. browser.js is a Phase 2 placeholder that
throws loudly. Covered by test/server.test.js on node:test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd