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