// Dev server: rebuilds the inlined bundle on every request so you just refresh // the browser to see changes. Same output as `bun run build`, unminified. // // It also proxies the regtest backends (which bind to localhost on this machine) // through the same origin, so the app works unchanged whether you load it on // localhost or from a phone over the LAN/Tailscale — no per-device config, no // CORS. HTTP backends by path prefix; WebSocket backends by exact path. import { startFeedCache } from './feed/service.js'; import { snapshotResponse } from './feed/cache.js'; const publicFeedCache = await startFeedCache(); import { buildHtml, buildJsQr, buildNip46, buildVerifyWorker } from './build.js'; const port = Number(process.env.PORT || 5173); const HTTP_PROXY = { '/boltz': 'http://localhost:9001', '/esplora': 'http://localhost:3000', '/sp': 'http://localhost:8888' }; const WS_PROXY = { '/electrum': 'ws://localhost:50003' }; const JS_ASSETS = { '/jsqr.js': buildJsQr, '/nip46.js': buildNip46, '/verify-worker.js': buildVerifyWorker }; const CORS = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,POST,OPTIONS', 'access-control-allow-headers': 'content-type' }; Bun.serve({ port, hostname: '0.0.0.0', // listen on all interfaces so the LAN/Tailscale can reach it async fetch(req, server) { const NO_STORE = 'no-store, must-revalidate'; try { const url = new URL(req.url); const path = url.pathname; if (path === '/api/feed') return snapshotResponse(publicFeedCache, req); // WebSocket proxies (Fulcrum electrum, SP-indexer push) — upgrade + relay. if (WS_PROXY[path]) { if (server.upgrade(req, { data: { target: WS_PROXY[path] } })) return; return new Response('expected websocket', { status: 426 }); } // HTTP proxies (boltz, esplora, SP indexer) — same-origin, CORS for cross-origin loads. for (const prefix in HTTP_PROXY) { if (path === prefix || path.startsWith(prefix + '/')) { if (req.method === 'OPTIONS') return new Response(null, { status: 204, headers: CORS }); const target = HTTP_PROXY[prefix] + path.slice(prefix.length) + url.search; const r = await fetch(target, { method: req.method, headers: req.headers.get('content-type') ? { 'content-type': req.headers.get('content-type') } : {}, body: ['GET', 'HEAD'].includes(req.method) ? undefined : await req.arrayBuffer(), }); return new Response(await r.arrayBuffer(), { status: r.status, headers: { 'content-type': r.headers.get('content-type') || 'application/json', ...CORS } }); } } if (Object.hasOwn(JS_ASSETS, path)) { return new Response(await JS_ASSETS[path]({ minify: false }), { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': NO_STORE }, }); } if (/^\/(punks|punks-sm)\//.test(path)) { // These are bundled images, not app routes. Returning index.html here // produces broken avatars and rebuilds the app for every face. if (!/^\/(punks|punks-sm)\/([1-9]|[1-5][0-9]|6[0-4])\.webp$/.test(path)) { return new Response('Not found', { status: 404 }); } const file = Bun.file('static' + path); return await file.exists() ? new Response(file, { headers: { 'content-type': 'image/webp', 'cache-control': NO_STORE } }) : new Response('Not found', { status: 404 }); } if (/^\/icon(-maskable)?(-\d+)?\.(png|svg)$/.test(path)) { const file = Bun.file('static' + path); if (await file.exists()) return new Response(file, { headers: { 'cache-control': NO_STORE } }); } const loc = path.match(/^\/locales\/([a-z]{2})\.json$/); if (loc) { const f = Bun.file('src/locales/' + loc[1] + '.json'); return (await f.exists()) ? new Response(f, { headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': NO_STORE } }) : new Response('{}', { status: 404, headers: { 'content-type': 'application/json', 'cache-control': NO_STORE } }); } const html = await buildHtml({ minify: false }); return new Response(html, { headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': NO_STORE }, }); } catch (e) { return new Response(`Build error:\n\n${e.stack || e}`, { status: 500, headers: { 'content-type': 'text/plain; charset=utf-8' }, }); } }, // Relay each browser WebSocket to its localhost backend, buffering until the // upstream connection opens. websocket: { open(ws) { const up = new WebSocket(ws.data.target); ws.data.up = up; ws.data.q = []; up.onopen = () => { const q = ws.data.q; ws.data.q = null; for (const m of q) up.send(m); }; up.onmessage = (e) => { try { ws.send(e.data); } catch {} }; up.onclose = () => { try { ws.close(); } catch {} }; up.onerror = () => { try { ws.close(); } catch {} }; }, message(ws, msg) { const up = ws.data.up; if (!up) return; if (up.readyState === 1) up.send(msg); else if (ws.data.q) ws.data.q.push(msg); }, close(ws) { try { ws.data.up && ws.data.up.close(); } catch {} }, }, }); console.log(`dev server → http://localhost:${port} (also on 0.0.0.0:${port} for LAN/Tailscale)`); > 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164