-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvite.config.ts
More file actions
224 lines (218 loc) · 11.4 KB
/
Copy pathvite.config.ts
File metadata and controls
224 lines (218 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/// <reference types="vitest/config" />
import { defineConfig } from "vitest/config";
import { loadEnv } from "vite";
import react from "@vitejs/plugin-react";
import { VitePWA } from "vite-plugin-pwa";
export function resolveDevServer(processEnvironment: NodeJS.ProcessEnv): {
port: number;
strictPort: boolean;
} {
const port = processEnvironment.PORT;
if (port === undefined) return { port: 5173, strictPort: false };
if (!/^\d+$/.test(port)) {
throw new Error("PORT must be an integer from 1 through 65535.");
}
const parsedPort = Number(port);
if (parsedPort < 1 || parsedPort > 65535) {
throw new Error("PORT must be an integer from 1 through 65535.");
}
return { port: parsedPort, strictPort: true };
}
export function resolveApiTarget(
processEnvironment: NodeJS.ProcessEnv,
fileEnvironment: Record<string, string>,
): string {
return processEnvironment.VITE_API_TARGET ?? fileEnvironment.VITE_API_TARGET ?? "http://localhost:8080";
}
// Dev-only proxy: the SPA calls same-origin "/api/..." and Vite forwards to the
// backend, so no CORS config is needed on the API for local dev. Override the
// target with VITE_API_TARGET (defaults to the docker-compose port 8080).
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
const server = resolveDevServer(process.env);
const target = resolveApiTarget(process.env, env);
return {
plugins: [
react(),
// #142 — installable PWA. Scope is the app SHELL only: the service worker
// makes the SPA launchable from a home screen and survivable on a bad
// connection. It caches no application data — offline capture is #50.
VitePWA({
// 'prompt', not 'autoUpdate': this app is used to type daily entries on
// barn phones, and skipWaiting can swap the running app out mid-form.
// The new shell installs in the background and waits for the user to
// accept (see UpdatePrompt), so an update never eats in-progress work.
registerType: "prompt",
// We register by hand in registerServiceWorker.ts so registration can be
// guarded on a secure context; the plugin's auto-injected snippet has no
// such guard.
injectRegister: null,
// No `includeAssets`: the workbox globPatterns below already sweep up
// every png/svg in the build, and listing them twice puts duplicate
// entries in the precache manifest.
manifest: {
name: "Cluckwork",
short_name: "Cluckwork",
description: "Poultry egg-farm management",
start_url: "/",
scope: "/",
display: "standalone",
// Matches the light-scheme theme-color already in index.html; the
// manifest takes a single value, so the light aubergine is the one
// that shows in the task switcher and splash.
theme_color: "#4a154b",
background_color: "#4a154b",
icons: [
{ src: "/icon-192.png", sizes: "192x192", type: "image/png" },
{ src: "/icon-512.png", sizes: "512x512", type: "image/png" },
// Separate maskable art: Android crops to a circle/squircle, and the
// standard mark is full-bleed, so it needs its own padded variant.
{ src: "/icon-192-maskable.png", sizes: "192x192", type: "image/png", purpose: "maskable" },
{ src: "/icon-512-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
],
},
workbox: {
// The built shell: hashed JS/CSS plus the root entry and icons.
globPatterns: ["**/*.{js,css,html,svg,png,woff2}"],
// An unknown route serves index.html from the cache, EXCEPT the
// server's own namespaces, which must always reach the network.
//
// The pattern is deliberately not `/^\/api\//`: that misses a bare
// `/api`, a query-only `/api?x=1`, and — since ASP.NET routing is
// case-insensitive — `/API/v1/...`, all of which would then be handed
// a cached index.html (#142 review). /health is excluded for the same
// reason: a probe opened in a browser carrying this worker would
// otherwise be answered with the SPA shell instead of the real health
// response (verified — it was).
navigateFallback: "/index.html",
navigateFallbackDenylist: [/^\/api(?:[/?]|$)/i, /^\/health(?:[/?]|$)/i],
// Belt to that braces: never let a runtime handler answer an /api
// request from cache. Auth state and tenant data are per-request; a
// stale shared response here would be a correctness bug, not a
// performance win. #50 adds an explicit, deliberate offline path.
runtimeCaching: [],
navigationPreload: false,
// #217 — the app's own maps are emitted hidden (see build below);
// workbox would otherwise ship sw.js.map + its own map WITH
// sourceMappingURL comments. They only cover generated worker
// code, so drop them rather than special-case them.
sourcemap: false,
},
// The SW is a production artifact; leaving it off in dev keeps `npm run
// dev` free of stale-cache confusion.
devOptions: { enabled: false },
}),
],
server: {
...server,
proxy: {
"/api": { target, changeOrigin: true },
},
},
// #217 — "hidden": emit .map files so a reported minified stack resolves
// to source lines, but without the sourceMappingURL comment, so browsers
// never fetch them uninvited. They ship next to the bundles; resolving a
// stack is an operator action (source-map CLI or a browser devtools "add
// source map"), not something the public page advertises.
build: {
sourcemap: "hidden",
},
// Unit tests only (Vitest). E2E stays the manual Playwright drill (#105).
// Explicit vitest imports in each test — no globals — so the app's strict
// tsconfig stays clean of test-runner ambient types.
test: {
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
globals: false,
include: ["src/**/*.test.{ts,tsx}"],
// Node 25+ ships the WHATWG Web Storage API natively, so `localStorage`
// already exists on globalThis (as undefined unless node runs with
// --localstorage-file). Vitest's jsdom environment only bridges window
// globals that are NOT already present on globalThis, so under Node 25+
// every bare `localStorage` access in app or test code hits Node's
// undefined stub instead of jsdom's Storage and throws. Strip Node's
// webstorage from the test workers so jsdom's implementation is bridged
// exactly as on older Node.
execArgv: ["--no-experimental-webstorage"],
// #121: coverage gate. The thresholds are a REGRESSION FLOOR near the
// current numbers, not a target — the global floor is deliberately low
// (the SPA still has ~14 untested screens) and ratchets up as each
// screen's tests land. The per-directory locks pin the already-covered
// foundation (auth/dates/api-client) at ~100% so it can't backslide.
coverage: {
provider: "v8",
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/main.tsx", // app entry: renders <App/>, nothing to unit-test
"src/api/types.ts", // type-only DTOs (no runtime code)
"src/**/*.d.ts", // ambient declarations (vite-env.d.ts, etc.)
"src/**/*.test.{ts,tsx}",
"src/test/**", // test-only helpers (renderWithProviders, jwt, setup)
],
reporter: ["text", "html"],
thresholds: {
// Global regression floor, re-baselined as screen tests land. After
// the farm settings screen + farm context (#123): lines 91.3 /
// branch 85.3 / funcs 71.9. Reports stays untested; the static
// Help/shell screens are excluded-in-spirit — hence sub-100. A
// screen-test PR raises lines/functions; branches move either way
// (testing a screen exposes all its conditional branches), so
// re-baseline branches in BOTH directions with headroom.
//
// Re-baselined for @vitest/coverage-v8 4.x: AST-aware remapping
// replaced v8-to-istanbul (no opt-out), which recounts everything —
// same tests, different denominators (lines 87.2 / stmts 83.7 /
// branch 73.2 / funcs 78.0). Not a coverage regression.
//
// Re-baselined for #182 (i18n foundation + Sales externalization):
// measured lines 87.66 / stmts 84.24 / funcs 78.27 / branches 73.91.
// lines/branches hold their prior floor (the new code landed inside
// the existing margin); statements/functions rose with the new,
// fully-tested src/i18n + src/session code, so those two floors move
// up to match.
// Ratcheted after #236 (pending-state migration touched every
// screen and shipped tests with it): actuals 88.5/85.5/80.1/75.0,
// floors keep ~1pt headroom.
//
// Ratcheted after #250 (NumberField floor + stepper rollout with
// component + screen tests): actuals 90.7/87.7/82.0/78.1, floors
// keep ~1pt headroom.
//
// Re-measured after #308 (step-up authentication: client.ts +
// cluckwork.ts + UsersPage.tsx changes, all covered): actuals
// 91.03/88.02/82.32/78.34 — every number moved UP, so the floors
// below are unchanged (still ~1-1.3pt headroom, same convention).
//
// Ratcheted after #469 (usePagedList extracted with its own 24-test
// race suite; six screens migrated onto it and shed their hand-rolled
// load/ticket code, so the remaining lines are better covered):
// actuals 91.96/89.11/84.62/80.32. Branches rose here rather than
// fell — the hook concentrates the conditionals that used to be
// spread across six screens, and they are exhaustively tested.
//
// Ratcheted after #479 (useDialogErrors + DialogError, then eleven
// screens migrated onto them with ~50 new placement tests): actuals
// 93.36/90.45/86.78/81.75. Branches rose again for the same reason
// #469 did — the conditionals that decided WHERE a message renders
// used to be per screen, and are now one tested hook plus one tested
// component. Floors keep the usual ~1.3pt headroom.
lines: 92,
statements: 89,
functions: 85,
branches: 80,
// high-water locks on the fully-covered foundation (AST-aware
// counting surfaces statements/branches the old remapper credited
// for free, so the 100s that survived stay; the rest pin to the
// new actuals)
"src/auth/**": { statements: 98, lines: 100, functions: 100, branches: 90 },
"src/lib/**": { statements: 100, lines: 100, functions: 100, branches: 100 },
// The farm context joins them (#123): every screen with a date field
// now reads its timezone through it, so a hole here is a hole in all
// of them at once.
"src/farm/**": { statements: 100, lines: 100, functions: 100, branches: 100 },
"src/api/client.ts": { statements: 94, lines: 95, functions: 100, branches: 85 },
},
},
},
};
});