import { createServer } from "node:http"; import { readFile, stat, writeFile } from "node:fs/promises"; import { extname, join, normalize } from "node:path"; import { fileURLToPath } from "node:url"; import { spawn } from "node:child_process"; import { dirname } from "node:path"; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const port = Number(process.env.PORT || 8090); const host = process.env.HOST || "127.0.0.1"; const pagePath = join(repoRoot, "content", "pages", "home.json"); const MIME = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".avif": "image/avif", ".webp": "image/webp", ".mp4": "video/mp4", ".glb": "model/gltf-binary", }; function send(res, status, body, contentType = "text/plain; charset=utf-8") { res.writeHead(status, { "content-type": contentType, "cache-control": "no-store", }); res.end(body); } function sendJson(res, status, payload) { send(res, status, `${JSON.stringify(payload, null, 2)}\n`, "application/json; charset=utf-8"); } async function readBody(req) { const chunks = []; for await (const chunk of req) { chunks.push(chunk); } return Buffer.concat(chunks).toString("utf8"); } function runNodeScript(scriptName) { return new Promise((resolve, reject) => { const child = spawn(process.execPath, [join(repoRoot, "tools", scriptName)], { cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => { stdout += chunk.toString(); }); child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); child.on("error", reject); child.on("close", (code) => { if (code === 0) { resolve({ stdout, stderr }); } else { reject(new Error(stderr || stdout || `${scriptName} exited with ${code}`)); } }); }); } function safePublicPath(urlPath) { const withoutQuery = decodeURIComponent(urlPath.split("?")[0]); const requestPath = withoutQuery === "/" ? "/index.html" : withoutQuery === "/admin" || withoutQuery === "/admin/" ? "/admin/index.html" : withoutQuery; const normalized = normalize(requestPath).replace(/^(\.\.[/\\])+/, ""); const absolute = join(repoRoot, normalized); if (!absolute.startsWith(repoRoot)) { return null; } return absolute; } async function serveStatic(req, res) { const filePath = safePublicPath(new URL(req.url, `http://${host}:${port}`).pathname); if (!filePath) { send(res, 403, "Forbidden"); return; } try { const fileStat = await stat(filePath); if (!fileStat.isFile()) { send(res, 404, "Not found"); return; } const ext = extname(filePath); send(res, 200, await readFile(filePath), MIME[ext] || "application/octet-stream"); } catch { send(res, 404, "Not found"); } } const server = createServer(async (req, res) => { try { const url = new URL(req.url, `http://${host}:${port}`); if (req.method === "GET" && url.pathname === "/api/page/home") { send(res, 200, await readFile(pagePath, "utf8"), "application/json; charset=utf-8"); return; } if (req.method === "PUT" && url.pathname === "/api/page/home") { const body = await readBody(req); const parsed = JSON.parse(body); await writeFile(pagePath, `${JSON.stringify(parsed, null, 2)}\n`); sendJson(res, 200, { ok: true }); return; } if (req.method === "POST" && url.pathname === "/api/render/home") { const result = await runNodeScript("render-home.mjs"); sendJson(res, 200, { ok: true, ...result }); return; } if (req.method === "POST" && url.pathname === "/api/extract/home") { const result = await runNodeScript("extract-home-blocks.mjs"); sendJson(res, 200, { ok: true, ...result }); return; } await serveStatic(req, res); } catch (error) { sendJson(res, 500, { ok: false, error: error.message }); } }); server.listen(port, host, () => { console.log(`NODE.DC admin: http://${host}:${port}/admin/`); console.log(`NODE.DC site: http://${host}:${port}/`); });