Files
photos/client-server/serve.mjs
T
jschoubben 08d5c118d3 Become a nox mesh module
The mesh builds all three images from this repository and a commit
(novox/hq ADR 0069) — the Drone pipeline that built outside Docker and
copied its output in retires with the predecessor. One module carries the
API and every client instance: eef and filip are the same built client
under two names, which is a second route contribution, not a second
module (ADR 0015 — an application is one repository).

The clients drop nginx and its bash startup script: the node the API
already needs serves the static build too (client-server/serve.mjs —
files, the SPA fallback, env-config.js from the environment), so the
whole module stands on one declared base.

What the machine serves does not move: the same four public names, the
same four machine ports. MongoDB, the bucket and every credential arrive
as mesh grants instead of hand-set environment; SUPER_ADMIN_KEY stops
being a default written in code and becomes a minted secret.
2026-09-25 20:50:19 +02:00

81 lines
3.1 KiB
JavaScript

// The client sites' server: static files, an SPA fallback, and env-config.js from the
// environment. Node built-ins only — the runtime base already carries node for the API, so a
// second web server (and the shell its startup script needed) would be two more moving parts to
// serve files the first one serves fine.
//
// What the nginx arrangement this replaces did, it does: every unknown path without an extension
// falls back to index.html (client-side routing), nothing is cached (the site is tiny and a stale
// index.html after a deploy is the only caching bug this app has ever had), and env-config.js is
// regenerated from the container's environment at start — which is how one built image serves
// under any API URL.
import { createServer } from "node:http";
import { readFileSync, writeFileSync, createReadStream, statSync } from "node:fs";
import { join, resolve, extname } from "node:path";
const root = resolve(process.env.HTML_ROOT ?? "/site");
const port = Number(process.env.PORT ?? 80);
// .env names the variables the site reads; the environment overrides their values. Written once
// at start, exactly as the env.sh this replaces did.
const pairs = [];
for (const line of readFileSync(join(root, ".env"), "utf8").split("\n")) {
const at = line.indexOf("=");
if (at < 1) continue;
const name = line.slice(0, at).trim();
pairs.push(` ${name}: ${JSON.stringify(process.env[name] ?? line.slice(at + 1).trim())},`);
}
writeFileSync(join(root, "env-config.js"), `window._env_ = {\n${pairs.join("\n")}\n}\n`);
const types = {
".html": "text/html; charset=utf-8",
".js": "application/javascript",
".mjs": "application/javascript",
".css": "text/css",
".json": "application/json",
".map": "application/json",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".ico": "image/x-icon",
".txt": "text/plain; charset=utf-8",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
};
createServer((request, response) => {
const asked = decodeURIComponent(new URL(request.url, "http://x").pathname);
// resolve() collapses any ".." before the prefix check, so a path cannot escape the root.
let path = resolve(join(root, asked));
if (!path.startsWith(root)) {
response.writeHead(403).end();
return;
}
let served;
try {
served = statSync(path);
if (served.isDirectory()) {
path = join(path, "index.html");
served = statSync(path);
}
} catch {
// Not a file: a client-side route, answered by the app itself — unless it asked for a file
// by extension, where index.html would be a wrong answer dressed as a right one.
if (extname(asked) !== "") {
response.writeHead(404, { "Content-Type": "text/plain" }).end("not found\n");
return;
}
path = join(root, "index.html");
served = statSync(path);
}
response.writeHead(200, {
"Content-Type": types[extname(path)] ?? "application/octet-stream",
"Content-Length": served.size,
"Cache-Control": "no-cache",
});
createReadStream(path).pipe(response);
}).listen(port, () => console.log(`serving ${root} on :${port}`));