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:
co-authored by
Claude Fable 5
parent
b957b02410
commit
ce1ec9e4a9
@@ -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
|
||||
@@ -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"
|
||||
@@ -12,3 +12,4 @@ coverage.json
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.idea
|
||||
dist-cdn/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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 () {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'))
|
||||
})
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>`
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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/*.`)
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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')))
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
@@ -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'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user