Fed by /api/explorer from the observer's tables. Latest blocks (hash, number, DAA, blue score, miner, txs, proof records, time); a block's header, parents, children, mergeset, coinbase outputs, EVM transactions, shards, checkpoint and certificate; an address's blocks, what they earned, vote keys and balance (eth_getBalance when EXPLORER_EVM_RPC is set). Same tokens as live.html. vercel.json rewrites /block/:id and /address/:addr; the footer links the explorer; the link checker skips template literals and resolves /api/<name> to its function. node tools/site-serve.mjs previews the site with the functions in-process. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
44 lines
3.4 KiB
JavaScript
44 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Local preview of the site with its API functions: serves site/ the way Vercel does (clean URLs, the rewrites and
|
|
// headers of site/vercel.json) and runs site/api/*.mjs in-process with a small req/res shim. DATABASE_URL comes from
|
|
// the environment or ~/.config/igneum/env, like the observer. Options through the environment:
|
|
// PORT (default 4173), LIVE_TABLE_PREFIX (a test observer's tables), EXPLORER_EVM_RPC (an EVM JSON-RPC for balances)
|
|
// node tools/site-serve.mjs
|
|
import { createServer } from 'node:http';
|
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
import { homedir } from 'node:os';
|
|
import { join, dirname, extname } from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
|
const site = join(dirname(fileURLToPath(import.meta.url)), '..', 'site');
|
|
const PORT = Number(process.env.PORT || 4173);
|
|
if (!process.env.DATABASE_URL) {
|
|
try { const m = /^DATABASE_URL=(.*)$/m.exec(readFileSync(`${homedir()}/.config/igneum/env`, 'utf8')); if (m) process.env.DATABASE_URL = m[1].trim().replace(/^['"]|['"]$/g, ''); } catch { }
|
|
}
|
|
const cfg = JSON.parse(readFileSync(join(site, 'vercel.json'), 'utf8'));
|
|
const rewrites = (cfg.rewrites || []).map(r => ({ re: new RegExp('^' + r.source.replace(/:[a-z]+/g, '[^/]+') + '$'), to: r.destination }));
|
|
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', '.woff2': 'font/woff2', '.webp': 'image/webp', '.txt': 'text/plain', '.xml': 'application/xml', '.webmanifest': 'application/manifest+json' };
|
|
const handlers = new Map();
|
|
async function api(name) {
|
|
if (!handlers.has(name)) handlers.set(name, (await import(pathToFileURL(join(site, 'api', name + '.mjs')).href)).default);
|
|
return handlers.get(name);
|
|
}
|
|
function shim(res) {
|
|
const r = { status(c) { res.statusCode = c; return r; }, setHeader: (k, v) => res.setHeader(k, v), json(o) { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(o)); }, end: (b) => res.end(b) };
|
|
return r;
|
|
}
|
|
createServer(async (req, res) => {
|
|
const url = new URL(req.url, 'http://x'); let p = decodeURIComponent(url.pathname);
|
|
for (const rw of rewrites) if (rw.re.test(p)) { p = rw.to; break; }
|
|
if (p.startsWith('/api/')) {
|
|
const name = p.slice(5).replace(/[^a-z0-9_-]/gi, '');
|
|
if (!existsSync(join(site, 'api', name + '.mjs'))) { res.statusCode = 404; return res.end('no such function'); }
|
|
try { const h = await api(name); return await h({ method: req.method, url: req.url, headers: req.headers, query: Object.fromEntries(url.searchParams), socket: req.socket }, shim(res)); }
|
|
catch (e) { res.statusCode = 500; return res.end(String(e.stack || e)); }
|
|
}
|
|
const candidates = [join(site, p), join(site, p + '.html'), join(site, p, 'index.html')];
|
|
const file = candidates.find(f => existsSync(f) && statSync(f).isFile());
|
|
if (!file || !file.startsWith(site)) { res.statusCode = 404; res.setHeader('content-type', 'text/html; charset=utf-8'); return res.end(readFileSync(join(site, '404.html'))); }
|
|
res.setHeader('content-type', TYPES[extname(file)] || 'application/octet-stream');
|
|
res.end(readFileSync(file));
|
|
}).listen(PORT, '127.0.0.1', () => console.log(`site preview on http://127.0.0.1:${PORT} (prefix '${process.env.LIVE_TABLE_PREFIX || ''}', evm rpc ${process.env.EXPLORER_EVM_RPC || 'none'})`));
|