← Files BranchaARCHIVED FILE
skills/brancha/app/src/server/http/static.ts
1.91 KB · Oct 4, 2026 · 12:29 UTC
import { existsSync, readFileSync } from 'node:fs';
import type { ServerResponse } from 'node:http';
import { extname, join, normalize, relative } from 'node:path';
import { sendText } from './response.js';
import { BASE_SECURITY_HEADERS, UI_CONTENT_SECURITY_POLICY } from './security.js';
const DIST = join(process.env.BRANCHA_APP_DIR || process.cwd(), 'dist', 'public');
const MIME: Record<string, string> = {
'.css': 'text/css',
'.html': 'text/html',
'.js': 'text/javascript',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.woff2': 'font/woff2',
};
export function serveStatic(res: ServerResponse, requestPath: string) {
const requested = normalize(requestPath === '/' ? 'index.html' : requestPath.replace(/^\/+/, ''));
let file = join(DIST, requested);
if (relative(DIST, file).startsWith('..')) {
sendText(res, 403, 'forbidden');
return;
}
if (!existsSync(file)) {
// SPA fallback 只用于无扩展名的路由路径;缺失的静态资源必须如实 404,
// 否则旧缓存页面加载已被替换的 hash 资源时会拿到 HTML,应用静默挂死。
if (extname(requested) !== '') {
sendText(res, 404, 'not found');
return;
}
file = join(DIST, 'index.html');
}
if (!existsSync(file)) {
sendText(res, 404, 'Brancha runtime is incomplete');
return;
}
const contentType = MIME[extname(file)] || 'application/octet-stream';
// 带内容 hash 的 assets 永不变化,可长缓存;HTML 壳必须每次回源验证。
const cacheControl = relative(DIST, file).startsWith('assets')
? 'public, max-age=31536000, immutable'
: 'no-cache';
res.writeHead(200, {
...BASE_SECURITY_HEADERS,
'Cache-Control': cacheControl,
'Content-Security-Policy': UI_CONTENT_SECURITY_POLICY,
'Content-Type': contentType.startsWith('text/') ? `${contentType}; charset=utf-8` : contentType,
'X-Frame-Options': 'DENY',
});
res.end(readFileSync(file));
}
SHA-256: e4cd7d7e254cfdf6abd2f1c331c2a025855dc126967ab5a1177481f6a2cbdbef