M2-T06: scaffold React SPA frontend with typed OpenAPI client

- Vite + React 18 + TypeScript + Mantine + TanStack Query + react-router-dom
- typed client: openapi-typescript -> src/api/schema.d.ts (committed), openapi-fetch
- fetch wrapper middleware: cookies, X-CSRF-Token on writes, 401 -> /login,
  non-401 errors carry parsed JSON body
- SessionProvider/useSession (GET /api/session), ProtectedRoute skeleton
- app shell (Mantine + router) with placeholder login/home/config pages + gear nav
- dev proxy to FastAPI; vitest smoke test; frontend README
- npm scripts: dev/build/preview/lint/typecheck/test/codegen
This commit is contained in:
2026-06-13 09:52:56 +02:00
parent dba9e28540
commit 6cfeb2b865
23 changed files with 9741 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
node_modules/
dist/
dist-ssr/
*.local
.env
.env.*
!.env.example
+209
View File
@@ -0,0 +1,209 @@
# Home Automation — Frontend
React SPA for the home-automation backend. Built with Vite + React 18 + TypeScript.
Scaffolded in M2-T06; feature pages filled in by T07T10.
## Stack
| Layer | Library | Version |
|---|---|---|
| Build | Vite | 6.x |
| UI framework | React | 18.x |
| Language | TypeScript | 5.x |
| Component library | Mantine | 7.x |
| Data fetching | TanStack Query | 5.x |
| Routing | react-router-dom | 6.x |
| API client codegen | openapi-typescript | 7.x |
| API client runtime | openapi-fetch | 0.17.x |
| Testing | Vitest + @testing-library/react | 4.x / 14.x |
## npm Scripts
| Command | What it does |
|---|---|
| `npm run dev` | Start Vite dev server (with backend proxy — see below) |
| `npm run build` | `tsc -b && vite build` — type-check then build to `dist/` |
| `npm run preview` | Serve the built `dist/` locally |
| `npm run lint` | ESLint (flat config, React + TypeScript rules) |
| `npm run typecheck` | `tsc --noEmit` — type-check without emitting files |
| `npm run test` | Vitest (run once, no watch) |
| `npm run codegen` | Regenerate `src/api/schema.d.ts` from `../openapi/openapi.json` |
All frontend gates must pass before any task is considered done:
```bash
npm run codegen
npm run lint
npm run typecheck
npm run test
npm run build # must produce dist/
```
## Directory Structure
```
frontend/
├── index.html Vite entry HTML
├── vite.config.ts Vite + Vitest config; dev proxy
├── tsconfig.json References tsconfig.app.json + tsconfig.node.json
├── tsconfig.app.json App source TS config (strict, react-jsx)
├── tsconfig.node.json Vite config TS config
├── eslint.config.js Flat ESLint config (React + TypeScript rules)
├── package.json Dependencies + npm scripts
├── package-lock.json Lockfile (committed; CI uses npm ci)
└── src/
├── main.tsx Entry point; mounts <App> into #root
├── App.tsx Provider stack + route tree (MantineProvider → QueryClient → Router → SessionProvider)
├── vite-env.d.ts /// <reference types="vite/client" /> for CSS imports
├── test-setup.ts Vitest global setup (@testing-library/jest-dom)
├── api/
│ ├── schema.d.ts AUTO-GENERATED from openapi/openapi.json (committed)
│ ├── client.ts openapi-fetch client + CSRF/cookie/401 middleware
│ └── csrf.ts Module-level CSRF token holder (setCsrfToken / getCsrfToken)
├── auth/
│ ├── SessionProvider.tsx TanStack Query against GET /api/session; exposes useSession()
│ └── ProtectedRoute.tsx Redirects to /login when unauthenticated
└── pages/
├── LoginPage.tsx Placeholder → T07 builds the real form
├── HomePage.tsx Placeholder → T09 builds the map/heatmap view
└── ConfigPage.tsx Placeholder → T08 builds the config editor
```
## Dev Proxy (local development)
`npm run dev` starts Vite on port 5173. The Vite config proxies API/auth paths
to the FastAPI backend running on port 8000:
| Proxied path | Backend URL |
|---|---|
| `/api/*` | `http://localhost:8000` |
| `/login` | `http://localhost:8000` |
| `/logout` | `http://localhost:8000` |
| `/static/*` | `http://localhost:8000` |
| `/docs` | `http://localhost:8000` |
| `/openapi.json` | `http://localhost:8000` |
To develop locally:
1. Start the backend: `uvicorn app.main:app --reload --host 0.0.0.0 --port 8000`
2. Start the frontend: `cd frontend && npm run dev`
3. Open `http://localhost:5173` — the app proxies all API calls to the backend.
Since the dev server proxies the session cookie path, auth flows work exactly as
they would in the deployed (same-origin) setup.
## Adding a New Page + Typed Query
This is the pattern every task T07T10 follows to wire up a real page:
### 1. Run codegen (if the OpenAPI contract changed)
```bash
npm run codegen
```
The generated `src/api/schema.d.ts` is committed to the repo. CI enforces that
the file is in sync with `openapi/openapi.json` via:
```bash
npm run codegen && git diff --exit-code frontend/src/api/schema.d.ts
```
### 2. Import the typed client
```typescript
// src/pages/SomePage.tsx
import apiClient from '../api/client'
```
### 3. Write a typed TanStack Query
```typescript
import { useQuery } from '@tanstack/react-query'
import apiClient from '../api/client'
function usePooRecords(limit = 100) {
return useQuery({
queryKey: ['poo', { limit }],
queryFn: async () => {
const res = await apiClient.GET('/api/poo', { params: { query: { limit } } })
// res.data is typed as PooResponse | undefined
// On non-2xx the middleware throws ApiError; TanStack Query catches it.
return res.data
},
})
}
```
The `params.query` and `params.path` objects are fully typed from `schema.d.ts`.
TypeScript will error if you pass unknown query params or mistype a path param.
### 4. Write a typed mutation (write request)
```typescript
import { useMutation, useQueryClient } from '@tanstack/react-query'
import apiClient from '../api/client'
function useDeletePoo() {
const qc = useQueryClient()
return useMutation({
mutationFn: (timestamp: string) =>
apiClient.DELETE('/api/poo/{timestamp}', {
params: { path: { timestamp } },
}),
onSuccess: () => qc.invalidateQueries({ queryKey: ['poo'] }),
})
}
```
The middleware (`src/api/client.ts`) automatically injects the `X-CSRF-Token` header
on all non-GET/HEAD requests (sourced from `getCsrfToken()`). You do not need to
handle CSRF manually in page code.
### 5. Add the route in App.tsx
```typescript
// App.tsx
import { SomePage } from './pages/SomePage'
// Inside <Routes>:
<Route path="/some-path" element={<SomePage />} />
// or, if protected:
<Route
element={
<ProtectedRoute>
<AppLayout />
</ProtectedRoute>
}
>
<Route path="/some-path" element={<SomePage />} />
</Route>
```
## OpenAPI codegen + CI sync rule
`src/api/schema.d.ts` is committed to the repository (not gitignored).
**Rule**: whenever `openapi/openapi.json` changes (any backend task that modifies
a route or schema), CI must run:
```bash
cd frontend && npm run codegen
git diff --exit-code frontend/src/api/schema.d.ts
```
If the file has changed but the new version was not committed, CI fails.
To update manually after a backend change:
```bash
cd frontend
npm run codegen
git add src/api/schema.d.ts
git commit -m "M2-Txx: update generated OpenAPI types"
```
## Production Build
The production build (`npm run build`) writes static files to `frontend/dist/`.
In the deployed setup (M2-T11 onwards), FastAPI serves `dist/` as a static
directory and falls back to `dist/index.html` for all non-`/api` paths,
enabling client-side routing with deep links.
The multi-stage Dockerfile (M2-T12) builds the frontend in a Node container and
copies only `dist/` into the Python image — the production image does not
contain Node or npm.
+29
View File
@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactPlugin from 'eslint-plugin-react'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist', 'src/api/schema.d.ts'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
react: reactPlugin,
},
rules: {
...reactHooks.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
)
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home Automation</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+7204
View File
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
{
"name": "home-automation-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"codegen": "openapi-typescript ../openapi/openapi.json -o ./src/api/schema.d.ts"
},
"dependencies": {
"@mantine/core": "^7.17.8",
"@mantine/hooks": "^7.17.8",
"@tanstack/react-query": "^5.101.0",
"openapi-fetch": "^0.17.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.30.4"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.6.1",
"@types/react": "^18.3.31",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react": "^4.7.0",
"eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"jsdom": "^29.1.1",
"openapi-typescript": "^7.13.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.61.0",
"vite": "^6.4.3",
"vitest": "^4.1.8"
}
}
+118
View File
@@ -0,0 +1,118 @@
/**
* App — top-level provider stack and route tree.
*
* Provider order (outermost first):
* MantineProvider → QueryClientProvider → BrowserRouter → SessionProvider → routes
*
* Route tree:
* /login → LoginPage (public, T07 will build the real form)
* / → ProtectedRoute → AppLayout → HomePage (T09)
* /config → ProtectedRoute → AppLayout → ConfigPage (T08)
*
* AppLayout renders a minimal shell with a gear-icon nav entry for /config (§5#10).
* T07T10 slot their real pages in without touching the provider/router setup.
*/
import { MantineProvider } from '@mantine/core'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter, Routes, Route, Link, Outlet } from 'react-router-dom'
// Mantine requires its CSS to be imported once.
import '@mantine/core/styles.css'
import { SessionProvider } from './auth/SessionProvider'
import { ProtectedRoute } from './auth/ProtectedRoute'
import { LoginPage } from './pages/LoginPage'
import { HomePage } from './pages/HomePage'
import { ConfigPage } from './pages/ConfigPage'
// ---------------------------------------------------------------------------
// TanStack Query client (singleton, created outside render to avoid re-creation)
// ---------------------------------------------------------------------------
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Don't retry on 4xx — we handle 401 in the middleware
retry: (failureCount, error) => {
if (error instanceof Error && 'status' in error) {
const status = (error as unknown as { status: number }).status
if (status >= 400 && status < 500) return false
}
return failureCount < 2
},
},
},
})
// ---------------------------------------------------------------------------
// App shell layout (used by all protected pages)
// ---------------------------------------------------------------------------
function AppLayout() {
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Minimal top nav — T07T10 can enhance this with Mantine AppShell */}
<nav
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0.5rem 1rem',
borderBottom: '1px solid #eee',
}}
>
<Link to="/" style={{ fontWeight: 600, textDecoration: 'none' }}>
Home Automation
</Link>
{/* Gear icon nav slot — links to config page (§5#10) */}
<Link
to="/config"
aria-label="Configuration"
style={{ fontSize: '1.25rem', textDecoration: 'none' }}
title="Configuration"
>
</Link>
</nav>
{/* Page content */}
<main style={{ flex: 1 }}>
<Outlet />
</main>
</div>
)
}
// ---------------------------------------------------------------------------
// Root app
// ---------------------------------------------------------------------------
export default function App() {
return (
<MantineProvider>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<SessionProvider>
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
{/* Protected routes — all nested under AppLayout */}
<Route
element={
<ProtectedRoute>
<AppLayout />
</ProtectedRoute>
}
>
<Route index element={<HomePage />} />
<Route path="/config" element={<ConfigPage />} />
</Route>
</Routes>
</SessionProvider>
</BrowserRouter>
</QueryClientProvider>
</MantineProvider>
)
}
+109
View File
@@ -0,0 +1,109 @@
/**
* Typed API client built on openapi-fetch + generated schema.d.ts.
*
* Middleware contract (orchestrator-decisions.md §11):
* 1. Always send cookies (credentials: "include"; same-origin auto-sends but explicit is clear).
* 2. Non-GET/HEAD requests inject X-CSRF-Token from the csrf holder.
* Exception: POST /api/auth/login skips injection (unauthenticated endpoint).
* 3. 401 responses → clear session state + navigate to /login.
* 4. Other non-2xx responses → throw an ApiError carrying the parsed JSON body,
* so callers (e.g. SMTP test) can inspect body.result.
*/
import createClient, { type Middleware } from 'openapi-fetch'
import type { paths } from './schema.d.ts'
import { getCsrfToken } from './csrf'
// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------
/** Error thrown for non-2xx, non-401 responses. Carries the parsed JSON body. */
export class ApiError extends Error {
constructor(
public readonly status: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public readonly body: any,
) {
super(`API error ${status}`)
this.name = 'ApiError'
}
}
// ---------------------------------------------------------------------------
// Internal navigation helper (avoids React-router import at module level)
// ---------------------------------------------------------------------------
let _navigateToLogin: (() => void) | null = null
/**
* Register a callback that the middleware calls on 401.
* SessionProvider calls this during its setup.
*/
export function registerLoginRedirect(fn: () => void): void {
_navigateToLogin = fn
}
// ---------------------------------------------------------------------------
// CSRF middleware
// ---------------------------------------------------------------------------
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
const LOGIN_PATH = '/api/auth/login'
const csrfMiddleware: Middleware = {
async onRequest({ request }) {
// Always include cookies (same-origin; explicit for clarity)
// Note: credentials is set at client level; this is belt-and-suspenders doc.
const method = request.method.toUpperCase()
const url = new URL(request.url)
if (WRITE_METHODS.has(method) && url.pathname !== LOGIN_PATH) {
const token = getCsrfToken()
if (token) {
request.headers.set('X-CSRF-Token', token)
}
}
return request
},
async onResponse({ response }) {
if (response.status === 401) {
// Clear any cached session state by triggering a page navigation.
// The SessionProvider query will refetch and find no session.
if (_navigateToLogin) {
_navigateToLogin()
}
// Return the original response so callers can handle 401 if needed.
return response
}
if (!response.ok) {
// Parse body and throw; caller can catch ApiError and read .body
let body: unknown
try {
body = await response.clone().json()
} catch {
body = null
}
throw new ApiError(response.status, body)
}
return response
},
}
// ---------------------------------------------------------------------------
// Client instance
// ---------------------------------------------------------------------------
const apiClient = createClient<paths>({
baseUrl: '/',
credentials: 'include',
})
apiClient.use(csrfMiddleware)
export default apiClient
+35
View File
@@ -0,0 +1,35 @@
/**
* Smoke tests for the CSRF token holder.
* These run in isolation (no DOM, no React) and validate the module contract.
*/
import { describe, it, expect, beforeEach } from 'vitest'
import { setCsrfToken, getCsrfToken } from './csrf'
describe('csrf holder', () => {
beforeEach(() => {
// Reset to empty between tests by setting empty string
setCsrfToken('')
})
it('returns empty string before any token is set', () => {
expect(getCsrfToken()).toBe('')
})
it('stores and returns the token that was set', () => {
setCsrfToken('test-token-abc123')
expect(getCsrfToken()).toBe('test-token-abc123')
})
it('overwrites a previously set token', () => {
setCsrfToken('first')
setCsrfToken('second')
expect(getCsrfToken()).toBe('second')
})
it('can be reset to empty', () => {
setCsrfToken('some-token')
setCsrfToken('')
expect(getCsrfToken()).toBe('')
})
})
+23
View File
@@ -0,0 +1,23 @@
/**
* Module-level CSRF token holder.
*
* The token is populated by SessionProvider after a successful GET /api/session.
* The fetch client middleware reads it on every non-GET/HEAD request.
*
* Per the project CSRF contract (m2-frontend-v2.md §3.2, orchestrator-decisions.md §3):
* - Server checks presence/non-empty only, does NOT validate the value.
* - Sending an empty-string or stale value will result in a 403; callers must
* ensure setCsrfToken() is called before issuing write requests.
*/
let _csrfToken = ''
/** Store the CSRF token returned by GET /api/session. */
export function setCsrfToken(token: string): void {
_csrfToken = token
}
/** Return the current CSRF token (may be empty string if not yet set). */
export function getCsrfToken(): string {
return _csrfToken
}
+1651
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
/**
* ProtectedRoute — renders children when authenticated; redirects to /login otherwise.
*
* Shows nothing while the session is still loading to avoid flash-of-login.
* T07 can replace the loading placeholder with a proper spinner/skeleton.
*/
import type { ReactNode } from 'react'
import { Navigate } from 'react-router-dom'
import { useSession } from './SessionProvider'
interface ProtectedRouteProps {
children: ReactNode
}
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const { status } = useSession()
if (status === 'loading') {
// Render nothing while we check the session — avoids a flash to /login.
return null
}
if (status === 'unauthenticated') {
return <Navigate to="/login" replace />
}
return <>{children}</>
}
+109
View File
@@ -0,0 +1,109 @@
/**
* SessionProvider — fetches GET /api/session once on mount via TanStack Query.
*
* Contract (orchestrator-decisions.md §4, §11):
* - 200 → authenticated; calls setCsrfToken(data.csrf_token) so write requests work.
* - 401 → unauthenticated (not an error toast; normal state before login).
* - Exposes { user, status } to descendants via useSession().
*
* Also registers the 401 → /login redirect with the API client middleware.
*/
import { createContext, useContext, useEffect, type ReactNode } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import apiClient, { registerLoginRedirect } from '../api/client'
import { setCsrfToken } from '../api/csrf'
import type { components } from '../api/schema.d.ts'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type SessionUser = components['schemas']['SessionUser']
type SessionStatus = 'loading' | 'authenticated' | 'unauthenticated'
interface SessionContextValue {
user: SessionUser | null
status: SessionStatus
}
// ---------------------------------------------------------------------------
// Context
// ---------------------------------------------------------------------------
const SessionContext = createContext<SessionContextValue>({
user: null,
status: 'loading',
})
// ---------------------------------------------------------------------------
// Hook
// ---------------------------------------------------------------------------
/** Access the current session from any descendant component. */
export function useSession(): SessionContextValue {
return useContext(SessionContext)
}
// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------
interface SessionProviderProps {
children: ReactNode
}
export function SessionProvider({ children }: SessionProviderProps) {
const navigate = useNavigate()
const queryClient = useQueryClient()
// Register the 401 redirect callback with the API client once.
useEffect(() => {
registerLoginRedirect(() => {
// Invalidate the session query so any subscriber re-fetches (→ unauthenticated).
queryClient.invalidateQueries({ queryKey: ['session'] })
navigate('/login', { replace: true })
})
}, [navigate, queryClient])
const { data, status, error } = useQuery({
queryKey: ['session'],
queryFn: async () => {
const res = await apiClient.GET('/api/session')
// openapi-fetch returns { data, error, response }.
// On 401 the middleware already navigates; here data will be undefined.
return res.data ?? null
},
// Don't treat 401 as a React Query "error" — it's a normal unauthenticated state.
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes
})
// When we get session data, store the CSRF token.
useEffect(() => {
if (data?.csrf_token) {
setCsrfToken(data.csrf_token)
}
}, [data])
let sessionStatus: SessionStatus
if (status === 'pending') {
sessionStatus = 'loading'
} else if (status === 'error' || data === null || !data) {
// 401 returns null from our queryFn; any actual network error → unauthenticated.
sessionStatus = 'unauthenticated'
// Suppress unused variable warning for error in non-401 cases
void error
} else {
sessionStatus = 'authenticated'
}
const value: SessionContextValue = {
user: data?.user ?? null,
status: sessionStatus,
}
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Entry point — mounts the React app into #root.
*/
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
const rootElement = document.getElementById('root')
if (!rootElement) {
throw new Error('Root element #root not found in document')
}
createRoot(rootElement).render(
<StrictMode>
<App />
</StrictMode>,
)
+19
View File
@@ -0,0 +1,19 @@
/**
* ConfigPage — placeholder for M2-T08.
*
* T08 replaces this with the real config UI: all config sections rendered as editable
* fields, secret masking, save button (PUT /api/config), and SMTP test action.
*/
import { Container, Title, Text } from '@mantine/core'
export function ConfigPage() {
return (
<Container pt="xl">
<Title order={2}>Configuration</Title>
<Text c="dimmed" mt="sm">
Config editor implemented in M2-T08.
</Text>
</Container>
)
}
+19
View File
@@ -0,0 +1,19 @@
/**
* HomePage — placeholder for M2-T09.
*
* T09 replaces this with the real home view: Leaflet map, heatmap layer,
* time-range selector, scatter-point layer, and poo overlay.
*/
import { Container, Title, Text } from '@mantine/core'
export function HomePage() {
return (
<Container pt="xl">
<Title order={2}>Home</Title>
<Text c="dimmed" mt="sm">
Map / heatmap visualisation implemented in M2-T09.
</Text>
</Container>
)
}
+19
View File
@@ -0,0 +1,19 @@
/**
* LoginPage — placeholder for M2-T07.
*
* T07 replaces this with the real login form (username/password → POST /api/auth/login,
* force-password-change flow, redirect to home on success).
*/
import { Container, Title, Text } from '@mantine/core'
export function LoginPage() {
return (
<Container size="xs" pt="xl">
<Title order={2}>Login</Title>
<Text c="dimmed" mt="sm">
Login form implemented in M2-T07.
</Text>
</Container>
)
}
+5
View File
@@ -0,0 +1,5 @@
/**
* Vitest global setup file.
* Imports @testing-library/jest-dom to extend vitest matchers with DOM assertions.
*/
import '@testing-library/jest-dom'
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"allowArbitraryExtensions": true
},
"include": ["src"]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+24
View File
@@ -0,0 +1,24 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:8000',
'/login': 'http://localhost:8000',
'/logout': 'http://localhost:8000',
'/static': 'http://localhost:8000',
'/docs': 'http://localhost:8000',
'/openapi.json': 'http://localhost:8000',
},
},
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test-setup.ts'],
},
})