diff --git a/vite.config.js b/vite.config.js index b220fb8..7e6c703 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,47 +1,92 @@ -import { defineConfig } from 'vite' +import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' /** - * Dev-only stub for /api/* requests. + * Dev runner for /api/* requests in `npm run dev`. * - * The files in /api are Vercel serverless functions — they run only on Vercel, - * not in the local Vite dev server. When the frontend polls an endpoint like - * /api/hail-status during local dev, Vite would otherwise resolve that URL to - * the on-disk api/*.js file and push it through its module-transform pipeline, - * which crashes on server-only imports such as `@vercel/kv`. + * The files in /api are Vercel serverless functions — on Vercel they run as + * real functions; locally Vite doesn't execute them. To test the real APIs + * locally (e.g. the Tomorrow.io forecast) this middleware loads .env into + * process.env and invokes the ACTUAL handler for KV-free routes via a small + * req/res shim that matches the Vercel handler contract. * - * This middleware intercepts /api/* before Vite transforms anything and returns - * a 503. Every frontend hook that hits these endpoints already degrades - * gracefully on a failed fetch (falls back to mock data / inactive storm state), - * so the app behaves correctly in local dev — just without the live API. + * Routes that use server-only deps (`@vercel/kv`: hail-status, hail-webhook) + * stay stubbed with a 503 — every frontend hook that hits these already + * degrades gracefully on a failed fetch. */ -const devApiStub = { - name: 'dev-api-stub', - apply: 'serve', - configureServer(server) { - server.middlewares.use((req, res, next) => { - if (req.url && req.url.startsWith('/api/')) { - res.statusCode = 503 - res.setHeader('Content-Type', 'application/json') - res.end(JSON.stringify({ - error: 'API routes run only on Vercel and are unavailable in local dev.', - })) - return - } - next() - }) - }, + +// Only KV-free handlers are safe to import into the dev pipeline. +const DEV_API_HANDLERS = { + 'storm-forecast': () => import('./api/storm-forecast.js'), + 'hail-proxy': () => import('./api/hail-proxy.js'), + 'storm-events': () => import('./api/storm-events.js'), + 'storm-polygons': () => import('./api/storm-polygons.js'), } -export default defineConfig({ - plugins: [react(), devApiStub], - server: { - // Don't watch or serve /api — those are Vercel serverless functions, not frontend code - watch: { ignored: ['**/api/**'] }, - fs: { deny: ['api'] }, - }, - optimizeDeps: { - // Don't pre-bundle serverless function files - exclude: ['api'], - }, +// Server-side env vars the handlers read (mirrors what you'd set in Vercel). +const SERVER_ENV_KEYS = [ + 'TOMORROW_API_KEY', + 'HAIL_RECON_ACCESS_KEY', + 'HAIL_RECON_ACCESS_SECRET', + 'HAIL_RECON_WEBHOOK_SECRET', +] + +function devApi(env) { + for (const k of SERVER_ENV_KEYS) { + if (env[k] && !process.env[k]) process.env[k] = env[k] + } + return { + name: 'dev-api', + apply: 'serve', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + if (!req.url || !req.url.startsWith('/api/')) return next() + + const url = new URL(req.url, 'http://localhost') + const name = url.pathname.replace(/^\/api\//, '').replace(/\/+$/, '') + const load = DEV_API_HANDLERS[name] + + if (!load) { + res.statusCode = 503 + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ error: 'API route not available in local dev (needs Vercel).' })) + return + } + + // Shim the Node req/res to the Vercel handler contract. + req.query = Object.fromEntries(url.searchParams.entries()) + res.status = (code) => { res.statusCode = code; return res } + res.json = (obj) => { + if (!res.getHeader('Content-Type')) res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify(obj)) + return res + } + + try { + const mod = await load() + await mod.default(req, res) + } catch (e) { + res.statusCode = 500 + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify({ error: 'dev api handler failed', detail: e.message })) + } + }) + }, + } +} + +export default defineConfig(({ mode }) => { + // Empty prefix → load ALL vars from .env (including non-VITE_ server keys). + const env = loadEnv(mode, process.cwd(), '') + return { + plugins: [react(), devApi(env)], + server: { + // Don't watch/serve /api as client files — they're invoked server-side above. + watch: { ignored: ['**/api/**'] }, + fs: { deny: ['api'] }, + }, + optimizeDeps: { + exclude: ['api'], + }, + } })