134 lines
4.1 KiB
TypeScript
134 lines
4.1 KiB
TypeScript
// @ts-nocheck -- copied byte-for-byte into the self-contained Node deployment artifact.
|
|
import { readFile, stat } from "node:fs/promises";
|
|
import { createServer } from "node:http";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
const contentTypes = Object.freeze({
|
|
".css": "text/css;charset=UTF-8",
|
|
".html": "text/html;charset=UTF-8",
|
|
".js": "text/javascript;charset=UTF-8",
|
|
".json": "application/json",
|
|
".png": "image/png",
|
|
".svg": "image/svg+xml",
|
|
".woff": "font/woff",
|
|
".woff2": "font/woff2",
|
|
});
|
|
|
|
function isStudioPath(pathname, prefix) {
|
|
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
}
|
|
|
|
async function sendFile(
|
|
file,
|
|
method,
|
|
response,
|
|
status = 200,
|
|
) {
|
|
const body = await readFile(file);
|
|
response.writeHead(status, {
|
|
"Cache-Control": "no-cache",
|
|
"Content-Length": body.byteLength,
|
|
"Content-Type": contentTypes[path.extname(file)] ?? "application/octet-stream",
|
|
Vary: "Origin",
|
|
});
|
|
if (method === "HEAD") {
|
|
response.end();
|
|
return;
|
|
}
|
|
response.end(body);
|
|
}
|
|
|
|
export function createTechLogProductionServer({
|
|
root,
|
|
contract,
|
|
}) {
|
|
const absoluteRoot = path.resolve(root);
|
|
const publicSpaPaths = new Set(contract.publicSpaPaths);
|
|
const studioSpaPathPatterns = contract.studioSpaPathPatterns.map(
|
|
(pattern) => new RegExp(pattern),
|
|
);
|
|
const server = createServer(async (request, response) => {
|
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
response.writeHead(405, { Allow: "GET, HEAD" }).end();
|
|
return;
|
|
}
|
|
try {
|
|
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
const pathname = decodeURIComponent(url.pathname);
|
|
const requested = path.resolve(absoluteRoot, `.${pathname}`);
|
|
if (
|
|
requested !== absoluteRoot &&
|
|
requested.startsWith(`${absoluteRoot}${path.sep}`)
|
|
) {
|
|
const details = await stat(requested).catch(() => null);
|
|
if (details?.isFile()) {
|
|
await sendFile(requested, request.method, response);
|
|
return;
|
|
}
|
|
}
|
|
if (
|
|
publicSpaPaths.has(pathname) ||
|
|
studioSpaPathPatterns.some((pattern) => pattern.test(pathname))
|
|
) {
|
|
await sendFile(path.join(absoluteRoot, "index.html"), request.method, response);
|
|
return;
|
|
}
|
|
if (isStudioPath(pathname, contract.studioPathPrefix)) {
|
|
await sendFile(
|
|
path.join(absoluteRoot, "index.html"),
|
|
request.method,
|
|
response,
|
|
contract.notFound.status,
|
|
);
|
|
return;
|
|
}
|
|
response.writeHead(contract.notFound.status, {
|
|
"Content-Type": contract.notFound.contentType,
|
|
});
|
|
response.end(request.method === "HEAD" ? undefined : contract.notFound.body);
|
|
} catch {
|
|
response.writeHead(contract.notFound.status, {
|
|
"Content-Type": contract.notFound.contentType,
|
|
});
|
|
response.end(request.method === "HEAD" ? undefined : contract.notFound.body);
|
|
}
|
|
});
|
|
return server;
|
|
}
|
|
|
|
function optionValue(name) {
|
|
const index = process.argv.indexOf(name);
|
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
}
|
|
|
|
async function startFromCommandLine() {
|
|
const root = path.resolve(
|
|
optionValue("--root") ?? path.dirname(fileURLToPath(import.meta.url)),
|
|
);
|
|
const host = optionValue("--host") ?? "127.0.0.1";
|
|
const port = Number(optionValue("--port") ?? 4173);
|
|
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
throw new TypeError("--port must be an integer from 1 through 65535");
|
|
}
|
|
const contract = JSON.parse(
|
|
await readFile(path.join(root, "tech-log-serving-contract.json"), "utf8"),
|
|
);
|
|
const server = createTechLogProductionServer({ root, contract });
|
|
server.listen(port, host, () => {
|
|
process.stdout.write(`TechLog production server: ${root} on http://${host}:${port}\n`);
|
|
});
|
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
process.on(signal, () => server.close(() => process.exit(0)));
|
|
}
|
|
}
|
|
|
|
const commandPath = process.argv[1];
|
|
if (
|
|
commandPath &&
|
|
import.meta.url === pathToFileURL(path.resolve(commandPath)).href
|
|
) {
|
|
await startFromCommandLine();
|
|
}
|