feat(bankai): Phase 4 follow-ups — css convention, live titles, prerender, HTTP/2, size budget
- style.css convention: <entry dir>/style.css is imported by the virtual client entry (CSS is a client concern; server code never sees it — the v8 answer to sheetify), extracted and hashed by Vite, stylesheet-linked in the head and preloaded via Early Hints. Counter example styled. - SSR <title> from state: the server reads the first stream chunk before writing the head, by which point stores and the first render slice have run — DOMTitleChange emits land in the document title. Counter emits. - bankai build --prerender /,/about renders routes through the same toStream path to static <route>/index.html (precompressed, precached by the service worker, served with Early Hints). - bankai serve --h2: HTTP/2 with a generated local cert (openssl, cached; allowHTTP1) — browsers only act on Early Hints over h2/h3. Tested with a real h2 client observing the 103 interim response. - npm run size: the framework wire-size budget, enforced in CI. Whole framework (core + html engine + morph + hydrate) is 7.97 kB min+gzip / 7.15 kB brotli; budget 8.5 kB with the v7 '4kb' context documented (that number excluded the html engine, which lived in the browserify transform). 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
0a351d7f33
commit
99f9da1e3b
@@ -16,11 +16,11 @@ import serve from '../lib/serve.js'
|
||||
const repo = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
||||
const entry = join(repo, 'examples', 'counter', 'app.js')
|
||||
|
||||
let outDir, meta, srv
|
||||
let outDir, meta, prerendered, srv
|
||||
|
||||
before(async () => {
|
||||
outDir = await mkdtemp(join(tmpdir(), 'bankai-test-'))
|
||||
;({ meta } = await build({ entry, outDir, title: 'counter' }))
|
||||
;({ meta, prerendered } = await build({ entry, outDir, title: 'counter', prerender: ['/'] }))
|
||||
srv = await serve({ outDir, port: 0 })
|
||||
})
|
||||
|
||||
@@ -58,16 +58,38 @@ test('build produced manifest, metadata and precompressed assets', async () => {
|
||||
await access(join(outDir, chunk.slice(1) + '.gz'))
|
||||
})
|
||||
|
||||
test('pages get 103 Early Hints carrying the route assets', async () => {
|
||||
test('the style.css convention bundles css the server never imports', async () => {
|
||||
assert.strictEqual(meta.assets.css.length, 1, 'extracted css asset')
|
||||
const res = await get('/some/page')
|
||||
const text = res.body.toString()
|
||||
assert.match(text, /<link rel="stylesheet" href="\/assets\/.*\.css">/)
|
||||
assert.match(String(res.headers.link), /as=style/, 'stylesheet in the Link header')
|
||||
})
|
||||
|
||||
test('prerendered routes are written and served statically with hints', async () => {
|
||||
assert.deepStrictEqual(prerendered, ['/'])
|
||||
await access(join(outDir, 'index.html'))
|
||||
const res = await get('/')
|
||||
assert.strictEqual(res.status, 200)
|
||||
assert.match(res.body.toString(), /count is 0/)
|
||||
assert.strictEqual(res.interim[0]?.statusCode, 103, 'prerendered pages still send Early Hints')
|
||||
})
|
||||
|
||||
test('the SSR <title> comes from state (DOMTitleChange during render)', async () => {
|
||||
const res = await get('/some/page')
|
||||
assert.match(res.body.toString(), /<title>count is 0<\/title>/)
|
||||
})
|
||||
|
||||
test('live pages get 103 Early Hints carrying the route assets', async () => {
|
||||
const res = await get('/some/page')
|
||||
assert.strictEqual(res.interim.length, 1, 'one interim response')
|
||||
assert.strictEqual(res.interim[0].statusCode, 103)
|
||||
assert.match(String(res.interim[0].headers.link), /rel=modulepreload/)
|
||||
assert.match(String(res.headers.link), /rel=modulepreload/, 'Link header repeated on the final response')
|
||||
})
|
||||
|
||||
test('pages stream SSR html with a filtered initialState tail', async () => {
|
||||
const res = await get('/')
|
||||
test('live pages stream SSR html with a filtered initialState tail', async () => {
|
||||
const res = await get('/some/page')
|
||||
const text = res.body.toString()
|
||||
assert.strictEqual(res.status, 200)
|
||||
assert.match(text, /<h1>count is 0<\/h1>/, 'server-rendered view')
|
||||
@@ -76,6 +98,38 @@ test('pages stream SSR html with a filtered initialState tail', async () => {
|
||||
assert.ok(!/initialState=.*"events"/.test(text), 'internals filtered from state')
|
||||
})
|
||||
|
||||
test('h2: HTTP/2 server sends Early Hints and streams the page', async (t) => {
|
||||
const { default: http2 } = await import('node:http2')
|
||||
let h2srv
|
||||
try {
|
||||
h2srv = await serve({ outDir, port: 0, h2: true })
|
||||
} catch (err) {
|
||||
t.skip('openssl unavailable: ' + err.message)
|
||||
return
|
||||
}
|
||||
|
||||
const session = http2.connect(`https://localhost:${h2srv.port}`, { rejectUnauthorized: false })
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const req = session.request({ ':path': '/some/page' })
|
||||
let interim = null
|
||||
let status = null
|
||||
let body = ''
|
||||
req.on('headers', (headers) => { interim = headers })
|
||||
req.on('response', (headers) => { status = headers[':status'] })
|
||||
req.on('data', (d) => { body += d })
|
||||
req.on('end', () => resolve({ interim, status, body }))
|
||||
req.on('error', reject)
|
||||
req.end()
|
||||
})
|
||||
session.close()
|
||||
await h2srv.close()
|
||||
|
||||
assert.strictEqual(result.status, 200)
|
||||
assert.strictEqual(result.interim?.[':status'], 103, '103 interim over h2')
|
||||
assert.match(String(result.interim?.link), /rel=modulepreload/)
|
||||
assert.match(result.body, /count is 0/)
|
||||
})
|
||||
|
||||
test('hashed assets are immutable and served precompressed on request', async () => {
|
||||
const chunk = meta.assets.scripts[0]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user