Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,344 @@
|
||||
import { ServerResponse, createServer, type IncomingMessage } from "node:http";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import type { ServerBuild } from "react-router";
|
||||
import { loadRequiredWebConfig } from "@toolctrl/config";
|
||||
import { createChildLogger, defaultLogger } from "@toolctrl/logger";
|
||||
import { app as apiApp } from "../apps/api/src/app";
|
||||
import { getHostRoutingRuntimeDecision } from "./host-routing-runtime";
|
||||
|
||||
process.env.NODE_ENV = process.env.NODE_ENV ?? "production";
|
||||
|
||||
const RUNTIME_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT_DIRECTORY = RUNTIME_DIRECTORY.endsWith(path.join("build", "server-runtime"))
|
||||
? path.resolve(RUNTIME_DIRECTORY, "..", "..")
|
||||
: path.resolve(RUNTIME_DIRECTORY, "..");
|
||||
const WEB_BUILD_DIRECTORY = process.env.WEB_BUILD_DIRECTORY
|
||||
? path.resolve(process.env.WEB_BUILD_DIRECTORY)
|
||||
: path.join(REPO_ROOT_DIRECTORY, "apps", "web", "build");
|
||||
const BUILD_PATH = path.join(WEB_BUILD_DIRECTORY, "server", "index.js");
|
||||
const CLIENT_BUILD_DIRECTORY = path.join(WEB_BUILD_DIRECTORY, "client");
|
||||
const ALLOWED_STATIC_ASSET_EXTENSIONS = new Set([
|
||||
".avif",
|
||||
".css",
|
||||
".eot",
|
||||
".gif",
|
||||
".ico",
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".js",
|
||||
".json",
|
||||
".mjs",
|
||||
".png",
|
||||
".svg",
|
||||
".ttf",
|
||||
".txt",
|
||||
".webmanifest",
|
||||
".webp",
|
||||
".woff",
|
||||
".woff2",
|
||||
".xml",
|
||||
]);
|
||||
const webConfig = loadRequiredWebConfig();
|
||||
const PORT = webConfig.listen.port;
|
||||
const HOST = webConfig.listen.host;
|
||||
const serverLogger = createChildLogger(defaultLogger, { component: "server" });
|
||||
|
||||
const [{ createRequestHandler }, { createReadableStreamFromReadable, writeReadableStreamToWritable }] = await Promise.all([
|
||||
import("react-router"),
|
||||
import("@react-router/node"),
|
||||
]);
|
||||
const build = (await import(pathToFileURL(BUILD_PATH).href)) as ServerBuild;
|
||||
const handleReactRouterRequest = createRequestHandler(build, process.env.NODE_ENV);
|
||||
|
||||
type NodeRequestInit = RequestInit & { duplex?: "half" };
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
try {
|
||||
if (await serveStaticAsset(req, res)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const request = createWebRequest(req, res);
|
||||
const routingDecision = getHostRoutingRuntimeDecision(request);
|
||||
if (routingDecision.type === "redirect") {
|
||||
await sendWebResponse(
|
||||
res,
|
||||
new Response(null, {
|
||||
status: routingDecision.status,
|
||||
headers: { Location: routingDecision.location },
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (routingDecision.type === "reject") {
|
||||
await sendWebResponse(res, new Response("Not Found", { status: routingDecision.status }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (routingDecision.target === "web" && isWebOperationalRoute(request)) {
|
||||
await sendWebResponse(res, await handleReactRouterRequest(request));
|
||||
return;
|
||||
}
|
||||
|
||||
const response = routingDecision.target === "api" ? await apiApp.fetch(routingDecision.request) : await handleReactRouterRequest(routingDecision.request);
|
||||
await sendWebResponse(res, response);
|
||||
} catch (error) {
|
||||
serverLogger.error({ error }, "server request failed");
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 500;
|
||||
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
||||
}
|
||||
res.end("Internal Server Error");
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
const address = HOST ?? getLocalAddress() ?? "localhost";
|
||||
serverLogger.info({ host: address, port: PORT }, "server listening");
|
||||
});
|
||||
|
||||
for (const signal of ["SIGTERM", "SIGINT"] as const) {
|
||||
process.once(signal, () => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
serverLogger.error({ error }, "server shutdown failed");
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function serveStaticAsset(req: IncomingMessage, res: ServerResponse) {
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const requestUrl = new URL(req.url ?? "/", "http://internal");
|
||||
const filePath = getClientFilePath(requestUrl.pathname);
|
||||
if (!filePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fileStat = await stat(filePath).catch(() => null);
|
||||
if (!fileStat?.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const headers = getStaticHeaders(filePath, requestUrl.pathname);
|
||||
res.writeHead(200, headers);
|
||||
|
||||
if (req.method === "HEAD") {
|
||||
res.end();
|
||||
return true;
|
||||
}
|
||||
|
||||
createReadStream(filePath)
|
||||
.on("error", (error) => {
|
||||
serverLogger.error({ error }, "static asset stream failed");
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500);
|
||||
}
|
||||
res.end();
|
||||
})
|
||||
.pipe(res);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getClientFilePath(pathname: string) {
|
||||
let decodedPathname: string;
|
||||
try {
|
||||
decodedPathname = decodeURIComponent(pathname);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relativePath = decodedPathname.replace(/^\/+/, "");
|
||||
if (!relativePath) {
|
||||
return null;
|
||||
}
|
||||
if (!ALLOWED_STATIC_ASSET_EXTENSIONS.has(path.extname(relativePath).toLowerCase())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filePath = path.resolve(CLIENT_BUILD_DIRECTORY, relativePath);
|
||||
if (filePath !== CLIENT_BUILD_DIRECTORY && !filePath.startsWith(`${CLIENT_BUILD_DIRECTORY}${path.sep}`)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
function createWebRequest(req: IncomingMessage, res: ServerResponse) {
|
||||
const protocol = req.headers["x-forwarded-proto"]?.toString().split(",")[0]?.trim() || "http";
|
||||
const host = req.headers.host || "localhost";
|
||||
const url = new URL(req.url ?? "/", `${protocol}://${host}`);
|
||||
const controller = new AbortController();
|
||||
|
||||
res.on("close", () => {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
const init: NodeRequestInit = {
|
||||
method: req.method,
|
||||
headers: createWebHeaders(req.headers),
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||
init.body = createReadableStreamFromReadable(req);
|
||||
init.duplex = "half";
|
||||
}
|
||||
|
||||
return new Request(url, init);
|
||||
}
|
||||
|
||||
function isWebOperationalRoute(request: Request) {
|
||||
const { pathname } = new URL(request.url);
|
||||
return pathname === "/healthz" || pathname === "/readyz" || pathname === "/healthcheck";
|
||||
}
|
||||
|
||||
function createWebHeaders(headers: IncomingMessage["headers"]) {
|
||||
const webHeaders = new Headers();
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
webHeaders.append(key, item);
|
||||
}
|
||||
} else if (value !== undefined) {
|
||||
webHeaders.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return webHeaders;
|
||||
}
|
||||
|
||||
async function sendWebResponse(res: ServerResponse, response: Response) {
|
||||
res.statusCode = response.status;
|
||||
res.statusMessage = response.statusText;
|
||||
|
||||
for (const [key, value] of response.headers) {
|
||||
res.appendHeader(key, value);
|
||||
}
|
||||
appendWebSecurityHeaders(res);
|
||||
|
||||
if (!response.body) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
await writeReadableStreamToWritable(response.body, res);
|
||||
}
|
||||
|
||||
function appendWebSecurityHeaders(headers: Headers): Headers;
|
||||
function appendWebSecurityHeaders(headers: Record<string, string>): Record<string, string>;
|
||||
function appendWebSecurityHeaders(headers: ServerResponse): ServerResponse;
|
||||
function appendWebSecurityHeaders(headers: Headers | Record<string, string> | ServerResponse) {
|
||||
const securityHeaders: Record<string, string> = {
|
||||
"Content-Security-Policy": "base-uri 'self'; object-src 'none'; frame-ancestors 'self'",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||
"X-Frame-Options": "SAMEORIGIN",
|
||||
"Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=()",
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Resource-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "unsafe-none",
|
||||
};
|
||||
|
||||
if (webConfig.routing.protocol === "https") {
|
||||
securityHeaders["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains";
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(securityHeaders)) {
|
||||
if (key === "Content-Security-Policy" && hasHeader(headers, key)) {
|
||||
continue;
|
||||
}
|
||||
if (headers instanceof Headers) {
|
||||
headers.set(key, value);
|
||||
} else if (headers instanceof ServerResponse) {
|
||||
headers.setHeader(key, value);
|
||||
} else {
|
||||
headers[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function hasHeader(headers: Headers | Record<string, string> | ServerResponse, key: string) {
|
||||
if (headers instanceof Headers) {
|
||||
return headers.has(key);
|
||||
}
|
||||
if (headers instanceof ServerResponse) {
|
||||
return headers.hasHeader(key);
|
||||
}
|
||||
const normalizedKey = key.toLowerCase();
|
||||
return Object.keys(headers).some((candidate) => candidate.toLowerCase() === normalizedKey);
|
||||
}
|
||||
|
||||
function getStaticHeaders(filePath: string, pathname: string) {
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": getContentType(filePath),
|
||||
};
|
||||
|
||||
// Cache-Control strategy aligned with docs/ops/asset-cdn.md:
|
||||
// * /assets/* → Vite-hashed, safe to cache forever (1 year, immutable)
|
||||
// * /fonts/* → static brand assets, 1 day
|
||||
// * /images/* → static brand assets, 1 day
|
||||
// Anything else (HTML / loader responses / favicons) keeps the default
|
||||
// (no Cache-Control here — the loader / entry.server is in charge).
|
||||
if (pathname === "/assets" || pathname.startsWith("/assets/")) {
|
||||
headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
||||
} else if (pathname === "/fonts" || pathname.startsWith("/fonts/") || pathname === "/images" || pathname.startsWith("/images/")) {
|
||||
headers["Cache-Control"] = "public, max-age=86400";
|
||||
}
|
||||
|
||||
return appendWebSecurityHeaders(headers);
|
||||
}
|
||||
|
||||
function getContentType(filePath: string) {
|
||||
switch (path.extname(filePath).toLowerCase()) {
|
||||
case ".css":
|
||||
return "text/css; charset=utf-8";
|
||||
case ".js":
|
||||
case ".mjs":
|
||||
return "text/javascript; charset=utf-8";
|
||||
case ".json":
|
||||
case ".webmanifest":
|
||||
return "application/json; charset=utf-8";
|
||||
case ".svg":
|
||||
return "image/svg+xml";
|
||||
case ".png":
|
||||
return "image/png";
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
return "image/jpeg";
|
||||
case ".gif":
|
||||
return "image/gif";
|
||||
case ".ico":
|
||||
return "image/x-icon";
|
||||
case ".txt":
|
||||
return "text/plain; charset=utf-8";
|
||||
case ".html":
|
||||
return "text/html; charset=utf-8";
|
||||
case ".woff":
|
||||
return "font/woff";
|
||||
case ".woff2":
|
||||
return "font/woff2";
|
||||
case ".ttf":
|
||||
return "font/ttf";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
|
||||
function getLocalAddress() {
|
||||
return Object.values(os.networkInterfaces())
|
||||
.flat()
|
||||
.find((entry) => entry && String(entry.family).includes("4") && !entry.internal)?.address;
|
||||
}
|
||||
Reference in new issue
Block a user