// bankai serve — the production server. Static assets from dist // (precompressed variants negotiated, hashed assets cached forever); // everything else is a page: 103 Early Hints from the build manifest, // then streaming SSR of the entry module — imported natively, because // the server side never needed a build. import { createServer } from 'node:http' import { createReadStream } from 'node:fs' import { readFile, stat } from 'node:fs/promises' import { join, normalize, extname, resolve } from 'node:path' import { pathToFileURL } from 'node:url' import { resolveApp } from './app.js' import { documentHead, documentTail, assetLinks } from './document.js' const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.map': 'application/json', '.txt': 'text/plain; charset=utf-8', '.woff2': 'font/woff2', '.png': 'image/png', '.webp': 'image/webp', '.ico': 'image/x-icon' } export default async function serve ({ outDir = 'dist', port = 8080 }) { outDir = resolve(outDir) const meta = JSON.parse(await readFile(join(outDir, 'bankai.json'), 'utf8')) const mod = await import(pathToFileURL(meta.entry)) const links = assetLinks(meta.assets) const server = createServer(async (req, res) => { const url = new URL(req.url, 'http://localhost') const pathname = decodeURIComponent(url.pathname) // static asset? const file = normalize(join(outDir, pathname)) if (file.startsWith(outDir) && extname(pathname)) { if (await isFile(file)) return sendFile(req, res, file, pathname) if (MIME[extname(pathname)]) { // a missing asset is a miss, not a page res.writeHead(404, { 'content-type': 'text/plain' }) return res.end('not found: ' + pathname) } } // page: hints first, then stream try { const app = resolveApp(mod, meta.entry) const state = {} if (links.length && res.writeEarlyHints) { res.writeEarlyHints({ link: links }) } res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', link: links.join(', ') }) res.write(documentHead({ title: meta.title, ...meta.assets })) for await (const chunk of app.toStream(req.url, state)) { res.write(chunk) } res.end(documentTail(state)) } catch (err) { console.error(err) if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain' }) res.end('bankai: render error') } }) await new Promise((resolveListen) => server.listen(port, resolveListen)) return { server, port: server.address().port, origin: `http://localhost:${server.address().port}`, close: () => new Promise((resolveClose) => server.close(resolveClose)) } } async function isFile (path) { try { return (await stat(path)).isFile() } catch (e) { return false } } async function sendFile (req, res, file, pathname) { const headers = { 'content-type': MIME[extname(file)] || 'application/octet-stream', // hashed build assets are immutable; everything else revalidates 'cache-control': pathname.startsWith('/assets/') ? 'public, max-age=31536000, immutable' : 'no-cache', vary: 'accept-encoding' } // negotiate precompressed siblings written at build time const accepted = String(req.headers['accept-encoding'] || '') for (const [encoding, ext] of [['br', '.br'], ['gzip', '.gz']]) { if (accepted.includes(encoding) && await isFile(file + ext)) { headers['content-encoding'] = encoding res.writeHead(200, headers) createReadStream(file + ext).pipe(res) return } } res.writeHead(200, headers) createReadStream(file).pipe(res) }