Files
Bendik Aagaard LynghaugandClaude Fable 5 ce1ec9e4a9 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
2026-09-08 19:55:20 +02:00

79 lines
3.0 KiB
JavaScript

// Async holes and streaming in the server tag.
import { test } from 'node:test'
import assert from 'node:assert'
import html from '@uhhm/buuh-html'
import raw from '@uhhm/buuh-html/raw'
async function collect (chunks) {
const out = []
for await (const chunk of chunks) out.push(chunk)
return out
}
const wait = (ms, value) => new Promise((resolve) => setTimeout(() => resolve(value), ms))
test('sync templates stream as a single chunk equal to toString()', async () => {
const res = html`<p>hello ${'world'}</p>`
const chunks = await collect(res)
assert.deepStrictEqual(chunks, ['<p>hello world</p>'])
assert.strictEqual(chunks.join(''), res.toString())
})
test('a promise child splits the stream at the hole', async () => {
const res = html`<main><h1>shell</h1>${wait(10, 'late')}<footer>end</footer></main>`
const chunks = await collect(res)
assert.strictEqual(chunks[0], '<main><h1>shell</h1>', 'everything before the hole flushes first')
assert.strictEqual(chunks.join(''), '<main><h1>shell</h1>late<footer>end</footer></main>')
})
test('toString() on async content throws with streaming guidance', () => {
const res = html`<p>${Promise.resolve('x')}</p>`
assert.throws(() => res.toString(), /toStream/)
})
test('resolved promises can carry templates, arrays, and raw', async () => {
const res = html`<ul>${wait(5, [html`<li>a</li>`, html`<li>${'<b>'}</li>`, raw('<li>raw</li>')])}</ul>`
const chunks = await collect(res)
assert.strictEqual(chunks.join(''), '<ul><li>a</li><li>&lt;b&gt;</li><li>raw</li></ul>')
})
test('nested templates with async holes stream through their parents', async () => {
const inner = html`<section>${wait(5, 'inner-late')}</section>`
const res = html`<div><p>early</p>${inner}</div>`
const chunks = await collect(res)
assert.strictEqual(chunks[0], '<div><p>early</p><section>', 'parent flushes up to the nested hole')
assert.strictEqual(chunks.join(''), '<div><p>early</p><section>inner-late</section></div>')
})
test('async iterable children stream chunk by chunk', async () => {
async function * rows () {
for (let i = 0; i < 3; i++) yield html`<li>${i}</li>`
}
const res = html`<ul>${rows()}</ul>`
const chunks = await collect(res)
assert.strictEqual(chunks.join(''), '<ul><li>0</li><li>1</li><li>2</li></ul>')
assert.ok(chunks.length >= 4, 'each yielded row is its own chunk')
})
test('multiple async holes resolve in document order', async () => {
// the second promise resolves first; output order must follow the document
const res = html`<div>${wait(20, 'A')}|${wait(5, 'B')}</div>`
const chunks = await collect(res)
assert.strictEqual(chunks.join(''), '<div>A|B</div>')
})
test('async values in attribute position throw immediately', () => {
assert.throws(
() => html`<div class=${Promise.resolve('x')}></div>`,
/child position/
)
})
test('resolved async strings are escaped like any child', async () => {
const res = html`<p>${wait(5, '<script>')}</p>`
const chunks = await collect(res)
assert.strictEqual(chunks.join(''), '<p>&lt;script&gt;</p>')
})