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
buuh
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: seeexamples/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 withtoStream(route)— a web-standardReadableStream, 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.prefetchpromises. 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.