The advertised import URL now exists. The CDN is just a pages site (PAGES.md pattern): DNS + allowlist TXT in terraform, _headers for CORS and immutable caching, and the release workflow PATCHes each version's bundle in — incremental, so old versions persist. Verified in real Chromium importing buuh@8.0.0 from the live CDN via an import map. 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.