README rewritten for the v8 branch: honest size claim (7.97 kB min+gzip for the whole framework, with the v7 '4kb' context), the zero-build story up front, bankai v10, package map, credits to yoshuawuyts, the choojs contributors and the pirxpilot fork line. docs/migrating-v7-to-v8.md: the codemod path, the specifier map, and every deliberate behavior change spelled out. docs/deploy.md: plain Node, Docker, proxy/CDN — including the HTTP/3 answer (h3 is infrastructure's job; bankai's contract is the 103 + Link headers that any hints-aware edge, h3 included, propagates; server push is dead everywhere and never existed in h3) — and web-standard runtimes. docs/rfc.md: the draft announcement for choojs/choo — continuation framing, pings to yoshuawuyts and pirxpilot, the npm-rights ask, API feedback questions, and a three-week comment window with a silence-is-consent close. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
choo
This is the v8 branch. choo v7 is on
masterand stays untouched until 8.0.0 ships. Everything below describes v8: same API, same philosophy, rebuilt for the platform of 2026. Seedocs/v8.mdfor status and the full plan.
What is this
Choo 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 '@choojs/core'
import html from '@choojs/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')
Why v8
- No build step, anywhere. Templates parse at runtime (cached per
call site in a WeakMap, clone-based instantiation — as fast as the 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, finally.
app.route('/big', lazy(() => import('./big.js')))— loading view or held tree while in flight, awaited on the server. (choojs/choo#653, answered.) - 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 v10
The isomorphic compiler & server, rebuilt on Vite 8 / Rolldown. Only the client ever bundles — v8 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 |
|---|---|
@choojs/core |
the Choo class: stores, router, emitter, toString/toStream, lazy() |
@choojs/html |
tagged templates: DOM in the browser, strings/streams on the server, morph, hydrate, raw |
@choojs/component |
stateful components with lifecycle hooks; the hydration boundary |
@choojs/devtools |
window.choo console tooling |
@choojs/migrate |
choo-migrate — the v7 → v8 codemod |
bankai |
the compiler & server above |
Migrating from v7
$ npx @choojs/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
Credits
Choo v8 carries forward a decade of work by Yoshua Wuyts and the choojs contributors, and builds on the ESM groundwork of the @pirxpilot fork line. Per-file attribution headers name the module and version each port came from. MIT, as always.