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
JavaScript
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