Files
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

5.9 KiB

buuh

🚂🚋🚋🚋🚋👻
The sturdy little frontend framework — a friendly fork of choo, rebuilt for the modern platform.

buuh is a fork of choo — same API, same philosophy, modern engine. It exists because we love choo and wanted it alive on today's platform; all credit for the design belongs upstream (see Credits). Everything is MIT, and if upstream ever wants this work home, the door is open (docs/upstream-rfc-draft.md).

What is this

buuh is a small framework for building frontend applications with plain JavaScript and HTML. State lives in stores, changes flow through an event emitter, views are tagged template literals, and re-renders morph the real DOM. The whole framework — core, html engine, morphing, hydration, router, event bus — is 7.97 kB min+gzip (7.15 kB brotli), enforced by CI.

import choo from '@uhhm/buuh'
import html from '@uhhm/buuh-html'

const app = choo()

app.use((state, emitter) => {
  state.count = 0
  emitter.on('increment', (n) => {
    state.count += n
    emitter.emit('render')
  })
})

app.route('/', (state, emit) => html`
  <body>
    <h1>count is ${state.count}</h1>
    <button onclick=${() => emit('increment', 1)}>Increment</button>
  </body>
`)

app.mount('body')

Getting it

From the uhhm registry (one .npmrc line routes the scope; everything else still resolves from npmjs):

$ echo "@uhhm:registry=https://project.uhhm.no/api/packages/uhhm/npm/" >> .npmrc
$ npm i @uhhm/buuh @uhhm/buuh-html
$ npm i -D @uhhm/bankai

Or with no package manager at all — one file, one import map:

<script type="importmap">
  { "imports": { "buuh": "https://cdn.uhhm.no/buuh@8.0.0.js" } }
</script>
<script type="module">
  import { choo, html } from 'buuh'
</script>

See docs/publishing.md for how the registry and the CDN bundle fit together.

Why this fork

  • No build step, anywhere. Templates parse at runtime (cached per call site in a WeakMap, clone-based instantiation — as fast as choo's old compile-time transform, without the toolchain). An import map and a <script type="module"> is a complete development setup: see examples/counter/index.html.
  • Isomorphic by contract. One app module. The browser mounts it, the server imports the same file and calls toString(route) or streams it with toStream(route) — a web-standard ReadableStream, so it plugs into Node, Deno, Bun, and edge runtimes alike.
  • Streaming SSR. Async values in child position flush the shell first and stream the rest in document order. Store data via state.prefetch promises. Hydration adopts the server DOM (same element references, form state kept) and warns precisely when server and client markup disagree.
  • Async routes. app.route('/big', lazy(() => import('./big.js'))) — loading view or held tree while in flight, awaited on the server (the answer to choojs/choo#653).
  • Modern platform, no shims. ESM only, Node ≥ 24, Baseline Widely Available browsers. The router speaks WHATWG URL and survives emoji, literal %, and Unicode normalization differences.

bankai

The isomorphic compiler & server, rebuilt on Vite 8 / Rolldown. Only the client ever bundles — server code is plain ESM Node runs as-authored.

$ bankai start app.js     # dev: HMR + streaming SSR on every request
$ bankai build app.js     # client bundle, manifest, service worker,
                          # brotli/gzip precompression, --prerender
$ bankai serve            # prod: 103 Early Hints, immutable assets,
                          # streaming SSR, --h2
$ bankai inspect          # raw/gzip/brotli size report

Conventions over config: style.css next to your entry is bundled for the client (server code never sees it), sw.js becomes a service worker with the precache manifest injected, <title> follows your DOMTitleChange emits — on the server too.

Packages

package what it is
@uhhm/buuh the app class: stores, router, emitter, toString/toStream, lazy()
@uhhm/buuh-html tagged templates: DOM in the browser, strings/streams on the server, morph, hydrate, raw
@uhhm/buuh-component stateful components with lifecycle hooks; the hydration boundary
@uhhm/buuh-devtools window.choo console tooling
@uhhm/buuh-migrate buuh-migrate — the choo v7 → buuh codemod
@uhhm/bankai the compiler & server above (bin: bankai)

Migrating from choo v7

$ npx @uhhm/buuh-migrate .

converts simple CJS, remaps every specifier, and tells you honestly what it didn't dare touch. The API surface is unchanged — choo() still works without new. See docs/migrating-v7-to-v8.md.

Development

$ npm install
$ npm test              # 109 tests on node:test, zero framework deps
$ npm run test:e2e      # real Chromium: hydration, streaming, bankai
$ npm run size          # the wire-size budget
$ npm run bench         # vs nanohtml v1 and µhtml
$ npm run bundle        # the single-file CDN build

Releases: bump versions, git tag vX.Y.Z, push the tag — Gitea Actions tests, publishes every package to the uhhm registry, and uploads the CDN bundle (.gitea/workflows/release.yml).

Credits

buuh is choo. The design, the API, and most of the ideas are Yoshua Wuyts's and the choojs contributors'; the ESM groundwork came from the @pirxpilot fork line; this fork modernized the engine and the toolchain around them. Per-file attribution headers name the module and version each port came from. MIT, as always.