elitea-web development
apps/elitea-web is a single Vite/React project that builds four separate outputs — the main app, the admin console, the maintenance page and the brand previewer — from four entry points under src/entries/. This documentation site is a separate app; see Work on the documentation.
Dev server modes
Section titled “Dev server modes”npm run dev # the main app (src/entries/app), no --mode needednpm run dev -- --mode admin # the admin consolenpm run dev -- --mode maintenance # the maintenance pagenpm run dev -- --mode brand-previewvite.config.ts branches on mode to choose the entry root, the base path, and the build output directory (dist/app, dist/admin, dist/maintenance, dist/brand-preview) — the same four targets npm run build chains through in order.
Unit tests and Storybook
Section titled “Unit tests and Storybook”npm run test:unit # vitest, node projectnpm run test:storybook # component stories, through the Storybook Vitest addonnpm run typecheck # tsc --noEmitnpm run lint # oxlint -c .oxlintrc.jsonThere is no separate npm run storybook dev-server script in this repository yet — stories run through test:storybook’s Vitest project rather than a standalone Storybook UI.
End-to-end tests
Section titled “End-to-end tests”See Run the browser test stack for bringing up the stack Playwright needs and running npm run e2e, npm run e2e:chat-stream, and npm run e2e:visual.
Gate scripts
Section titled “Gate scripts”CI runs each of these as its own step; run them locally before you push to catch the same failure sooner. None of them takes a flag to fix things beyond what’s noted.
Generated API client
services/elitea-main/api/openapi/v2.yaml is the source of truth. orval.config.ts reads it (input.target: '../../services/elitea-main/api/openapi/v2.yaml') and writes src/shared/api/generated/** — hooks, Zod schemas, and MSW mock handlers. node scripts/check-generated-client.mjs regenerates into an empty directory and diffs it against the committed tree; a stale client fails open (it keeps exporting operations the API no longer has), which is why the gate compares full file trees rather than a git status after an in-place regeneration.
Test mocks come from the contract too. orval runs its MSW generator with useExamples: true, so an operation whose response carries an example in v2.yaml serves that example as its mock body; one without falls back to generated sample data. node scripts/check-spec-examples.mjs validates every example/examples value in v2.yaml against its schema, so a mock body can never drift from the shape it documents.
Regenerate: npx orval (from apps/elitea-web), then commit the result.
Endpoint manifest
src/shared/api/endpoints.manifest.json is a hand-maintained catalogue (id, method, path, operationId, source, response schema, fixture, callers) of every network call the app makes. node scripts/check-endpoint-manifest.mjs checks each source: generated entry’s operationId actually exists among the real generated hooks on disk — not just that the OpenAPI spec declares it, so drift between “orval was run” and “the spec says so” is caught too — plus duplicate-id and structural checks on hand-written entries.
Fix: add or correct the manifest entry by hand; it is not generated.
MSW handler hygiene and i18n backfill
node scripts/check-handlers.mjs walks src/test/msw/handlers/** (the hand-authored mock handlers, as opposed to orval’s own generated .msw.ts files) and requires every body to be either a generated contract mock (such as getListBucketsResponseMock()) or a typed module constant (typed with the generated model type where the contract has one). It flags an inline literal body, an untyped object/array constant and any JSON import. To give a handler a new realistic body for a real route, add a response example to that operation in v2.yaml and regenerate; keep a typed constant only for what the contract cannot describe (raw bytes, error bodies, transport probes).
node scripts/i18n-backfill.mjs --check walks every t(key, fallback) call site and fails if src/shared/i18n/en.json is missing a key a call site introduced, or if a call site’s fallback text has drifted from what’s already shipped. Run it without --check to have it write the missing keys into en.json for you.
Dead code (knip)
node scripts/check-dead-code.mjs wraps npx knip, reading knip.json’s project/entry globs and asserting they actually match files before trusting knip’s zero-issues report — a glob that matches nothing gives knip nothing to analyze, and a bare npx knip --max-issues 0 would pass either way.
Fix: delete the unreached file/export knip names, or add it to knip.json’s ignore list if it’s a deliberate false positive (dynamic import, entry point knip can’t see).
Bundle budget
node scripts/check-bundle-budget.mjs, run after npm run build, measures dist/: each target’s initial set (the entry <script type="module"> plus its <link rel="modulepreload"> chain — what a browser fetches before first paint) against bundle-budget.json’s 300 KB ceiling, and every other route chunk against a 250 KB ceiling.
Fix: split a large module behind a dynamic import(), or trim what the initial chunk pulls in — the script only measures; it does not tell you which import to move.