feat(dev): run real KV-free /api handlers in vite dev (loads .env) instead of stubbing
This commit is contained in:
+83
-38
@@ -1,47 +1,92 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
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,
|
* The files in /api are Vercel serverless functions — on Vercel they run as
|
||||||
* not in the local Vite dev server. When the frontend polls an endpoint like
|
* real functions; locally Vite doesn't execute them. To test the real APIs
|
||||||
* /api/hail-status during local dev, Vite would otherwise resolve that URL to
|
* locally (e.g. the Tomorrow.io forecast) this middleware loads .env into
|
||||||
* the on-disk api/*.js file and push it through its module-transform pipeline,
|
* process.env and invokes the ACTUAL handler for KV-free routes via a small
|
||||||
* which crashes on server-only imports such as `@vercel/kv`.
|
* req/res shim that matches the Vercel handler contract.
|
||||||
*
|
*
|
||||||
* This middleware intercepts /api/* before Vite transforms anything and returns
|
* Routes that use server-only deps (`@vercel/kv`: hail-status, hail-webhook)
|
||||||
* a 503. Every frontend hook that hits these endpoints already degrades
|
* stay stubbed with a 503 — every frontend hook that hits these already
|
||||||
* gracefully on a failed fetch (falls back to mock data / inactive storm state),
|
* degrades gracefully on a failed fetch.
|
||||||
* so the app behaves correctly in local dev — just without the live API.
|
|
||||||
*/
|
*/
|
||||||
const devApiStub = {
|
|
||||||
name: 'dev-api-stub',
|
// Only KV-free handlers are safe to import into the dev pipeline.
|
||||||
apply: 'serve',
|
const DEV_API_HANDLERS = {
|
||||||
configureServer(server) {
|
'storm-forecast': () => import('./api/storm-forecast.js'),
|
||||||
server.middlewares.use((req, res, next) => {
|
'hail-proxy': () => import('./api/hail-proxy.js'),
|
||||||
if (req.url && req.url.startsWith('/api/')) {
|
'storm-events': () => import('./api/storm-events.js'),
|
||||||
res.statusCode = 503
|
'storm-polygons': () => import('./api/storm-polygons.js'),
|
||||||
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()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
// Server-side env vars the handlers read (mirrors what you'd set in Vercel).
|
||||||
plugins: [react(), devApiStub],
|
const SERVER_ENV_KEYS = [
|
||||||
server: {
|
'TOMORROW_API_KEY',
|
||||||
// Don't watch or serve /api — those are Vercel serverless functions, not frontend code
|
'HAIL_RECON_ACCESS_KEY',
|
||||||
watch: { ignored: ['**/api/**'] },
|
'HAIL_RECON_ACCESS_SECRET',
|
||||||
fs: { deny: ['api'] },
|
'HAIL_RECON_WEBHOOK_SECRET',
|
||||||
},
|
]
|
||||||
optimizeDeps: {
|
|
||||||
// Don't pre-bundle serverless function files
|
function devApi(env) {
|
||||||
exclude: ['api'],
|
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'],
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user