feat(bankai): bankai v10 — the isomorphic compiler & server on Vite 8

One command, zero config, and only the client ever bundles: v8 server
code is plain ESM that Node runs as-authored, so there is no server
build to rot (the lesson of v9).

- start: Vite middleware mode + HMR with per-request streaming SSR via
  ssrLoadModule; a virtual client entry generates the browser glue so
  the user writes exactly one isomorphic module (plan decision D5).
- build: client bundle via Vite 8/Rolldown, manifest-derived route
  assets in dist/bankai.json, service worker built with the precache
  list injected (choo-service-worker convention, manifest edition),
  brotli+gzip precompression of every text asset.
- serve: immutable caching + precompressed negotiation for hashed
  assets, 103 Early Hints (res.writeEarlyHints) with the route's assets
  before every page, streaming SSR, and a window.initialState tail with
  script-breakout-safe serialization and choo internals filtered out.
- inspect: raw/gzip/brotli size report.

Integration tests drive the real counter example through build and
serve, asserting the 103 interim response at the HTTP level.

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:32:37 +02:00
co-authored by Claude Fable 5
parent d755af5ca0
commit 7e08b585ac
13 changed files with 716 additions and 1 deletions
+56
View File
@@ -0,0 +1,56 @@
// bankai inspect — what did the build produce, and how big is it on the
// wire? Reads the manifest + dist and prints raw/gzip/brotli sizes.
import { readFile, readdir, stat } from 'node:fs/promises'
import { join, resolve, relative } from 'node:path'
export default async function inspect ({ outDir = 'dist' }) {
outDir = resolve(outDir)
const meta = JSON.parse(await readFile(join(outDir, 'bankai.json'), 'utf8'))
const rows = []
await walk(outDir, rows, outDir)
rows.sort((a, b) => b.raw - a.raw)
console.log(`\nbankai build — ${meta.title} (${meta.builtAt})\n`)
console.log(' ' + 'file'.padEnd(44) + 'raw'.padStart(10) + 'gzip'.padStart(10) + 'brotli'.padStart(10))
let totals = { raw: 0, gz: 0, br: 0 }
for (const row of rows) {
totals.raw += row.raw
totals.gz += row.gz || 0
totals.br += row.br || 0
console.log(' ' + row.file.padEnd(44) + kb(row.raw) + kb(row.gz) + kb(row.br))
}
console.log(' ' + 'total'.padEnd(44) + kb(totals.raw) + kb(totals.gz) + kb(totals.br) + '\n')
return rows
}
async function walk (dir, rows, root) {
for (const name of await readdir(dir)) {
if (name === '.vite') continue
const path = join(dir, name)
const info = await stat(path)
if (info.isDirectory()) {
await walk(path, rows, root)
continue
}
if (name.endsWith('.br') || name.endsWith('.gz')) continue
const row = { file: relative(root, path), raw: info.size, gz: null, br: null }
row.gz = await sizeOf(path + '.gz')
row.br = await sizeOf(path + '.br')
rows.push(row)
}
}
async function sizeOf (path) {
try {
return (await stat(path)).size
} catch (e) {
return null
}
}
function kb (bytes) {
if (bytes === null) return '—'.padStart(10)
return (bytes < 1024 ? bytes + ' B' : (bytes / 1024).toFixed(1) + ' kB').padStart(10)
}