#!/usr/bin/env node // A static server for site/ as Vercel serves it (clean URLs, the two rewrites of site/vercel.json), with /api/* passed through to // the live site so the chain scene paints from the real feed. For captures and the overlap sweep on a build box; never a deploy. // node tools/site/serve.mjs [port] [site-dir] [api-origin] default 4500, ../../site, https://igneum.network import http from 'node:http'; import https from 'node:https'; import { readFileSync, existsSync, statSync } from 'node:fs'; import { join, extname, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const port = Number(process.argv[2] || 4500); const site = process.argv[3] || join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'site'); const api = process.argv[4] || 'https://igneum.network'; const TYPES = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.png': 'image/png', '.ico': 'image/x-icon', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.woff2': 'font/woff2', '.webmanifest': 'application/manifest+json', '.xml': 'application/xml', '.txt': 'text/plain' }; function file(p) { try { return existsSync(p) && statSync(p).isFile() ? p : null; } catch (e) { return null; } } http.createServer((req, res) => { const u = new URL(req.url, 'http://x'); if (u.pathname.startsWith('/api/')) { // an http: origin (or one that refuses) answers 502 so the pages render their pending state; the server itself never dies const fail = () => { if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' }); res.end('{"ok":false,"error":"upstream"}'); }; try { const mod = api.startsWith('http:') ? http : https; const r = mod.request(api + u.pathname + u.search, { method: 'GET', headers: { accept: 'application/json' } }, pr => { res.writeHead(pr.statusCode || 502, { 'content-type': pr.headers['content-type'] || 'application/json', 'cache-control': 'no-store' }); pr.pipe(res); }); r.on('error', fail); r.end(); } catch (e) { fail(); } return; } let p = u.pathname.replace(/\/+$/, '') || '/'; if (/^\/block\/[^/]+$/.test(p)) p = '/block'; else if (/^\/address\/[^/]+$/.test(p)) p = '/address'; else if (p === '/benchmarks') p = '/miners'; const rel = p === '/' ? 'index.html' : p.slice(1); const hit = file(join(site, rel)) || file(join(site, rel + '.html')) || file(join(site, rel, 'index.html')); if (!hit) { res.writeHead(404, { 'content-type': 'text/html; charset=utf-8' }); res.end(readFileSync(join(site, '404.html'))); return; } res.writeHead(200, { 'content-type': TYPES[extname(hit)] || 'application/octet-stream', 'cache-control': 'no-store' }); res.end(readFileSync(hit)); }).listen(port, '127.0.0.1', () => console.log('site at http://127.0.0.1:' + port + '/ from ' + site + ', /api from ' + api));