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
79 lines
3.0 KiB
JavaScript
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><b></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><script></p>')
|
|
})
|