missed a file
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* 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}`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user