/** * # 脚本 生产环境入口 * 包装 dist/server/server.js(TanStack Start 的 fetch handler): * 先用 dist/client 的静态文件(构建产物 JS/CSS 等)直接应答,未命中的请求 * 交给 Start handler。没有这层包装时 /assets/* 会被路由 307 加尾斜杠后 404, * 页面完全无 JS(水合不运行)。 */ // 构建产物无类型声明(产物哈希文件名随构建变化) // @ts-expect-error 构建产物无 d.ts import serverEntry from "../dist/server/server.js"; import { readdirSync } from "node:fs"; import { resolve, sep } from "node:path"; /** # 成员变量 dist/client 静态文件表(pathname → 绝对路径,启动时递归扫描一次) **/ const CLIENT_DIR = new URL("../dist/client/", import.meta.url).pathname; const staticFiles = new Map(); /** # 成员变量 uploads 根目录(媒体上传写入 public/uploads,运行时直读,启动静态表不含新增文件) **/ const UPLOADS_ROOT = resolve(process.cwd(), "public", "uploads"); /** # 函数 递归收集 dist/client 下全部静态文件 */ function walk(dir: string): void { for (const e of readdirSync(dir, { withFileTypes: true })) { const p = dir + "/" + e.name; if (e.isDirectory()) walk(p); // 归一化 key:物理路径去掉 CLIENT_DIR 前缀(可能残留多余斜杠)后补单斜杠 else staticFiles.set("/" + p.slice(CLIENT_DIR.length).replace(/^\/+/, ""), p); } } walk(CLIENT_DIR); export default { /** # 成员变量 监听端口(PORT 环境变量可覆盖) **/ port: Number(process.env.PORT ?? 3312), /** # 函数 请求入口:/uploads 直读磁盘(新增文件无需重启)→ 启动静态表 → Start handler */ async fetch(req: Request): Promise { const url = new URL(req.url); // /uploads/ 运行时直读磁盘:媒体库上传落在 public/uploads,静态表启动时已定格, // 不直读会导致运行中上传的图片 404。resolve 后校验前缀,防路径穿越。 if (url.pathname.startsWith("/uploads/")) { const abs = resolve(UPLOADS_ROOT, "." + url.pathname.slice("/uploads".length)); if (abs.startsWith(UPLOADS_ROOT + sep)) { const uf = Bun.file(abs); if (await uf.exists()) { return new Response(uf, { headers: { "cache-control": "public, max-age=3600" } }); } } } // 尾斜杠归一化:偶发的 ".jpg/" 形态请求(SPA 预取取回/上游转发)直接按静态 // 文件应答,避免交给 Start handler 后落入路由 catch-all 返回 404 let f = staticFiles.get(url.pathname); if (!f && url.pathname.endsWith("/")) f = staticFiles.get(url.pathname.replace(/\/+$/, "")); if (f) { return new Response(Bun.file(f), { headers: { "cache-control": "public, max-age=31536000, immutable" }, }); } return serverEntry.fetch(req); }, };