8.4 KiB
Task 1.3 — Vite dev proxy + path aliases + tsconfig hardening
Phase: 1 — Foundation
Status: ⬜ Not started
Depends on: 1.2
Wiki refs: docs/wiki/entities/react-spa.md §Auth pattern
Goal
Configure Vite's dev server to proxy /api, /ws-business, /ws-live, and /config.json to the appropriate local services so the SPA dev experience matches the stage/prod reverse-proxy topology. Same-origin in dev means cookies flow correctly, identical to stage/prod where Traefik does the routing. Also: add path aliases (@/) so imports stay short, and tighten tsconfig.json for stricter type safety.
After this task, pnpm dev against a locally-running Directus + (eventually) Processor lets the SPA exercise the real auth flow without CORS workarounds.
Deliverables
vite.config.tsupdated:server.proxyconfigured for:/api→http://localhost:8055(Directus REST/GraphQL)./ws-business→ws://localhost:8055/websocket(Directus business-plane WS), withws: trueandrewriteto strip/ws-business./ws-live→ws://localhost:8081(Processor live WS, once Phase 1.5 lands), withws: trueandrewriteto strip/ws-live.- Don't proxy
/config.json— it's served frompublic/by Vite directly in dev.
resolve.aliasfor@/→src/.server.portpinned to5173(Vite default; mention in README so anyone running other Vite apps avoids the conflict).
tsconfig.app.jsonupdated:compilerOptions.baseUrlto.,pathsto{ "@/*": ["src/*"] }.- Add
noUncheckedIndexedAccess: true(matchestcp-ingestion/processorstrictness). - Add
noImplicitOverride: true. verbatimModuleSyntax: trueis fine if already on; otherwise leave for now.
README.mdupdated: a "local dev" section listing the assumed local services + how to override the proxy targets via env vars (next bullet).- Optional: env-var override for proxy targets:
VITE_DEV_DIRECTUS_URL(defaulthttp://localhost:8055).VITE_DEV_PROCESSOR_WS_URL(defaultws://localhost:8081).- Read in
vite.config.tsvialoadEnv. - Document in
.env.dev.example(committed) and.env.dev.local(gitignored).
Specification
Why proxy in dev instead of CORS
CORS-with-credentials requires:
- The browser sending
withCredentials: trueon every request. - The server responding with
Access-Control-Allow-Origin: <exact-origin>(not*) andAccess-Control-Allow-Credentials: true. - Cookies being scoped correctly (
SameSite=None; Securefor cross-site — which means HTTPS even in dev).
This works but it's three places that must agree, and SameSite=None; Secure requires localhost-with-TLS in dev (extra setup). The dev proxy collapses all of it into "everything is one origin," matching stage/prod's reverse-proxy topology. Pick the dev proxy.
Proxy config shape
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import path from 'node:path';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), 'VITE_');
const directusUrl = env.VITE_DEV_DIRECTUS_URL ?? 'http://localhost:8055';
const processorWsUrl = env.VITE_DEV_PROCESSOR_WS_URL ?? 'ws://localhost:8081';
return {
plugins: [react(), tailwindcss()],
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
server: {
port: 5173,
proxy: {
'/api': {
target: directusUrl,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
'/ws-business': {
target: directusUrl,
ws: true,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/ws-business/, '/websocket'),
},
'/ws-live': {
target: processorWsUrl,
ws: true,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/ws-live/, ''),
},
},
},
};
});
The path namespacing (/api, /ws-business, /ws-live) is what the SPA code calls. The proxy strips the namespace and forwards. Stage/prod's Traefik does the same routing under the public domain — same paths, same code, no env branching in the SPA.
Path alias
@/ is the convention shadcn/ui assumes (its components.json config wires @/components/..., @/lib/utils). Aligning Vite + tsconfig with it means imports look like:
import { Button } from '@/ui/primitives/button';
import { useAuthStore } from '@/auth/store';
instead of ../../../ui/primitives/button. Don't add other top-level aliases — @/ for src/ is enough.
Why noUncheckedIndexedAccess
Without it, arr[0] is typed as T even when the array might be empty. With it, arr[0] is typed T | undefined, forcing a check. Catches a real class of bugs in map-position code (where "first device" assumptions are easy to make and easy to get wrong when the array is empty during reconnect).
The processor and tcp-ingestion both have this on. Match.
Vite proxy and WebSocket behaviour
The ws: true flag on the proxy entry tells Vite (which uses http-proxy-middleware) to forward the upgrade request. Cookies attached to the upgrade carry through transparently — that's the whole point of running same-origin in dev.
One gotcha: the proxy's target for a WS route must use a ws:// or wss:// scheme, not http://. If you accidentally use http://, the upgrade fails silently and the SPA gets a connection error. Document in the README.
Acceptance criteria
pnpm devstarts athttp://localhost:5173; the SPA loads.- With Directus running locally on
:8055,fetch('/api/server/info')from the browser console returns Directus's server info — proxy is forwarding. - WebSocket smoke test:
new WebSocket('ws://localhost:5173/ws-business')connects (returns code 1006 if Directus's WS endpoint is locked down without auth, but the upgrade itself succeeds — that's the test). import { foo } from '@/auth/foo'resolves both at type-check time and at runtime.pnpm typecheckis stricter — addingarr[0].whateverwithout a check should now fail.- No CORS errors in the browser console when calling
/api/...fromhttp://localhost:5173.
Risks / open questions
- Directus's WS path is
/websocket, not/ws. Confirmed by reading Directus 11.x source. The proxyrewriterule reflects this. - Processor WS path is unsettled. processor-ws-contract uses
/processor/wsas illustrative; the actual path will be set by the proxy config in stage/prod (trm/deploy). For dev we assume the Processor binds to:8081with no path prefix, and the proxy adds the/ws-liveprefix on the SPA side. Reconcile if Phase 1.5 lands with a different path convention. - Multiple Vite apps on one machine. Pinning to
:5173collides if another Vite app is running. Document the override (VITE_PORT=5174 pnpm devworks as a one-off; permanent change needs the config edit).
Done
vite.config.ts switched to the function form, reads env via loadEnv(mode, process.cwd(), 'VITE_'), exposes three proxy routes:
/api/*→${VITE_DEV_DIRECTUS_URL}/*(defaulthttp://localhost:8055), strips the/apiprefix./ws-business→ derivedws://...from the Directus URL, rewrites to/websocket./ws-live→${VITE_DEV_PROCESSOR_WS_URL}(defaultws://localhost:8081), strips the/ws-liveprefix.
tsconfig.app.json gains noUncheckedIndexedAccess: true and noImplicitOverride: true. The @/* path alias was already set up in 1.2 (deviation noted there).
.env.example (committed) documents the two proxy override env vars; .env.local (already gitignored via the existing *.local pattern) is the developer's actual values file. README "Local dev" section rewritten with the proxy table and override instructions.
Deviation from spec: the spec called for .env.dev.example / .env.dev.local but Vite's mode for pnpm dev is development (not dev), so mode-specific files would be .env.development.local and Vite wouldn't auto-load .env.dev.local at all. Switched to the standard Vite convention .env.example (committed docs) + .env.local (auto-loaded in any mode, gitignored). Cleaner and matches Vite's actual behaviour.
Smoke check: pnpm typecheck, pnpm lint, pnpm format:check, pnpm build all green. Stricter tsconfig didn't surface any existing issues (current code is small).
Landed in 39b60c9.