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
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-08 19:55:20 +02:00
co-authored by Claude Fable 5
parent b957b02410
commit ce1ec9e4a9
42 changed files with 447 additions and 171 deletions
+32
View File
@@ -0,0 +1,32 @@
name: ci
on:
push:
branches: [main, master, v8]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [24, 26]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm install
- run: npm test
- run: npm run size
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm install
- run: npx playwright install --with-deps chromium-headless-shell
- run: npm run test:e2e
+44
View File
@@ -0,0 +1,44 @@
name: release
# Tag a version (git tag v8.0.0 && git push --tags) and this publishes
# every workspace package to the uhhm npm registry on this Gitea, plus
# the single-file CDN bundle as a generic package.
#
# Repo secrets needed (Settings → Actions → Secrets):
# PACKAGES_TOKEN — a Gitea access token with package:write scope
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm install
- run: npm test
- name: point npm at the uhhm registry
run: |
echo "@uhhm:registry=https://project.uhhm.no/api/packages/uhhm/npm/" >> "$HOME/.npmrc"
echo "//project.uhhm.no/api/packages/uhhm/npm/:_authToken=${{ secrets.PACKAGES_TOKEN }}" >> "$HOME/.npmrc"
- name: publish workspaces
run: npm publish --workspaces
- name: build + upload the CDN bundle (generic package)
run: |
npm run bundle
VERSION="${GITHUB_REF_NAME#v}"
curl --fail -X PUT \
-H "Authorization: token ${{ secrets.PACKAGES_TOKEN }}" \
--upload-file dist-cdn/buuh.js \
"https://project.uhhm.no/api/packages/uhhm/generic/buuh-cdn/${VERSION}/buuh.js"
curl --fail -X PUT \
-H "Authorization: token ${{ secrets.PACKAGES_TOKEN }}" \
--upload-file dist-cdn/buuh.js.map \
"https://project.uhhm.no/api/packages/uhhm/generic/buuh-cdn/${VERSION}/buuh.js.map"
+1
View File
@@ -12,3 +12,4 @@ coverage.json
package-lock.json
yarn.lock
.idea
dist-cdn/
+67 -33
View File
@@ -1,30 +1,33 @@
<h1 align="center">choo</h1>
<h1 align="center">buuh</h1>
<div align="center">
<strong>🚂🚋🚋🚋🚋🚋</strong>
<strong>🚂🚋🚋🚋🚋👻</strong>
</div>
<div align="center">
The sturdy frontend framework — now with a modern engine under the same hood.
The sturdy little frontend framework — a friendly fork of choo,
rebuilt for the modern platform.
</div>
<br>
> **This is the v8 branch.** choo v7 is on `master` and stays untouched
> until 8.0.0 ships. Everything below describes v8: same API, same
> philosophy, rebuilt for the platform of 2026. See
> [`docs/v8.md`](docs/v8.md) for status and the full plan.
> **buuh is a fork of [choo](https://github.com/choojs/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](#credits)). Everything is MIT, and if
> upstream ever wants this work home, the door is open
> ([`docs/upstream-rfc-draft.md`](docs/upstream-rfc-draft.md)).
## What is this
Choo is a small framework for building frontend applications with plain
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.
```js
import choo from '@choojs/core'
import html from '@choojs/html'
import choo from '@uhhm/buuh'
import html from '@uhhm/buuh-html'
const app = choo()
@@ -46,12 +49,37 @@ app.route('/', (state, emit) => html`
app.mount('body')
```
## Why v8
## Getting it
From the uhhm registry (one `.npmrc` line routes the scope; everything
else still resolves from npmjs):
```console
$ 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:
```html
<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`](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 the old
compile-time transform, without the toolchain). An import map and a
`<script type="module">` is a complete development setup:
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`](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
@@ -62,17 +90,17 @@ app.mount('body')
`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, finally.** `app.route('/big', lazy(() => import('./big.js')))`
— loading view or held tree while in flight, awaited on the server.
([choojs/choo#653](https://github.com/choojs/choo/pull/653), answered.)
- **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](https://github.com/choojs/choo/pull/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 v10
## bankai
The isomorphic compiler & server, rebuilt on Vite 8 / Rolldown. Only the
client ever bundles — v8 server code is plain ESM Node runs as-authored.
client ever bundles — server code is plain ESM Node runs as-authored.
```console
$ bankai start app.js # dev: HMR + streaming SSR on every request
@@ -92,17 +120,17 @@ with the precache manifest injected, `<title>` follows your
| 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 |
| `@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 v7
## Migrating from choo v7
```console
$ npx @choojs/migrate .
$ npx @uhhm/buuh-migrate .
```
converts simple CJS, remaps every specifier, and tells you honestly what
@@ -117,13 +145,19 @@ $ 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`](.gitea/workflows/release.yml)).
## Credits
Choo v8 carries forward a decade of work by
[Yoshua Wuyts](https://github.com/yoshuawuyts) and the
[choojs contributors](https://github.com/choojs), and builds on the
ESM groundwork of the [@pirxpilot](https://github.com/pirxpilot) fork
line. Per-file attribution headers name the module and version each port
came from. MIT, as always.
buuh is choo. The design, the API, and most of the ideas are
[Yoshua Wuyts](https://github.com/yoshuawuyts)'s and the
[choojs contributors](https://github.com/choojs)'; the ESM groundwork
came from the [@pirxpilot](https://github.com/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.
+7 -7
View File
@@ -1,4 +1,4 @@
// Real-browser benchmark (Playwright Chromium): @choojs/html vs µhtml v5,
// Real-browser benchmark (Playwright Chromium): @uhhm/buuh-html vs µhtml v5,
// same create/update scenarios as bench/render.js but with native DOM.
// nanohtml v1 is CJS-only and can't be import-mapped, so it only appears
// in the happy-dom bench.
@@ -16,8 +16,8 @@ const PAGE = `<!doctype html>
<html><head><meta charset="utf-8">
<script type="importmap">${JSON.stringify({
imports: {
'@choojs/html': '/packages/html/browser.js',
'@choojs/html/morph': '/packages/html/morph.js',
'@uhhm/buuh-html': '/packages/html/browser.js',
'@uhhm/buuh-html/morph': '/packages/html/morph.js',
uhtml: '/node_modules/uhtml/dist/prod/dom.js'
}
})}</script>
@@ -27,8 +27,8 @@ await page.route('**/bench-page', (route) => route.fulfill({ contentType: 'text/
await page.goto(srv.origin + '/bench-page')
const results = await page.evaluate(async () => {
const { default: html } = await import('@choojs/html')
const { default: morph } = await import('@choojs/html/morph')
const { default: html } = await import('@uhhm/buuh-html')
const { default: morph } = await import('@uhhm/buuh-html/morph')
const { html: uhtml, render: urender } = await import('uhtml')
const ROWS = 100
@@ -51,7 +51,7 @@ const results = await page.evaluate(async () => {
const uTable = (rows) => uhtml`<table><tbody>${rows.map((r) => uhtml`<tr class=${r.selected ? 'selected' : ''}><td>${r.id}</td><td>${r.label}</td></tr>`)}</tbody></table>`
const out = {}
out['@choojs/html create'] = bench((t) => ourTable(data(t)))
out['@uhhm/buuh-html create'] = bench((t) => ourTable(data(t)))
out['uhtml v5 create (fresh container)'] = bench((t) => {
const c = document.createElement('div')
urender(c, uTable(data(t)))
@@ -59,7 +59,7 @@ const results = await page.evaluate(async () => {
const live = ourTable(data(0))
document.body.appendChild(live)
out['@choojs/html + nanomorph update'] = bench((t) => morph(live, ourTable(data(t))))
out['@uhhm/buuh-html + nanomorph update'] = bench((t) => morph(live, ourTable(data(t))))
live.remove()
const c = document.createElement('div')
+7 -7
View File
@@ -1,4 +1,4 @@
// Rendering benchmarks: @choojs/html vs nanohtml v1 vs µhtml.
// Rendering benchmarks: @uhhm/buuh-html vs nanohtml v1 vs µhtml.
//
// npm run bench
//
@@ -51,12 +51,12 @@ function bench (name, fn) {
console.log('\nserver rendering — %d-row table to string:', ROWS)
{
const { default: ourHtml } = await import('@choojs/html/server')
const { default: ourHtml } = await import('@uhhm/buuh-html/server')
const nano = require('nanohtml') // main entry is the server renderer
const table = (html, rows) => html`<table><tbody>${rows.map((r) => html`<tr class=${r.selected ? 'selected' : ''}><td>${r.id}</td><td>${r.label}</td></tr>`)}</tbody></table>`
bench('@choojs/html (server)', (t) => String(table(ourHtml, data(t))))
bench('@uhhm/buuh-html (server)', (t) => String(table(ourHtml, data(t))))
bench('nanohtml v1 (server)', (t) => String(table(nano, data(t))))
}
@@ -75,15 +75,15 @@ for (const name of [
if (!(name in globalThis) && name in win) globalThis[name] = win[name]
}
const { default: ourBrowserHtml } = await import('@choojs/html/browser')
const { default: morph } = await import('@choojs/html/morph')
const { default: ourBrowserHtml } = await import('@uhhm/buuh-html/browser')
const { default: morph } = await import('@uhhm/buuh-html/morph')
const nanoBrowser = require('nanohtml/lib/browser.js')
const { html: uhtml, render: urender } = await import('uhtml')
const table = (html, rows) => html`<table><tbody>${rows.map((r) => html`<tr class=${r.selected ? 'selected' : ''}><td>${r.id}</td><td>${r.label}</td></tr>`)}</tbody></table>`
console.log('\nbrowser-path create — fresh %d-row table per iteration (happy-dom):', ROWS)
bench('@choojs/html (cached templates)', (t) => table(ourBrowserHtml, data(t)))
bench('@uhhm/buuh-html (cached templates)', (t) => table(ourBrowserHtml, data(t)))
bench('nanohtml v1 (hyperx runtime)', (t) => table(nanoBrowser, data(t)))
bench('uhtml v5 (fresh container)', (t) => {
const c = document.createElement('div')
@@ -94,7 +94,7 @@ console.log('\nbrowser-path update — re-render same live tree (happy-dom):')
{
const live = table(ourBrowserHtml, data(0))
document.body.appendChild(live)
bench('@choojs/html + nanomorph', (t) => morph(live, table(ourBrowserHtml, data(t))))
bench('@uhhm/buuh-html + nanomorph', (t) => morph(live, table(ourBrowserHtml, data(t))))
document.body.removeChild(live)
}
{
+11 -11
View File
@@ -8,15 +8,15 @@ a platform baseline of Node ≥ 24 + Baseline Widely Available browsers.
## The fast path
```console
$ npx @choojs/migrate . # rewrite in place
$ npx @choojs/migrate --dry . # or report only
$ npx @uhhm/buuh-migrate . # rewrite in place
$ npx @uhhm/buuh-migrate --dry . # or report only
```
The codemod converts simple top-level CJS to ESM, remaps specifiers, and
prints a note for everything it won't guess at. Then:
1. Add `"type": "module"` to package.json.
2. Swap dependencies for `@choojs/core`, `@choojs/html`, and friends.
2. Swap dependencies for `@uhhm/buuh`, `@uhhm/buuh-html`, and friends.
3. Run your app. Read the console: hydration now *tells you* when server
and client markup disagree.
@@ -24,14 +24,14 @@ prints a note for everything it won't guess at. Then:
| v7 | v8 |
|---|---|
| `choo` | `@choojs/core` |
| `choo/html`, `nanohtml` | `@choojs/html` |
| `nanohtml/raw` | `@choojs/html/raw` |
| `nanomorph` | `@choojs/html/morph` |
| `nanocomponent`, `choo/component` | `@choojs/component` |
| `choo-devtools` | `@choojs/devtools` |
| `choo-lazy-route` | `lazy()` from `@choojs/core` |
| `nanobus`, `nanorouter`, `nanohref`, `nanotiming` | built into `@choojs/core` |
| `choo` | `@uhhm/buuh` |
| `choo/html`, `nanohtml` | `@uhhm/buuh-html` |
| `nanohtml/raw` | `@uhhm/buuh-html/raw` |
| `nanomorph` | `@uhhm/buuh-html/morph` |
| `nanocomponent`, `choo/component` | `@uhhm/buuh-component` |
| `choo-devtools` | `@uhhm/buuh-devtools` |
| `choo-lazy-route` | `lazy()` from `@uhhm/buuh` |
| `nanobus`, `nanorouter`, `nanohref`, `nanotiming` | built into `@uhhm/buuh` |
| `nanoquery` | built in (`state.query`); use `URLSearchParams` elsewhere |
| `nanoraf`, `nanoassert`, `nanolru` | retired — platform/built-in |
+101
View File
@@ -0,0 +1,101 @@
# Publishing buuh on project.uhhm.no
Two different distribution problems, two different mechanisms. Gitea
handles one of them natively; the other needs a static host.
## 1. `npm install` — Gitea Packages (yes, it does what you hope)
Gitea ships a real npm registry per user/org. For the `uhhm` org the
endpoint is:
```
https://project.uhhm.no/api/packages/uhhm/npm/
```
**Publishing** happens from the release workflow
(`.gitea/workflows/release.yml`): tag `v8.0.0`, push the tag, and every
workspace package is published with a `package:write` token stored as
the `PACKAGES_TOKEN` repo secret. Bump the workspace versions before
tagging.
**Consuming** needs one line of `.npmrc` in a project (or `~/.npmrc`):
```ini
@uhhm:registry=https://project.uhhm.no/api/packages/uhhm/npm/
```
Then plain npm works, and this is why the packages are scoped: npm
routes *by scope*, so `@uhhm/*` resolves against your Gitea while
everything else still comes from npmjs.org. No token is needed to
install if the packages are public (they inherit visibility from the
owner). Starting a project is:
```console
$ echo "@uhhm:registry=https://project.uhhm.no/api/packages/uhhm/npm/" >> .npmrc
$ npm i @uhhm/buuh @uhhm/buuh-html
$ npm i -D @uhhm/bankai
$ npx bankai start app.js
```
## 2. "CDN require" — the part Gitea does NOT do
This is the misunderstanding worth clearing up: **Gitea's npm registry
serves tarballs to package managers, not individual JavaScript files to
browsers.** There is no unpkg-style `https://…/@uhhm/buuh/index.js`
endpoint, and the generic-package download URLs serve
`application/octet-stream` — browsers refuse that for ES modules (strict
MIME checking). So an import map cannot point at Gitea directly.
What works instead, in order of effort:
**a. The single-file bundle + any static host (recommended).**
`npm run bundle` produces `dist-cdn/buuh.js` — the whole framework as
one minified ES module with named exports. The release workflow uploads
it to Gitea's generic package store as the archive of record; to make it
importable, serve a copy from any host that sends
`text/javascript` + CORS. Since you run uhhm.no, that's a few lines of
Caddy:
```
cdn.uhhm.no {
root * /srv/cdn
file_server
header Access-Control-Allow-Origin *
header /buuh@* Cache-Control "public, max-age=31536000, immutable"
}
```
Drop each release in as `buuh@8.0.0.js` (a `curl` from the generic
package URL, or an extra `scp` step in the workflow), and every project
on earth can do:
```html
<script type="importmap">
{ "imports": { "buuh": "https://cdn.uhhm.no/buuh@8.0.0.js" } }
</script>
<script type="module">
import { choo, html } from 'buuh'
</script>
```
That is the zero-build story with your own domain on it.
**b. Self-hosted esm.sh.** esm.sh is open source and can be pointed at
a custom npm registry — run it against the Gitea endpoint and you get
real CDN semantics (per-package URLs, versioning, bundling) for
everything you publish. More moving parts; worth it only if you want
per-package URLs rather than the one-file bundle.
**c. Raw Gitea file URLs — don't.** Gitea serves raw `.js` as
`text/plain` for safety, which module loading rejects. Fronting raw
URLs with a MIME-rewriting proxy works but is a hack with none of the
caching benefits of (a).
## Version hygiene
- Workspace versions are currently `8.0.0-dev`; set real versions
before the first tag (`npm version 8.0.0 --workspaces --no-git-tag-version`).
- The tag drives the generic-package version in the workflow, so keep
tags and package.json versions in step.
- `bankai` the bin name survives; the package is `@uhhm/bankai` so scope
routing works.
+20 -12
View File
@@ -1,4 +1,12 @@
# choo v8 — branch notes
# 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;
@@ -6,7 +14,7 @@ everything new lives under `packages/`.
## Layout
- `packages/core``@choojs/core`: the Choo class, same API as choo v7
- `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
@@ -16,7 +24,7 @@ everything new lives under `packages/`.
- `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``@choojs/html`: the rendering package.
- `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
@@ -29,13 +37,13 @@ everything new lives under `packages/`.
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``@choojs/component`: ← nanocomponent 6.6.0 +
- `packages/component``@uhhm/buuh-component`: ← nanocomponent 6.6.0 +
on-load 3.4.1 as ES classes; the future island/hydration boundary.
- `packages/devtools``@choojs/devtools`: window.choo with live state,
- `packages/devtools``@uhhm/buuh-devtools`: window.choo with live state,
event log, timings via PerformanceObserver, copy(); no-op on the server.
- `packages/migrate``@choojs/migrate`: the `choo-migrate` codemod.
- `packages/migrate``@uhhm/buuh-migrate`: the `choo-migrate` codemod.
Regex-based on purpose: converts simple top-level CJS to ESM, remaps
specifiers (choo → @choojs/core, nanohtml → @choojs/html, …), points
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
@@ -51,20 +59,20 @@ everything new lives under `packages/`.
- [x] URL normalization fix: WHATWG URL parsing, single decode with raw
fallback (no more URIError on '%'), NFC matching, per-segment
wildcard decode, decoded state.href
- [x] Phase 2 (core): browser renderer rewrite, `@choojs/component`,
- [x] Phase 2 (core): browser renderer rewrite, `@uhhm/buuh-component`,
zero-build counter example, full-app integration test in happy-dom
- [x] Phase 2 (tail): adoption-style hydration with mismatch warnings
(`@choojs/html/hydrate`, wired into `mount()`), real-browser
(`@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: @choojs/html creates fresh trees ~12% faster than µhtml v5
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 @choojs/component caching (proxy nodes skip unchanged subtrees),
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.
@@ -82,7 +90,7 @@ Server string rendering is on par with nanohtml v1 (~13k ops/s, within
the current tree) until the dynamic import lands, server awaits
it in toStream; toString fails with guidance. Views cache after
first load.
- `@choojs/devtools` and the `choo-migrate` codemod (validated by
- `@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
+3 -3
View File
@@ -1,10 +1,10 @@
// The classic choo counter. This one module is the whole app, and both
// sides consume it: the browser mounts it (import map resolves
// @choojs/html to the DOM renderer), Node stringifies it (same specifier
// @uhhm/buuh-html to the DOM renderer), Node stringifies it (same specifier
// resolves to the string renderer). That's the isomorphic contract.
import choo from '@choojs/core'
import html from '@choojs/html'
import choo from '@uhhm/buuh'
import html from '@uhhm/buuh-html'
export default function createApp () {
const app = choo()
+6 -6
View File
@@ -13,12 +13,12 @@
<script type="importmap">
{
"imports": {
"@choojs/core": "../../packages/core/index.js",
"@choojs/core/timing": "../../packages/core/lib/timing.js",
"@choojs/html": "../../packages/html/browser.js",
"@choojs/html/raw": "../../packages/html/raw.js",
"@choojs/html/morph": "../../packages/html/morph.js",
"@choojs/html/hydrate": "../../packages/html/hydrate.js"
"@uhhm/buuh": "../../packages/core/index.js",
"@uhhm/buuh/timing": "../../packages/core/lib/timing.js",
"@uhhm/buuh-html": "../../packages/html/browser.js",
"@uhhm/buuh-html/raw": "../../packages/html/raw.js",
"@uhhm/buuh-html/morph": "../../packages/html/morph.js",
"@uhhm/buuh-html/hydrate": "../../packages/html/hydrate.js"
}
}
</script>
+2 -2
View File
@@ -6,8 +6,8 @@
// stream on the server, while client-side views must be synchronous.
// Serializing streamed state for hydration is bankai v10 territory.
import choo from '@choojs/core'
import html from '@choojs/html'
import choo from '@uhhm/buuh'
import html from '@uhhm/buuh-html'
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
+5 -4
View File
@@ -1,8 +1,8 @@
{
"name": "@choojs/monorepo",
"name": "@uhhm/buuh-monorepo",
"private": true,
"version": "8.0.0-dev",
"description": "Monorepo for choo v8 — a 4kb framework for creating sturdy frontend applications",
"description": "Monorepo for buuh \u2014 a friendly fork of choo, rebuilt for the modern platform",
"type": "module",
"workspaces": [
"packages/*"
@@ -15,9 +15,10 @@
"test:e2e": "node --test test/e2e/",
"bench": "node bench/render.js",
"bench:browser": "node bench/real-browser.js",
"size": "node scripts/size.js"
"size": "node scripts/size.js",
"bundle": "node scripts/bundle.js"
},
"repository": "choojs/choo",
"repository": "https://project.uhhm.no/uhhm/buuh",
"license": "MIT",
"devDependencies": {
"happy-dom": "^20.14.0",
Regular → Executable
View File
+2 -2
View File
@@ -1,5 +1,5 @@
{
"name": "bankai",
"name": "@uhhm/bankai",
"version": "10.0.0-dev",
"description": "The isomorphic choo compiler & server: one command, streaming SSR, zero config",
"type": "module",
@@ -20,7 +20,7 @@
"dependencies": {
"vite": "^8.0.0"
},
"repository": "choojs/choo",
"repository": "https://project.uhhm.no/uhhm/buuh",
"keywords": [
"choo",
"bankai",
+2 -2
View File
@@ -2,8 +2,8 @@
// Native DOM components: render once, morph on update, lifecycle hooks via
// onload. In v8 this class is also the intended island/hydration boundary.
import morph from '@choojs/html/morph'
import nanotiming from '@choojs/core/timing'
import morph from '@uhhm/buuh-html/morph'
import nanotiming from '@uhhm/buuh/timing'
import onload, { KEY_ATTR } from './onload.js'
function makeID () {
+1 -1
View File
@@ -8,7 +8,7 @@ const KEY_ATTR = 'data-' + KEY_ID
let INDEX = 0
if (typeof window !== 'undefined' && window.MutationObserver) {
const observer = new MutationObserver(function (mutations) {
const observer = new window.MutationObserver(function (mutations) {
if (Object.keys(watch).length < 1) return
for (let i = 0; i < mutations.length; i++) {
if (mutations[i].attributeName === KEY_ATTR) {
+4 -4
View File
@@ -1,5 +1,5 @@
{
"name": "@choojs/component",
"name": "@uhhm/buuh-component",
"version": "8.0.0-dev",
"description": "Native DOM components with lifecycle hooks — the choo island boundary",
"type": "module",
@@ -15,10 +15,10 @@
"node": ">=24"
},
"dependencies": {
"@choojs/core": "*",
"@choojs/html": "*"
"@uhhm/buuh": "*",
"@uhhm/buuh-html": "*"
},
"repository": "choojs/choo",
"repository": "https://project.uhhm.no/uhhm/buuh",
"keywords": [
"component",
"dom",
+3 -3
View File
@@ -5,7 +5,7 @@ import { test, before } from 'node:test'
import assert from 'node:assert'
import { Window } from 'happy-dom'
import serverHtml from '@choojs/html/server'
import serverHtml from '@uhhm/buuh-html/server'
test('renders via createElement on the server (no window)', async () => {
const { default: Component } = await import('../index.js')
@@ -29,8 +29,8 @@ test('browser: render, mount, update morphs in place, proxy stands in', async ()
// module registry caches per specifier; component/index.js re-evaluates
// _hasWindow per instance, so importing after globals are set is enough
const { default: Component } = await import('../index.js?browser')
const { default: html } = await import('@choojs/html/browser')
const { default: morph } = await import('@choojs/html/morph')
const { default: html } = await import('@uhhm/buuh-html/browser')
const { default: morph } = await import('@uhhm/buuh-html/morph')
class Counter extends Component {
createElement (n) {
+2 -2
View File
@@ -1,8 +1,8 @@
// Ported from choo 7.1.0 index.js (MIT) — https://github.com/choojs/choo
// Same API, same event flow; ESM, consolidated nano* internals in ./lib.
import morph from '@choojs/html/morph'
import hydrate from '@choojs/html/hydrate'
import morph from '@uhhm/buuh-html/morph'
import hydrate from '@uhhm/buuh-html/hydrate'
import nanotiming from './lib/timing.js'
import Nanorouter from './lib/router.js'
import Nanobus from './lib/bus.js'
+3 -3
View File
@@ -1,5 +1,5 @@
{
"name": "@choojs/core",
"name": "@uhhm/buuh",
"version": "8.0.0-dev",
"description": "A 4kb framework for creating sturdy frontend applications",
"type": "module",
@@ -15,9 +15,9 @@
"node": ">=24"
},
"dependencies": {
"@choojs/html": "*"
"@uhhm/buuh-html": "*"
},
"repository": "choojs/choo",
"repository": "https://project.uhhm.no/uhhm/buuh",
"keywords": [
"client",
"frontend",
@@ -12,8 +12,8 @@ before(async () => {
globalThis.window = win
globalThis.document = win.document
globalThis.requestAnimationFrame = win.requestAnimationFrame.bind(win)
;({ default: choo } = await import('@choojs/core'))
;({ default: html } = await import('@choojs/html/browser'))
;({ default: choo } = await import('@uhhm/buuh'))
;({ default: html } = await import('@uhhm/buuh-html/browser'))
})
const tick = (ms = 30) => new Promise((resolve) => setTimeout(resolve, ms))
+4 -4
View File
@@ -5,11 +5,11 @@
import { test } from 'node:test'
import assert from 'node:assert'
import html from '@choojs/html'
import raw from '@choojs/html/raw'
import choo, { Choo } from '@choojs/core'
import html from '@uhhm/buuh-html'
import raw from '@uhhm/buuh-html/raw'
import choo, { Choo } from '@uhhm/buuh'
test('should render on the server with @choojs/html', () => {
test('should render on the server with @uhhm/buuh-html', () => {
const app = choo()
app.route('/', function (state, emit) {
const strong = '<strong>Hello filthy planet</strong>'
@@ -12,15 +12,15 @@ let choo, browserHtml, serverString
before(async () => {
// Server side first, before any window exists — exactly like production,
// and toString() enforces it.
;({ default: choo } = await import('@choojs/core'))
const { default: serverHtml } = await import('@choojs/html/server')
;({ default: choo } = await import('@uhhm/buuh'))
const { default: serverHtml } = await import('@uhhm/buuh-html/server')
serverString = makeApp(serverHtml).toString('/')
const win = new Window({ url: 'http://localhost/' })
globalThis.window = win
globalThis.document = win.document
globalThis.requestAnimationFrame = win.requestAnimationFrame.bind(win)
;({ default: browserHtml } = await import('@choojs/html/browser'))
;({ default: browserHtml } = await import('@uhhm/buuh-html/browser'))
})
const tick = (ms = 30) => new Promise((resolve) => setTimeout(resolve, ms))
+2 -2
View File
@@ -13,8 +13,8 @@ before(async () => {
globalThis.window = win
globalThis.document = win.document
globalThis.requestAnimationFrame = win.requestAnimationFrame.bind(win)
;({ default: choo, lazy } = await import('@choojs/core'))
;({ default: html } = await import('@choojs/html/browser'))
;({ default: choo, lazy } = await import('@uhhm/buuh'))
;({ default: html } = await import('@uhhm/buuh-html/browser'))
})
const tick = (ms = 30) => new Promise((resolve) => setTimeout(resolve, ms))
+2 -2
View File
@@ -3,8 +3,8 @@
import { test } from 'node:test'
import assert from 'node:assert'
import html from '@choojs/html'
import choo, { lazy } from '@choojs/core'
import html from '@uhhm/buuh-html'
import choo, { lazy } from '@uhhm/buuh'
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms))
+2 -2
View File
@@ -4,8 +4,8 @@
import { test } from 'node:test'
import assert from 'node:assert'
import html from '@choojs/html'
import choo from '@choojs/core'
import html from '@uhhm/buuh-html'
import choo from '@uhhm/buuh'
function view (state) {
return html`<div></div>`
+1 -1
View File
@@ -3,7 +3,7 @@
// event log, emit-from-the-console, and render timings via the
// PerformanceObserver watching nanotiming's measures. Usage:
//
// import devtools from '@choojs/devtools'
// import devtools from '@uhhm/buuh-devtools'
// app.use(devtools())
//
// The store is a no-op on the server and (by default) stays quiet in the
+2 -2
View File
@@ -1,5 +1,5 @@
{
"name": "@choojs/devtools",
"name": "@uhhm/buuh-devtools",
"version": "8.0.0-dev",
"description": "Console devtools for choo: window.choo, event log, perf timings",
"type": "module",
@@ -12,7 +12,7 @@
"engines": {
"node": ">=24"
},
"repository": "choojs/choo",
"repository": "https://project.uhhm.no/uhhm/buuh",
"keywords": [
"choo",
"devtools",
+2 -2
View File
@@ -9,8 +9,8 @@ before(async () => {
globalThis.window = win
globalThis.document = win.document
globalThis.requestAnimationFrame = win.requestAnimationFrame.bind(win)
;({ default: choo } = await import('@choojs/core'))
;({ default: html } = await import('@choojs/html/browser'))
;({ default: choo } = await import('@uhhm/buuh'))
;({ default: html } = await import('@uhhm/buuh-html/browser'))
;({ default: devtools } = await import('../index.js'))
})
+2 -2
View File
@@ -1,5 +1,5 @@
{
"name": "@choojs/html",
"name": "@uhhm/buuh-html",
"version": "8.0.0-dev",
"description": "HTML template literals that render to DOM in the browser and strings on the server",
"type": "module",
@@ -24,7 +24,7 @@
"engines": {
"node": ">=24"
},
"repository": "choojs/choo",
"repository": "https://project.uhhm.no/uhhm/buuh",
"keywords": [
"html",
"template",
+2 -2
View File
@@ -80,7 +80,7 @@ export class HtmlChunks {
for (const part of this.parts) {
if (typeof part !== 'string') {
throw new Error(
'@choojs/html: this template has async content and cannot render synchronously — stream it (choo: use toStream() instead of toString())'
'@uhhm/buuh-html: this template has async content and cannot render synchronously — stream it (choo: use toStream() instead of toString())'
)
}
out += part
@@ -168,7 +168,7 @@ export default function html (pieces, ...values) {
// tag position: attributes are synchronous, always
if (isThenable(value) || isAsyncIterable(value)) {
throw new Error('@choojs/html: async values are only allowed in child position, not in attributes')
throw new Error('@uhhm/buuh-html: async values are only allowed in child position, not in attributes')
}
// Event handlers are behavior, not markup: `onclick=${fn}` renders
+2 -2
View File
@@ -3,8 +3,8 @@
import { test } from 'node:test'
import assert from 'node:assert'
import html from '@choojs/html'
import raw from '@choojs/html/raw'
import html from '@uhhm/buuh-html'
import raw from '@uhhm/buuh-html/raw'
test('renders a template to a string', () => {
const res = html`<p>hello</p>`
+2 -2
View File
@@ -3,8 +3,8 @@
import { test } from 'node:test'
import assert from 'node:assert'
import html from '@choojs/html'
import raw from '@choojs/html/raw'
import html from '@uhhm/buuh-html'
import raw from '@uhhm/buuh-html/raw'
async function collect (chunks) {
const out = []
+1 -1
View File
@@ -49,4 +49,4 @@ for (const target of targets) {
}
}
console.log(`\n${dry ? 'dry run — no files written' : changedCount + ' file(s) rewritten'}. Remember: package.json needs "type": "module" and deps on @choojs/*.`)
console.log(`\n${dry ? 'dry run — no files written' : changedCount + ' file(s) rewritten'}. Remember: package.json needs "type": "module" and deps on @uhhm/*.`)
+15 -15
View File
@@ -5,28 +5,28 @@
// old specifier → new specifier
export const SPECIFIERS = {
choo: '@choojs/core',
'choo/html': '@choojs/html',
'choo/html/raw': '@choojs/html/raw',
'choo/component': '@choojs/component',
nanohtml: '@choojs/html',
'nanohtml/raw': '@choojs/html/raw',
nanomorph: '@choojs/html/morph',
nanocomponent: '@choojs/component',
'choo-devtools': '@choojs/devtools'
choo: '@uhhm/buuh',
'choo/html': '@uhhm/buuh-html',
'choo/html/raw': '@uhhm/buuh-html/raw',
'choo/component': '@uhhm/buuh-component',
nanohtml: '@uhhm/buuh-html',
'nanohtml/raw': '@uhhm/buuh-html/raw',
nanomorph: '@uhhm/buuh-html/morph',
nanocomponent: '@uhhm/buuh-component',
'choo-devtools': '@uhhm/buuh-devtools'
}
// old specifier → guidance (no direct replacement package)
export const RETIRED = {
nanobus: "built into @choojs/core — use app.emitter, or import Nanobus from '@choojs/core' internals is no longer needed",
nanorouter: 'built into @choojs/core — app.route covers it',
nanohref: 'built into @choojs/core — link handling is automatic',
nanotiming: "built into @choojs/core — import from '@choojs/core/timing' if you need it directly",
nanobus: "built into @uhhm/buuh — use app.emitter, or import Nanobus from '@uhhm/buuh' internals is no longer needed",
nanorouter: 'built into @uhhm/buuh — app.route covers it',
nanohref: 'built into @uhhm/buuh — link handling is automatic',
nanotiming: "built into @uhhm/buuh — import from '@uhhm/buuh/timing' if you need it directly",
nanoraf: 'retired — use requestAnimationFrame, or rely on choo render batching',
nanoquery: 'retired — state.query is built in; use URLSearchParams directly elsewhere',
nanoassert: 'retired — use plain checks or node:assert',
nanolru: 'retired — the component cache is built into @choojs/core',
'choo-lazy-route': "replaced by lazy() from '@choojs/core': app.route('/x', lazy(() => import('./x.js')))",
nanolru: 'retired — the component cache is built into @uhhm/buuh',
'choo-lazy-route': "replaced by lazy() from '@uhhm/buuh': app.route('/x', lazy(() => import('./x.js')))",
'choo-service-worker': 'kept as a concept; bankai v10 will inject the asset manifest — check docs before migrating this one'
}
+4 -4
View File
@@ -1,10 +1,10 @@
{
"name": "@choojs/migrate",
"name": "@uhhm/buuh-migrate",
"version": "8.0.0-dev",
"description": "Codemod: migrate choo v7 apps to @choojs v8 (ESM, new package names)",
"description": "Codemod: migrate choo v7 apps to buuh (choo v8) (ESM, new package names)",
"type": "module",
"bin": {
"choo-migrate": "./cli.js"
"buuh-migrate": "./cli.js"
},
"exports": {
".": "./lib/transform.js"
@@ -16,7 +16,7 @@
"engines": {
"node": ">=24"
},
"repository": "choojs/choo",
"repository": "https://project.uhhm.no/uhhm/buuh",
"keywords": [
"choo",
"codemod",
+10 -10
View File
@@ -16,9 +16,9 @@ test('converts a classic choo v7 header to v8 ESM', () => {
const { code, changed, notes } = transform(src)
assert.ok(changed)
assert.match(code, /import choo from '@choojs\/core'/)
assert.match(code, /import html from '@choojs\/html'/)
assert.match(code, /import devtools from '@choojs\/devtools'/)
assert.match(code, /import choo from '@uhhm\/buuh'/)
assert.match(code, /import html from '@uhhm\/buuh-html'/)
assert.match(code, /import devtools from '@uhhm\/buuh-devtools'/)
assert.match(code, /export default app/)
assert.deepStrictEqual(notes, [])
})
@@ -48,11 +48,11 @@ test('already-ESM sources get specifiers remapped', () => {
].join('\n')
const { code } = transform(src)
assert.match(code, /from '@choojs\/core'/)
assert.match(code, /from '@choojs\/html'\n/)
assert.match(code, /from '@choojs\/html\/raw'/)
assert.match(code, /from '@choojs\/html\/morph'/)
assert.match(code, /from '@choojs\/component'/)
assert.match(code, /from '@uhhm\/buuh'/)
assert.match(code, /from '@uhhm\/buuh-html'\n/)
assert.match(code, /from '@uhhm\/buuh-html\/raw'/)
assert.match(code, /from '@uhhm\/buuh-html\/morph'/)
assert.match(code, /from '@uhhm\/buuh-component'/)
})
test('retired packages produce notes, not rewrites', () => {
@@ -69,7 +69,7 @@ test('retired packages produce notes, not rewrites', () => {
test('dynamic import() specifiers are remapped too', () => {
const { code } = transform("const mod = await import('choo/html')")
assert.match(code, /import\('@choojs\/html'\)/)
assert.match(code, /import\('@uhhm\/buuh-html'\)/)
})
test('unconvertible CJS is reported honestly', () => {
@@ -79,7 +79,7 @@ test('unconvertible CJS is reported honestly', () => {
].join('\n')
const { code, notes } = transform(src)
assert.match(code, /require\('@choojs\/core'\)/, 'specifier remapped even inside functions')
assert.match(code, /require\('@uhhm\/buuh'\)/, 'specifier remapped even inside functions')
assert.ok(notes.some((n) => n.includes('module.exports')))
assert.ok(notes.some((n) => n.includes('require() calls remain')))
})
+55
View File
@@ -0,0 +1,55 @@
// Build the single-file CDN bundle: everything a browser needs as one
// minified ES module, for import-map use against any static host that
// serves JavaScript with the right MIME type.
//
// npm run bundle → dist-cdn/buuh.js (+ sourcemap)
//
// <script type="importmap">
// { "imports": { "buuh": "https://cdn.uhhm.no/buuh@8.0.0.js" } }
// </script>
// <script type="module">
// import { choo, html } from 'buuh'
// </script>
import { build } from 'vite'
import { readFile } from 'node:fs/promises'
import { join, dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const { version } = JSON.parse(await readFile(join(root, 'packages/core/package.json'), 'utf8'))
const VIRTUAL = '\0bundle:entry'
await build({
appType: 'custom',
logLevel: 'warn',
plugins: [{
name: 'bundle-entry',
resolveId: (id) => id === 'bundle:entry' ? VIRTUAL : undefined,
load: (id) => id === VIRTUAL
? [
"export { default as choo, Choo, lazy } from '@uhhm/buuh'",
"export { default as html } from '@uhhm/buuh-html'",
"export { default as raw } from '@uhhm/buuh-html/raw'",
"export { default as morph } from '@uhhm/buuh-html/morph'",
"export { default as hydrate } from '@uhhm/buuh-html/hydrate'",
"export { default as Component } from '@uhhm/buuh-component'",
"export { default as devtools } from '@uhhm/buuh-devtools'"
].join('\n')
: undefined
}],
build: {
outDir: join(root, 'dist-cdn'),
emptyOutDir: true,
sourcemap: true,
lib: false,
rollupOptions: {
input: { buuh: 'bundle:entry' },
output: { entryFileNames: 'buuh.js' },
preserveEntrySignatures: 'strict'
}
}
})
console.log(`bundled buuh ${version} → dist-cdn/buuh.js`)
+5 -5
View File
@@ -1,5 +1,5 @@
// The size budget: what does the framework itself cost on the wire?
// Bundles @choojs/core + @choojs/html (browser condition) minified via
// Bundles @uhhm/buuh + @uhhm/buuh-html (browser condition) minified via
// the same Vite/Rolldown pipeline apps use, then reports min+gzip and
// min+brotli. CI fails if min+gzip exceeds the budget.
//
@@ -28,9 +28,9 @@ await build({
name: 'size-entry',
resolveId: (id) => id === 'size:entry' ? VIRTUAL : undefined,
load: (id) => id === VIRTUAL
? "import choo, { Choo, lazy } from '@choojs/core'\n" +
"import html from '@choojs/html'\n" +
"import raw from '@choojs/html/raw'\n" +
? "import choo, { Choo, lazy } from '@uhhm/buuh'\n" +
"import html from '@uhhm/buuh-html'\n" +
"import raw from '@uhhm/buuh-html/raw'\n" +
'window.__keep = { choo, Choo, lazy, html, raw }\n'
: undefined
}],
@@ -52,7 +52,7 @@ const br = brotliCompressSync(code, {
}).length
const kb = (n) => (n / 1024).toFixed(2) + ' kB'
console.log(`@choojs/core + @choojs/html (browser, minified)`)
console.log(`@uhhm/buuh + @uhhm/buuh-html (browser, minified)`)
console.log(` raw: ${kb(code.length)}`)
console.log(` gzip: ${kb(gz)} (budget ${kb(BUDGET_GZIP)})`)
console.log(` brotli: ${kb(br)}`)
+6 -6
View File
@@ -24,12 +24,12 @@ const MIME = {
const IMPORT_MAP = JSON.stringify({
imports: {
'@choojs/core': '/packages/core/index.js',
'@choojs/core/timing': '/packages/core/lib/timing.js',
'@choojs/html': '/packages/html/browser.js',
'@choojs/html/raw': '/packages/html/raw.js',
'@choojs/html/morph': '/packages/html/morph.js',
'@choojs/html/hydrate': '/packages/html/hydrate.js'
'@uhhm/buuh': '/packages/core/index.js',
'@uhhm/buuh/timing': '/packages/core/lib/timing.js',
'@uhhm/buuh-html': '/packages/html/browser.js',
'@uhhm/buuh-html/raw': '/packages/html/raw.js',
'@uhhm/buuh-html/morph': '/packages/html/morph.js',
'@uhhm/buuh-html/hydrate': '/packages/html/hydrate.js'
}
})