Files
krow_employer/vite.config.js
2026-09-07 20:36:18 +05:30

142 lines
6.0 KiB
JavaScript

import path from 'node:path';
import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';
// The `@/*` alias is declared directly rather than through a plugin.
// The API host the dev server forwards to. Only read here; nothing in client
// code names a backend host.
//
// It has to come from `loadEnv`, not `process.env`. Vite reads `.env` files for
// *client* code and exposes them on `import.meta.env`; it never copies them into
// `process.env`, and this config file runs in Node before any of that happens.
// So `process.env.VITE_API_PROXY_TARGET` was always undefined unless the
// variable was exported in the shell, and the line in `.env` documenting this
// knob did nothing at all — every dev server silently used the hard-coded
// fallback no matter what `.env` said. `loadEnv(mode, cwd, '')` reads the same
// files Vite would, with no prefix filter, so the `.env` value now actually
// takes effect.
function proxyTargetFor(mode) {
const env = loadEnv(mode, process.cwd(), '');
return env.VITE_API_PROXY_TARGET || 'https://mcp.krowforce.com';
}
/**
* One proxy entry per path prefix, all pointing at the same backend.
*
* `secure: true` keeps certificate verification on — the target is a real host
* with a real certificate, and turning verification off would hide a genuine
* TLS failure behind a working-looking proxy.
*
* `changeOrigin` rewrites the Host header to the target, which the backend's
* TLS termination requires to route the request at all.
*/
function proxyRoutes(prefixes, PROXY_TARGET) {
return Object.fromEntries(prefixes.map((prefix) => [prefix, {
target: PROXY_TARGET,
changeOrigin: true,
secure: true,
configure: (proxy) => {
proxy.on('error', (err, req, res) => {
// ECONNREFUSED here means there is no server on the other end at all,
// which is a different situation from a backend that answered badly.
// Say so, because the browser only ever shows "502 Bad Gateway" and
// that phrasing sends people looking at the backend's *response*.
const refused = err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET';
if (refused) {
console.error(
`\n[vite proxy] ${req?.method || ''} ${req?.url || ''} -> 502: nothing is ` +
`listening on ${PROXY_TARGET}.\n` +
' The API is a separate process. Start it with:\n' +
' cd <krow-backend> && make run\n' +
" `npm run dev` normally starts it for you (scripts/dev.mjs); " +
'this path means it exited mid-session.\n',
);
} else {
console.error('[vite proxy error]:', err.code || '', err.message);
}
if (res && res.writeHead && !res.headersSent) {
res.writeHead(502, { 'Content-Type': 'application/json' });
// Shaped as the API's own error envelope — {error:{code,message,details}}.
// src/api/httpClient.js reads `payload.error.message`, so a bare string
// here was silently dropped and the UI fell back to a generic
// "Request failed with status 502" that named neither the cause nor
// the fix.
res.end(JSON.stringify({
error: {
code: refused ? 'api_not_running' : 'bad_gateway',
message: refused
? `The API is not running. Nothing is listening on ${PROXY_TARGET}.`
: `Could not reach the API at ${PROXY_TARGET}: ${err.message}`,
details: { target: PROXY_TARGET, cause: err.code || err.message },
},
}));
}
});
},
}]));
}
export default defineConfig(({ mode }) => {
const PROXY_TARGET = proxyTargetFor(mode);
console.log(`[vite] proxying /api and /health to ${PROXY_TARGET}`);
return {
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(import.meta.dirname, './src'),
},
},
build: {
rollupOptions: {
output: {
// Split the heavy libraries out of the app bundle. They change far less
// often than app code, so browsers keep them cached across deploys.
manualChunks: {
react: ['react', 'react-dom', 'react-router-dom'],
charts: ['recharts'],
motion: ['framer-motion'],
mui: ['@mui/x-charts'],
},
},
},
},
server: {
// Expose server over network (0.0.0.0)
host: true,
// Honor a port assigned by the environment; fall back to Vite's default.
port: process.env.PORT ? Number(process.env.PORT) : 5173,
/**
* Proxy the API so the browser only ever talks to one origin.
*
* This exists for the session cookie. The cookie is HttpOnly with
* SameSite=Lax, and a Lax cookie is not sent on a cross-site subresource
* request — which is exactly what `fetch('http://127.0.0.1:8080/...')` from
* a page served by `http://localhost:5173` is, because a browser treats
* those two as different sites. Every request after login would arrive
* without the cookie and be answered 401.
*
* The alternatives are both worse. SameSite=None would send the cookie on
* genuine cross-site requests too, which is the CSRF hole Lax closes, and
* it requires Secure — so it cannot work over plain HTTP on localhost at
* all. Widening CORS with credentials would keep the cross-site problem and
* add a second one.
*
* browser → localhost:5173/api/v1 → this proxy → 127.0.0.1:8080/api/v1
*
* Same origin from the browser's point of view, so the cookie is a
* first-party cookie, CORS never enters into it, and nothing in the React
* code names a backend host.
*
* Production serves the built assets and the API from one origin (see
* nginx.conf), so this is a development-only shim for a property the
* deployed app has for free.
*/
proxy: proxyRoutes(['/api', '/health'], PROXY_TARGET),
},
};
});