From c0382117b807295844c7d3efea4ed004f2a41101 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Mon, 11 May 2026 10:46:08 +0200 Subject: [PATCH] ui: read dev-server config from .env files and add VITE_DEV_HOST opt-in `vite.config.ts` read `VITE_DEV_PROXY_TARGET` / `VITE_DEV_GRPC_PROXY_TARGET` straight from `process.env`, so the gateway-override knob only worked when the variable was exported in the shell that ran `pnpm dev`. Per-developer `.env.development.local` files (the documented way to override) were silently ignored by the config: Vite auto-populates `import.meta.env` for client code from those files, but the config itself runs in Node and has to call `loadEnv` explicitly. Switch the config to the function-form + `loadEnv` so every `VITE_*` entry in any `.env*` file reaches both client code and the config. Now adding `VITE_DEV_PROXY_TARGET=http://localhost:18080` to `.env.development.local` actually retargets the proxy, no shell gymnastics required. While there, introduce `VITE_DEV_HOST` as an opt-in for wider listener binding: unset (default) keeps Vite's loopback-only behaviour; `true`/`1`/`yes` flips to "all interfaces" (`0.0.0.0` + IPv6); any other string is passed through verbatim to pin a specific LAN address. Useful when reaching the dev server through SSH port forwarding, a VM, or a container needs a non-loopback bind, and intentionally opt-in so an unattended `pnpm dev` on a laptop never exposes the unauthenticated dev surface to the LAN by accident. Co-Authored-By: Claude Opus 4.7 --- ui/frontend/vite.config.ts | 115 +++++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 37 deletions(-) diff --git a/ui/frontend/vite.config.ts b/ui/frontend/vite.config.ts index 8fd91fa..21b438f 100644 --- a/ui/frontend/vite.config.ts +++ b/ui/frontend/vite.config.ts @@ -1,5 +1,5 @@ import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig } from "vite"; +import { defineConfig, loadEnv } from "vite"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; @@ -10,43 +10,84 @@ const pkg = JSON.parse( ), ) as { version: string }; -// Default upstream gateway addresses used by the dev proxy. Override -// by pointing `VITE_DEV_PROXY_TARGET` (REST surface) and -// `VITE_DEV_GRPC_PROXY_TARGET` (Connect-Web surface) at a different -// gateway when working with a remote stack instead of -// `tools/local-dev/`. In production the two surfaces sit behind a -// single host; the split here exists only because local-dev runs the -// REST listener on :8080 and the authenticated Connect-Web listener -// on :9090. -const DEV_PROXY_TARGET = - process.env.VITE_DEV_PROXY_TARGET ?? "http://localhost:8080"; -const DEV_GRPC_PROXY_TARGET = - process.env.VITE_DEV_GRPC_PROXY_TARGET ?? "http://localhost:9090"; +// Parse the VITE_DEV_HOST override into the shape `server.host` +// expects. `""` / `"false"` keeps Vite's safe default (loopback +// only); `"true"` / `"1"` flips to "all interfaces" (`0.0.0.0` plus +// IPv6); any other string is passed through verbatim so a developer +// can pin a single LAN address (e.g. `"192.168.1.5"`). Returning +// `undefined` lets Vite stay on its built-in default. +function parseDevHost(raw: string | undefined): string | boolean | undefined { + if (raw === undefined || raw === "") return undefined; + const normalised = raw.toLowerCase(); + if (normalised === "true" || normalised === "1" || normalised === "yes") { + return true; + } + if (normalised === "false" || normalised === "0" || normalised === "no") { + return false; + } + return raw; +} -export default defineConfig({ - plugins: [sveltekit()], - define: { - __APP_VERSION__: JSON.stringify(pkg.version), - }, - server: { - // Same-origin proxy so the browser sees only `localhost:5173` - // and never trips a cross-origin preflight against the - // gateway's REST + Connect-Web surfaces. Production deployments - // serve the UI and the gateway behind a single host, so the - // proxy is purely a dev-time convenience. - proxy: { - "/api": { - target: DEV_PROXY_TARGET, - changeOrigin: false, - }, - "/galaxy.gateway.v1.EdgeGateway": { - target: DEV_GRPC_PROXY_TARGET, - changeOrigin: false, - // Connect-Web server-streaming (`SubscribeEvents`) uses - // chunked HTTP responses; http-proxy passes them through - // transparently as long as buffering stays off, which is - // the default. +export default defineConfig(({ mode }) => { + // `loadEnv("", ...)` matches every `.env*` entry regardless of + // the customary `VITE_` prefix so the config sees the same view + // that client code sees via `import.meta.env`. Without this + // `process.env` would carry only the shell's exports, and + // per-developer files like `.env.development.local` would + // silently miss the config — every override would have to be + // passed on the `pnpm dev` command line. + const env = loadEnv(mode, process.cwd(), ""); + + // Default upstream gateway addresses used by the dev proxy. + // Override by pointing `VITE_DEV_PROXY_TARGET` (REST surface) + // and `VITE_DEV_GRPC_PROXY_TARGET` (Connect-Web surface) at a + // different gateway when working with a remote stack instead of + // `tools/local-dev/`. In production the two surfaces sit behind + // a single host; the split here exists only because local-dev + // runs the REST listener on :8080 and the authenticated + // Connect-Web listener on :9090. + const DEV_PROXY_TARGET = + env.VITE_DEV_PROXY_TARGET || "http://localhost:8080"; + const DEV_GRPC_PROXY_TARGET = + env.VITE_DEV_GRPC_PROXY_TARGET || "http://localhost:9090"; + + // `VITE_DEV_HOST` opts the dev server into wider listener + // binding. Default stays at Vite's safe loopback-only behaviour + // so an unattended `pnpm dev` on someone's laptop never exposes + // the unauthenticated dev surface to the LAN by accident. Set + // the value in `.env.development.local` (untracked) when + // reaching the server through SSH port forwarding, a VM, or a + // container needs a non-loopback bind. + const devHost = parseDevHost(env.VITE_DEV_HOST); + + return { + plugins: [sveltekit()], + define: { + __APP_VERSION__: JSON.stringify(pkg.version), + }, + server: { + ...(devHost !== undefined ? { host: devHost } : {}), + // Same-origin proxy so the browser sees only + // `localhost:5173` and never trips a cross-origin + // preflight against the gateway's REST + Connect-Web + // surfaces. Production deployments serve the UI and the + // gateway behind a single host, so the proxy is purely a + // dev-time convenience. + proxy: { + "/api": { + target: DEV_PROXY_TARGET, + changeOrigin: false, + }, + "/galaxy.gateway.v1.EdgeGateway": { + target: DEV_GRPC_PROXY_TARGET, + changeOrigin: false, + // Connect-Web server-streaming + // (`SubscribeEvents`) uses chunked HTTP + // responses; http-proxy passes them through + // transparently as long as buffering stays off, + // which is the default. + }, }, }, - }, + }; });