Spaces:
Sleeping
Sleeping
| // Minimal production host for the TanStack Start build. | |
| // | |
| // `vite build` emits a static client (dist/client) and a fetch-style SSR | |
| // handler (dist/server/server.js default export). Neither listens on a port, | |
| // so this tiny Node server does two jobs: serve files out of dist/client, and | |
| // pass everything else through the SSR handler. Listens on PORT (Hugging Face | |
| // Spaces routes to 7860). No framework, no extra dependencies. | |
| import { createServer } from 'node:http' | |
| import { Readable } from 'node:stream' | |
| import { stat } from 'node:fs/promises' | |
| import { createReadStream, readdirSync } from 'node:fs' | |
| import { join, normalize, extname } from 'node:path' | |
| import { fileURLToPath } from 'node:url' | |
| import handler from './dist/server/server.js' | |
| const ROOT = fileURLToPath(new URL('.', import.meta.url)) | |
| const CLIENT_DIR = join(ROOT, 'dist', 'client') | |
| const ASSETS_DIR = join(CLIENT_DIR, 'assets') | |
| const PORT = Number(process.env.PORT) || 3000 | |
| const HOST = process.env.HOST || '0.0.0.0' | |
| // Map "name-prefix" -> emitted CSS filename. Tailwind v4 + the TanStack Start | |
| // SSR build can hash the stylesheet differently between the client and server | |
| // passes, so the SSR HTML injects a CSS href whose hash does not match the file | |
| // the client actually emitted. Since there is exactly one file per prefix | |
| // (styles-*, mail-*), we resolve any missing hashed CSS to the real one. The | |
| // hash is only a cache key, so serving the emitted stylesheet is correct. | |
| // Key by the base NAME (the part before the first hyphen), not the hash. Vite's | |
| // base64url hashes can themselves contain hyphens, so splitting on the last | |
| // hyphen is unreliable; the first token (styles, tetris-loader -> "tetris", | |
| // mail) is stable and unique enough across the emitted stylesheets. | |
| const cssByToken = new Map() | |
| try { | |
| for (const f of readdirSync(ASSETS_DIR)) { | |
| if (f.endsWith('.css')) cssByToken.set(f.split('-')[0], f) | |
| } | |
| } catch (err) { | |
| console.warn('[server] could not index CSS assets:', err.message) | |
| } | |
| const MIME = { | |
| '.js': 'text/javascript', | |
| '.mjs': 'text/javascript', | |
| '.css': 'text/css', | |
| '.html': 'text/html; charset=utf-8', | |
| '.json': 'application/json', | |
| '.geojson': 'application/json', | |
| '.map': 'application/json', | |
| '.svg': 'image/svg+xml', | |
| '.png': 'image/png', | |
| '.jpg': 'image/jpeg', | |
| '.jpeg': 'image/jpeg', | |
| '.gif': 'image/gif', | |
| '.webp': 'image/webp', | |
| '.ico': 'image/x-icon', | |
| '.woff': 'font/woff', | |
| '.woff2': 'font/woff2', | |
| '.ttf': 'font/ttf', | |
| '.wasm': 'application/wasm', | |
| } | |
| // Serve a file from dist/client if it exists. Returns true if it handled the | |
| // request. Guards against path traversal outside the client directory. | |
| async function tryStatic(res, pathname) { | |
| const safe = normalize(decodeURIComponent(pathname)).replace(/^([/\\])+/, '') | |
| const filePath = join(CLIENT_DIR, safe) | |
| if (!filePath.startsWith(CLIENT_DIR)) return false | |
| try { | |
| const info = await stat(filePath) | |
| if (!info.isFile()) return false | |
| res.writeHead(200, { | |
| 'content-type': MIME[extname(filePath)] || 'application/octet-stream', | |
| 'content-length': info.size, | |
| 'cache-control': 'public, max-age=3600', | |
| }) | |
| createReadStream(filePath).pipe(res) | |
| return true | |
| } catch { | |
| return false | |
| } | |
| } | |
| // Build a Web Request from the incoming Node request so the SSR handler (which | |
| // speaks the Fetch API) can process it. | |
| function toWebRequest(req) { | |
| const url = `http://${req.headers.host || 'localhost'}${req.url}` | |
| const headers = new Headers() | |
| for (const [key, value] of Object.entries(req.headers)) { | |
| if (value !== undefined) headers.set(key, Array.isArray(value) ? value.join(', ') : value) | |
| } | |
| const method = req.method || 'GET' | |
| const hasBody = method !== 'GET' && method !== 'HEAD' | |
| return new Request(url, { | |
| method, | |
| headers, | |
| body: hasBody ? Readable.toWeb(req) : undefined, | |
| duplex: hasBody ? 'half' : undefined, | |
| }) | |
| } | |
| const server = createServer(async (req, res) => { | |
| try { | |
| const pathname = (req.url || '/').split('?')[0] | |
| // Static assets first (skip the bare root so it always renders via SSR). | |
| if ((req.method === 'GET' || req.method === 'HEAD') && pathname !== '/') { | |
| if (await tryStatic(res, pathname)) return | |
| // CSS fallback: the SSR build can inject a stylesheet hash that does not | |
| // match the emitted file. Resolve it to the real one by name prefix so the | |
| // page is never served unstyled. | |
| if (pathname.startsWith('/assets/') && pathname.endsWith('.css')) { | |
| const token = pathname.slice('/assets/'.length).split('-')[0] | |
| const actual = cssByToken.get(token) | |
| if (actual && (await tryStatic(res, `/assets/${actual}`))) return | |
| } | |
| } | |
| const response = await handler.fetch(toWebRequest(req)) | |
| res.statusCode = response.status | |
| response.headers.forEach((value, key) => res.setHeader(key, value)) | |
| if (response.body) { | |
| Readable.fromWeb(response.body).pipe(res) | |
| } else { | |
| res.end() | |
| } | |
| } catch (err) { | |
| console.error('[server] request failed:', err) | |
| if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain' }) | |
| res.end('Internal Server Error') | |
| } | |
| }) | |
| server.listen(PORT, HOST, () => { | |
| console.log(`GridLoc is listening on http://${HOST}:${PORT}`) | |
| }) | |