/** * Mozimo production server. * * Serves the static build output from dist/client (immutable caching for * hashed assets, gzip for text) and hands every other request to the * TanStack Start SSR handler exported by dist/server/server.js. * * Env: PORT (default 3000), HOST (default 0.0.0.0) */ import { createServer } from "node:http"; import { createReadStream, existsSync, statSync } from "node:fs"; import { createGzip } from "node:zlib"; import { extname, join, resolve, sep } from "node:path"; import { Readable } from "node:stream"; import handler from "./dist/server/server.js"; const HOST = process.env.HOST ?? "0.0.0.0"; const PORT = Number(process.env.PORT ?? 3000); const CLIENT_ROOT = resolve("dist/client"); 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", ".txt": "text/plain; charset=utf-8", ".xml": "application/xml; charset=utf-8", ".map": "application/json; charset=utf-8", ".svg": "image/svg+xml", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp", ".avif": "image/avif", ".gif": "image/gif", ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf", ".mp4": "video/mp4", ".webm": "video/webm", ".pdf": "application/pdf", ".webmanifest": "application/manifest+json", }; const COMPRESSIBLE = new Set([ ".html", ".js", ".mjs", ".css", ".json", ".txt", ".xml", ".map", ".svg", ".webmanifest", ]); function isHashedAsset(pathname) { // Vite emits content-hashed filenames under /assets/ return pathname.startsWith("/assets/") && /\-[A-Za-z0-9_-]{8}\.[^.]+$/.test(pathname); } function sendError(res, status, message) { if (!res.headersSent) { res.writeHead(status, { "content-type": "text/plain; charset=utf-8" }); } res.end(message); } /** Serve a static file from dist/client, or return null to fall through to SSR. */ function tryStatic(req, res, pathname) { if (req.method !== "GET" && req.method !== "HEAD") return false; const rel = pathname.slice(1); const filePath = resolve(CLIENT_ROOT, rel); if (filePath !== CLIENT_ROOT && !filePath.startsWith(CLIENT_ROOT + sep)) return false; if (!existsSync(filePath) || !statSync(filePath).isFile()) return false; const ext = extname(filePath).toLowerCase(); const type = MIME[ext] ?? "application/octet-stream"; const headers = { "content-type": type, "x-content-type-options": "nosniff", "cache-control": isHashedAsset(pathname) ? "public, max-age=31536000, immutable" : "public, max-age=86400", }; const acceptsGzip = COMPRESSIBLE.has(ext) && statSync(filePath).size > 1024 && /\bgzip\b/.test(req.headers["accept-encoding"] ?? ""); if (acceptsGzip) { headers["content-encoding"] = "gzip"; headers.vary = "accept-encoding"; } res.writeHead(200, headers); if (req.method === "HEAD") { res.end(); return true; } const stream = createReadStream(filePath); stream.on("error", () => sendError(res, 500, "Failed to read asset")); stream.on("open", () => { if (acceptsGzip) { stream.pipe(createGzip()).pipe(res); } else { stream.pipe(res); } }); return true; } async function toWebRequest(req, url) { const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value === undefined) continue; for (const v of Array.isArray(value) ? value : [value]) headers.append(key, v); } const hasBody = req.method !== "GET" && req.method !== "HEAD"; return new Request(url, { method: req.method, headers, body: hasBody ? Readable.toWeb(req) : undefined, ...(hasBody ? { duplex: "half" } : {}), }); } async function sendFromHandler(req, res) { const url = `http://${req.headers.host ?? `${HOST}:${PORT}`}${req.url}`; const request = await toWebRequest(req, url); const response = await handler.fetch(request); const responseHeaders = {}; for (const [key, value] of response.headers) responseHeaders[key] = value; if (typeof response.headers.getSetCookie === "function") { const cookies = response.headers.getSetCookie(); if (cookies.length) responseHeaders["set-cookie"] = cookies; } res.writeHead(response.status, responseHeaders); if (req.method === "HEAD" || !response.body) { res.end(); return; } Readable.fromWeb(response.body).pipe(res); } const server = createServer(async (req, res) => { let pathname = "/"; try { pathname = decodeURIComponent(new URL(req.url ?? "/", "http://localhost").pathname); } catch { return sendError(res, 400, "Bad request"); } try { if (tryStatic(req, res, pathname)) return; if (extname(pathname)) return sendError(res, 404, "Not found"); await sendFromHandler(req, res); } catch (error) { console.error("[mozimo] request failed:", error); sendError(res, 500, "Internal server error"); } }); // Graceful shutdown so `docker stop` is fast and clean. let shuttingDown = false; function shutdown(signal) { if (shuttingDown) return; shuttingDown = true; console.log(`[mozimo] ${signal} received — closing server`); server.close(() => process.exit(0)); // Drop keep-alive sockets so close() isn't held open by idle clients. server.closeAllConnections?.(); setTimeout(() => process.exit(0), 3000).unref(); } process.on("SIGTERM", () => shutdown("SIGTERM")); process.on("SIGINT", () => shutdown("SIGINT")); server.listen(PORT, HOST, () => { console.log(`[mozimo] serving on http://${HOST}:${PORT}`); });