import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { Readable } from "node:stream"; import express from "express"; const root = path.dirname(fileURLToPath(import.meta.url)); const isProd = process.argv.includes("--prod") || process.env.NODE_ENV === "production"; const port = Number(process.env.PORT ?? 5173); const backendOrigin = process.env.BACKEND_ORIGIN ?? "http://localhost:3000"; const app = express(); // Liveness only, so it must not touch the backend: the container healthcheck // reads this, and an unhealthy container is dropped by Traefik's Docker // provider - which would take the router, and with it the certificate, down. app.get("/healthz", (req, res) => res.status(200).type("text/plain").end("ok")); /** @type {import("vite").ViteDevServer | undefined} */ let vite; if (isProd) { // Built assets, plus the same /api -> backend rewrite that vite.config.ts // sets up for dev, so the browser talks to one origin either way. app.use(express.static(path.join(root, "dist/client"), { index: false })); app.use("/api", proxyToBackend); } else { const { createServer } = await import("vite"); vite = await createServer({ root, appType: "custom", server: { middlewareMode: true }, }); // Includes Vite's own /api proxy, asset serving and HMR. app.use(vite.middlewares); } const prodTemplate = isProd ? await fs.readFile(path.join(root, "dist/client/index.html"), "utf-8") : null; app.use(async (req, res, next) => { if (req.method !== "GET" && req.method !== "HEAD") return next(); const url = req.originalUrl; try { let template; let entry; if (vite) { template = await vite.transformIndexHtml( url, await fs.readFile(path.join(root, "index.html"), "utf-8"), ); entry = await vite.ssrLoadModule("/src/entry-server.tsx"); } else { template = prodTemplate; entry = await import( pathToFileURL(path.join(root, "dist/server/entry-server.js")).href ); } const result = await entry.render(url, (apiPath) => fetch(new URL(apiPath, backendOrigin), { headers: req.headers.cookie ? { cookie: req.headers.cookie } : {}, }), ); const status = result.kind === "not-found" ? 404 : 200; const html = result.kind === "ssr" ? fillTemplate(template, result.head, result.html, result.data) : fillTemplate(template, entry.defaultHead, "", null); res.status(status).set({ "Content-Type": "text/html" }).end(html); } catch (e) { vite?.ssrFixStacktrace(e); next(e); } }); app.listen(port, () => { console.log( `${isProd ? "Production" : "Dev"} server listening on http://localhost:${port}`, ); }); /** * Fills the SSR placeholders in index.html. `markup` is empty and `data` null * for routes that aren't server-rendered, which leaves the original * client-only shell. */ function fillTemplate(template, head, markup, data) { const headTags = [ `${escapeHtml(head.title)}`, ``, ``, ].join("\n "); const dataTag = data ? `` : ""; return template .replace("", headTags) .replace("", markup) .replace("", dataTag); } function escapeHtml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } /** JSON for inline