Files
spa/.planning/phase-1-foundation/03-vite-dev-proxy.md
T

150 lines
8.4 KiB
Markdown

# 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.ts` updated:
- `server.proxy` configured for:
- `/api``http://localhost:8055` (Directus REST/GraphQL).
- `/ws-business``ws://localhost:8055/websocket` (Directus business-plane WS), with `ws: true` and `rewrite` to strip `/ws-business`.
- `/ws-live``ws://localhost:8081` (Processor live WS, once Phase 1.5 lands), with `ws: true` and `rewrite` to strip `/ws-live`.
- Don't proxy `/config.json` — it's served from `public/` by Vite directly in dev.
- `resolve.alias` for `@/``src/`.
- `server.port` pinned to `5173` (Vite default; mention in README so anyone running other Vite apps avoids the conflict).
- `tsconfig.app.json` updated:
- `compilerOptions.baseUrl` to `.`, `paths` to `{ "@/*": ["src/*"] }`.
- Add `noUncheckedIndexedAccess: true` (matches `tcp-ingestion` / `processor` strictness).
- Add `noImplicitOverride: true`.
- `verbatimModuleSyntax: true` is fine if already on; otherwise leave for now.
- `README.md` updated: 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` (default `http://localhost:8055`).
- `VITE_DEV_PROCESSOR_WS_URL` (default `ws://localhost:8081`).
- Read in `vite.config.ts` via `loadEnv`.
- Document in `.env.dev.example` (committed) and `.env.dev.local` (gitignored).
## Specification
### Why proxy in dev instead of CORS
CORS-with-credentials requires:
1. The browser sending `withCredentials: true` on every request.
2. The server responding with `Access-Control-Allow-Origin: <exact-origin>` (not `*`) and `Access-Control-Allow-Credentials: true`.
3. Cookies being scoped correctly (`SameSite=None; Secure` for 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
```ts
// 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:
```ts
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 dev` starts at `http://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 typecheck` is stricter — adding `arr[0].whatever` without a check should now fail.
- [ ] No CORS errors in the browser console when calling `/api/...` from `http://localhost:5173`.
## Risks / open questions
- **Directus's WS path is `/websocket`, not `/ws`.** Confirmed by reading Directus 11.x source. The proxy `rewrite` rule reflects this.
- **Processor WS path is unsettled.** [[processor-ws-contract]] uses `/processor/ws` as illustrative; the actual path will be set by the proxy config in stage/prod (`trm/deploy`). For dev we assume the Processor binds to `:8081` with no path prefix, and the proxy adds the `/ws-live` prefix on the SPA side. Reconcile if Phase 1.5 lands with a different path convention.
- **Multiple Vite apps on one machine.** Pinning to `:5173` collides if another Vite app is running. Document the override (`VITE_PORT=5174 pnpm dev` works 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}/*` (default `http://localhost:8055`), strips the `/api` prefix.
- `/ws-business` → derived `ws://...` from the Directus URL, rewrites to `/websocket`.
- `/ws-live``${VITE_DEV_PROCESSOR_WS_URL}` (default `ws://localhost:8081`), strips the `/ws-live` prefix.
`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`.