UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

51 lines 2.57 kB
import { readFile, stat } from 'node:fs/promises'; import { join, normalize, sep } from 'node:path'; import { contentTypeFor } from './content-type.js'; import { requestPathname } from '../request-path.js'; // Serve the built dashboard bundle (#405). The dashboard is a plain Vite SPA — one static // `index.html` plus `assets/**` — so the daemon serves it as // plain files with an SPA fallback (any non-asset path yields `index.html`, which boots // the client router). Assets are copied into the // framework package at build time (see scripts/bundle-dashboard.mjs). /** Whether a real, readable file exists at `path`. */ async function isFile(path) { return stat(path).then(s => s.isFile()).catch(() => false); } /** Decode a percent-encoded path; a malformed escape names no file, so it decodes to nothing. */ function tryDecode(pathname) { try { return decodeURIComponent(pathname); } catch { return ''; } } /** * Serve `dir`'s static bundle for this request: the requested file when it exists, * otherwise `index.html` (the SPA fallback, so client routes and unknown paths still * boot the app). Path-traversal is guarded — a request that escapes `dir` falls back to * `index.html` rather than reading outside the bundle. */ export async function serveClientBundle(req, res, dir) { // Neither an unparseable request target nor a malformed escape (`/%zz`) may throw: this // runs void-dispatched, so an exception here would be an unhandled rejection that takes // the daemon down (#938). Both fall back to the SPA shell like any other unknown path. const pathname = requestPathname(req) ?? '/'; const rel = tryDecode(pathname).replace(/^\/+/, ''); const root = normalize(dir); const candidate = normalize(join(root, rel)); const within = candidate === root || candidate.startsWith(root + sep); const target = within && rel && (await isFile(candidate)) ? candidate : join(root, 'index.html'); const body = await readFile(target).catch(() => undefined); if (body === undefined) { res.writeHead(404, { 'content-type': 'text/plain' }); res.end('dashboard bundle not built'); return; } const type = contentTypeFor(target); // Fingerprinted assets are immutable; index.html must always revalidate. const cacheControl = target.endsWith('index.html') ? 'no-cache' : 'public, max-age=31536000, immutable'; res.writeHead(200, { 'content-type': type, 'cache-control': cacheControl }); res.end(body); } //# sourceMappingURL=static.js.map