152 lines
4.3 KiB
JavaScript
152 lines
4.3 KiB
JavaScript
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}/`);
|
|
});
|