Files
buuh/docs/v8.md
T
Bendik Aagaard LynghaugandClaude Fable 5 ce1ec9e4a9 rebrand: buuh — a friendly public fork under the uhhm org
Packages renamed to the @uhhm scope (@uhhm/buuh, @uhhm/buuh-html,
@uhhm/buuh-component, @uhhm/buuh-devtools, @uhhm/buuh-migrate,
@uhhm/bankai). Scoping is load-bearing twice over: npm routes registries
per scope so @uhhm/* resolves against project.uhhm.no while everything
else stays on npmjs, and it means this fork never squats upstream's
names anywhere. The codemod now migrates choo v7 apps to the @uhhm
names. README rewritten with the fork framing and full upstream credit;
the choojs RFC moves to docs/upstream-rfc-draft.md, in the drawer for if
this work ever goes home. API unchanged — choo() is still choo().

Also: Gitea Actions CI + release workflows (npm publish to the uhhm
registry on tag push, CDN bundle uploaded as a generic package),
npm run bundle producing dist-cdn/buuh.js (the whole framework as one
minified ES module for import-map use), docs/publishing.md explaining
what Gitea Packages is (a real npm registry) and is not (a CDN — serve
the bundle from a static host with module-safe MIME instead), and
onload.js constructing window.MutationObserver to match its own guard
(surfaced by smoke-testing the bundle outside a full browser).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
2026-09-08 19:55:20 +02:00

8.9 KiB

buuh (choo v8) — branch notes

Naming note: this work now ships publicly as buuh, a friendly fork under the uhhm org on project.uhhm.no, with packages scoped @uhhm/* (scoped so npm's per-scope registry routing works — and so we never squat upstream's names on any registry). The docs below use both names; "v8" refers to this modernization effort either way. The upstream RFC stays drafted in docs/upstream-rfc-draft.md if this ever goes home.

This branch is the working tree for the v8 modernization effort. The v7 code at the repo root is untouched and stays authoritative until 8.0.0 ships; everything new lives under packages/.

Layout

  • packages/core@uhhm/buuh: the Choo class, same API as choo v7 (use/route/start/mount/toString/emit), ported to ESM. The nano* internals are consolidated into lib/ as attributed ports:
    • lib/bus.js ← nanobus 4.5.0
    • lib/router.js ← nanorouter 4.0.0 + wayfarer 7.0.1 (+ trie)
    • lib/cache.js ← choo component/cache.js + nanolru 1.0.0
    • lib/raf.js ← nanoraf 3.1.0, lib/href.js ← nanohref 3.1.0
    • lib/timing.js ← nanotiming 7.3.1 (unified on global performance)
    • lib/dom.js ← document-ready 2.0.1 + scroll-to-anchor 1.0.0
    • lib/query.js — nanoquery replaced by URLSearchParams
  • packages/html@uhhm/buuh-html: the rendering package.
    • server.js — server-side tagged template (← nanohtml 1.10.0 server, transform branches removed; pure runtime)
    • browser.js — runtime-only cached template tag: each template literal is parsed once (WeakMap keyed on its strings array) into a plus hole instructions; renders clone and fill. Values never pass through innerHTML. Document-level roots ( etc.) parse via DOMParser since drops them. Known limits: no dynamic tag names, no holes in raw-text elements, SVG fragments need their root, and (unlike v7) whitespace is preserved as authored — which is what makes server and browser output byte-identical.
    • morph.js — ← nanomorph 5.4.3, consolidated to one module
    • raw.js — mark pre-encoded strings (works with both renderers)
    • packages/component@uhhm/buuh-component: ← nanocomponent 6.6.0 + on-load 3.4.1 as ES classes; the future island/hydration boundary.
    • packages/devtools@uhhm/buuh-devtools: window.choo with live state, event log, timings via PerformanceObserver, copy(); no-op on the server.
    • packages/migrate@uhhm/buuh-migrate: the choo-migrate codemod. Regex-based on purpose: converts simple top-level CJS to ESM, remaps specifiers (choo → @uhhm/buuh, nanohtml → @uhhm/buuh-html, …), points retired packages at their replacements, and reports everything it refuses to guess at.
    • examples/counter — the isomorphic proof: one app module, mounted zero-build in the browser via import map (index.html), string-rendered in Node (render.js).
    • examples/streaming — streaming SSR demo server (node examples/streaming/server.js).
    • Status vs the modernization plan

      • Phase 1: monorepo scaffold, ESM ports, choo v7 node test suite green on node --test (Node ≥ 24), CI on GitHub Actions
      • URL normalization fix: WHATWG URL parsing, single decode with raw fallback (no more URIError on '%'), NFC matching, per-segment wildcard decode, decoded state.href
      • Phase 2 (core): browser renderer rewrite, @uhhm/buuh-component, zero-build counter example, full-app integration test in happy-dom
      • Phase 2 (tail): adoption-style hydration with mismatch warnings (@uhhm/buuh-html/hydrate, wired into mount()), real-browser Playwright e2e (zero-build page, SSR-then-hydrate page, adoption proof; CI job included), benchmarks vs nanohtml v1 / µhtml

      Benchmarks (2026-09, 100-row table, npm run bench / bench:browser)

      Real Chromium: @uhhm/buuh-html creates fresh trees ~12% faster than µhtml v5 (3.5k vs 3.1k ops/s) — the parse-once/clone design pays off in native DOM. µhtml updates in place ~5x faster than our fresh-tree + nanomorph loop (3.0k vs 0.6k ops/s): that is choo's architectural cost, mitigated in real apps by @uhhm/buuh-component caching (proxy nodes skip unchanged subtrees), and the number to beat if Phase 3 explores keyed-hole optimizations. Server string rendering is on par with nanohtml v1 (~13k ops/s, within 6%). happy-dom numbers in bench/render.js are indicative only.

      • Phase 3: v8 wiring, all shipped together: - toStream(location, state) — web-standard ReadableStream of UTF-8; new Response(stream) on web servers, Readable.fromWeb(stream).pipe(res) on Node. Progressive: the server tag now builds a parts list, so promises/async iterables in child position flush the shell first and stream the rest in document order (proven byte-level and in real Chromium). - state.prefetch — stores push promises during init; toStream awaits them before rendering; toString refuses them loudly. - lazy(loader, loadingView?) — the answer to choojs/choo#653, both halves at once: browser renders the loading view (or holds the current tree) until the dynamic import lands, server awaits it in toStream; toString fails with guidance. Views cache after first load. - @uhhm/buuh-devtools and the choo-migrate codemod (validated by migrating choo's own v7 example and running the result on v8).
      • Phase 3 follow-ups for the RFC: API shape feedback on lazy() (thenable handlers vs wrapper), serializing streamed state for hydration of async holes (bankai v10 territory).
      • Phase 4: bankai v10 — packages/bankai, a thin shell over Vite 8 (Rolldown). The server side needs no build at all: v8 apps are plain ESM that Node runs as-authored, so only the client bundles. - bankai start <entry> — dev: Vite middleware mode + HMR, with per-request streaming SSR through vite.ssrLoadModule and a virtual client entry (the user writes one isomorphic module; bankai generates the two lines of browser glue). - bankai build <entry> — client bundle + Vite manifest + dist/bankai.json route/asset metadata + service worker (if <entry dir>/sw.js exists, built with the precache list injected) + brotli/gzip precompression of every text asset. - bankai serve — production: static assets with immutable caching and precompressed-variant negotiation, 103 Early Hints (res.writeEarlyHints) carrying the route's assets before every SSR page, streaming render, and a window.initialState tail (script-safe serialization, choo internals filtered; hydration ignores server-only scripts). - bankai inspect — raw/gzip/brotli size table from the manifest. All proven end-to-end in real Chromium: both dev and prod pages hydrate with zero console errors and zero mismatch warnings, and the 103 interim response is asserted at the HTTP level.
      • Phase 4 follow-ups: bankai serve --h2 (local certs via openssl, 103 verified with a real h2 client), bankai build --prerender, SSR <title> from state (the server reads the first stream chunk before writing the head), the style.css convention (client-only CSS — the sheetify answer), and the CI wire-size budget (npm run size: 7.97 kB min+gzip for the whole framework, budget 8.5 kB).
      • Phase 5: README rewritten for v8 (honest size claim, credits), docs/migrating-v7-to-v8.md, docs/deploy.md (Node, Docker, proxy/CDN with the HTTP/3 story — h3 is infrastructure's job, bankai's contract is the 103 + Link headers any hints-aware edge propagates — and web-standard runtimes), and docs/rfc.md: the draft announcement to post on choojs/choo, with the pings, the asks, and the three-week comment window.
      • Post-RFC: publish pre-releases under a next tag, refresh choo.io/handbook (separate repos), deprecation pointers on retired packages only after 8.0.0, first-class edge adapter for bankai.
      • Phase 4: bankai v10 (Vite 8/Rolldown shell, SSR middleware, 103 Early Hints, service worker, precompression)
      • Phase 5: docs, examples, launch

      Deliberate changes from v7

      • choo() still works without new; Choo is also a named export.
      • nanoquery → URLSearchParams (same output shape, repeated keys → arrays).
      • Assertions live in lib/assert.js so production builds can strip them.
      • Everything targets Node ≥ 24 and Baseline Widely Available browsers; there is no compile step and no transpilation anywhere.

      Attribution

      All ported modules are MIT-licensed work by the original choojs/nano* authors and contributors; per-file headers note the source package and version. This branch exists to carry that work forward, not to replace it.