chore: configure prettier with ignore rules and format scripts + running format

This commit is contained in:
lila 2026-03-20 18:37:38 +01:00
parent 22bb8a1e4c
commit ce42eb1811
12 changed files with 150 additions and 89 deletions

19
.prettierignore Normal file
View file

@ -0,0 +1,19 @@
.tmp/
# Build outputs
dist/
*.tsbuildinfo
# Dependencies
node_modules/
# Environment files
.env*
# Logs (if you create them)
logs/
# Coverage reports (when you add testing)
coverage/
pnpm-lock.yaml

13
.prettierrc Normal file
View file

@ -0,0 +1,13 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"objectWrap": "collapse",
"bracketSameLine": false,
"arrowParens": "always"
}

View file

@ -2,11 +2,8 @@
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"references": [ "references": [
{ "path": "../../packages/shared" }, { "path": "../../packages/shared" },
{ "path": "../../packages/db" }, { "path": "../../packages/db" }
], ],
"compilerOptions": { "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" },
"module": "NodeNext", "include": ["src"]
"moduleResolution": "NodeNext",
},
"include": ["src"],
} }

View file

@ -1,4 +1,4 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"references": [{ "path": "../../packages/shared" }], "references": [{ "path": "../../packages/shared" }]
} }

View file

@ -5,6 +5,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
--- ---
## Phase 0 — Foundation ## Phase 0 — Foundation
**Goal**: Empty repo that builds, lints, and runs end-to-end. **Goal**: Empty repo that builds, lints, and runs end-to-end.
**Done when**: `pnpm dev` starts both apps; `GET /api/health` returns 200; React renders a hello page. **Done when**: `pnpm dev` starts both apps; `GET /api/health` returns 200; React renders a hello page.
@ -23,6 +24,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
--- ---
## Phase 1 — Vocabulary Data ## Phase 1 — Vocabulary Data
**Goal**: Word data lives in the DB and can be queried via the API. **Goal**: Word data lives in the DB and can be queried via the API.
**Done when**: `GET /api/terms?pair=en-it&limit=10` returns 10 terms, each with 3 distractors attached. **Done when**: `GET /api/terms?pair=en-it&limit=10` returns 10 terms, each with 3 distractors attached.
@ -39,6 +41,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
--- ---
## Phase 2 — Auth ## Phase 2 — Auth
**Goal**: Users can log in via Google or GitHub and stay logged in. **Goal**: Users can log in via Google or GitHub and stay logged in.
**Done when**: JWT from OpenAuth is validated by the API; protected routes redirect unauthenticated users; user row is created on first login. **Done when**: JWT from OpenAuth is validated by the API; protected routes redirect unauthenticated users; user row is created on first login.
@ -57,6 +60,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
--- ---
## Phase 3 — Single-player Mode ## Phase 3 — Single-player Mode
**Goal**: A logged-in user can complete a full solo quiz session. **Goal**: A logged-in user can complete a full solo quiz session.
**Done when**: User sees 10 questions, picks answers, sees their final score. **Done when**: User sees 10 questions, picks answers, sees their final score.
@ -71,6 +75,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
--- ---
## Phase 4 — Multiplayer Rooms (Lobby) ## Phase 4 — Multiplayer Rooms (Lobby)
**Goal**: Players can create and join rooms; the host sees all joined players in real time. **Goal**: Players can create and join rooms; the host sees all joined players in real time.
**Done when**: Two browser tabs can join the same room and see each other's display names update live via WebSocket. **Done when**: Two browser tabs can join the same room and see each other's display names update live via WebSocket.
@ -92,6 +97,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
--- ---
## Phase 5 — Multiplayer Game ## Phase 5 — Multiplayer Game
**Goal**: Host starts a game; all players answer simultaneously in real time; a winner is declared. **Goal**: Host starts a game; all players answer simultaneously in real time; a winner is declared.
**Done when**: 24 players complete a 10-round game with correct live scores and a winner screen. **Done when**: 24 players complete a 10-round game with correct live scores and a winner screen.
@ -110,6 +116,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
--- ---
## Phase 6 — Production Deployment ## Phase 6 — Production Deployment
**Goal**: App is live on Hetzner, accessible via HTTPS on all subdomains. **Goal**: App is live on Hetzner, accessible via HTTPS on all subdomains.
**Done when**: `https://app.yourdomain.com` loads; `wss://api.yourdomain.com` connects; auth flow works end-to-end. **Done when**: `https://app.yourdomain.com` loads; `wss://api.yourdomain.com` connects; auth flow works end-to-end.
@ -122,7 +129,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
--- ---
## Phase 7 — Polish & Hardening *(post-MVP)* ## Phase 7 — Polish & Hardening _(post-MVP)_
Not required to ship, but address before real users arrive. Not required to ship, but address before real users arrive.

View file

@ -15,7 +15,7 @@ A multiplayer EnglishItalian vocabulary trainer with a Duolingo-style quiz in
## 2. Technology Stack ## 2. Technology Stack
| Layer | Technology | | Layer | Technology |
|---|---| | -------------------- | ----------------------------- |
| Monorepo | pnpm workspaces | | Monorepo | pnpm workspaces |
| Frontend | React 18, Vite, TypeScript | | Frontend | React 18, Vite, TypeScript |
| Routing | TanStack Router | | Routing | TanStack Router |
@ -35,12 +35,15 @@ A multiplayer EnglishItalian vocabulary trainer with a Duolingo-style quiz in
| Hosting | Hetzner VPS | | Hosting | Hetzner VPS |
### Why `ws` over Socket.io ### Why `ws` over Socket.io
`ws` is the raw WebSocket library. For rooms of 24 players there is no need for Socket.io's transport fallbacks or room-management abstractions. The protocol is defined explicitly in `packages/shared`, which gives the same guarantees without the overhead. `ws` is the raw WebSocket library. For rooms of 24 players there is no need for Socket.io's transport fallbacks or room-management abstractions. The protocol is defined explicitly in `packages/shared`, which gives the same guarantees without the overhead.
### Why Valkey ### Why Valkey
Valkey stores ephemeral room state that does not need to survive a server restart. It keeps the PostgreSQL schema clean and makes room lookups O(1). Valkey stores ephemeral room state that does not need to survive a server restart. It keeps the PostgreSQL schema clean and makes room lookups O(1).
### Why pnpm workspaces without Turborepo ### Why pnpm workspaces without Turborepo
Turborepo adds parallel task running and build caching on top of pnpm workspaces. For a two-app monorepo of this size, the plain pnpm workspace commands (`pnpm -r run build`, `pnpm --filter`) are sufficient and there is one less tool to configure and maintain. Turborepo adds parallel task running and build caching on top of pnpm workspaces. For a two-app monorepo of this size, the plain pnpm workspace commands (`pnpm -r run build`, `pnpm --filter`) are sufficient and there is one less tool to configure and maintain.
--- ---
@ -80,6 +83,7 @@ vocab-trainer/
### pnpm workspace config ### pnpm workspace config
`pnpm-workspace.yaml` declares: `pnpm-workspace.yaml` declares:
``` ```
packages: packages:
- 'apps/*' - 'apps/*'
@ -89,6 +93,7 @@ packages:
### Root scripts ### Root scripts
The root `package.json` defines convenience scripts that delegate to workspaces: The root `package.json` defines convenience scripts that delegate to workspaces:
- `dev` — starts `api` and `web` in parallel - `dev` — starts `api` and `web` in parallel
- `build` — builds all packages in dependency order - `build` — builds all packages in dependency order
- `test` — runs Vitest across all workspaces - `test` — runs Vitest across all workspaces
@ -124,7 +129,7 @@ Each layer only communicates with the layer directly below it. Business logic li
### Domain structure ### Domain structure
| Subdomain | Service | | Subdomain | Service |
|---|---| | --------------------- | ----------------------- |
| `app.yourdomain.com` | React frontend | | `app.yourdomain.com` | React frontend |
| `api.yourdomain.com` | Express API + WebSocket | | `api.yourdomain.com` | Express API + WebSocket |
| `auth.yourdomain.com` | OpenAuth service | | `auth.yourdomain.com` | OpenAuth service |
@ -132,7 +137,7 @@ Each layer only communicates with the layer directly below it. Business logic li
### Docker Compose services (production) ### Docker Compose services (production)
| Container | Role | | Container | Role |
|---|---| | ---------------- | ------------------------------------------- |
| `postgres` | PostgreSQL 16, named volume | | `postgres` | PostgreSQL 16, named volume |
| `valkey` | Valkey 8, ephemeral (no persistence needed) | | `valkey` | Valkey 8, ephemeral (no persistence needed) |
| `openauth` | OpenAuth service | | `openauth` | OpenAuth service |
@ -155,6 +160,7 @@ SSL is fully automatic via `nginx-proxy` + `acme-companion`. No manual Certbot n
## 6. Data Model ## 6. Data Model
### Design principle ### Design principle
Words are modelled as language-neutral **terms** with one or more **translations** per language. Adding a new language pair (e.g. EnglishFrench) requires **no schema changes** — only new rows in `translations` and `language_pairs`. The flat `english/italian` column pattern is explicitly avoided. Words are modelled as language-neutral **terms** with one or more **translations** per language. Adding a new language pair (e.g. EnglishFrench) requires **no schema changes** — only new rows in `translations` and `language_pairs`. The flat `english/italian` column pattern is explicitly avoided.
### Core tables ### Core tables
@ -222,10 +228,12 @@ CREATE INDEX ON room_players (user_id);
## 7. Vocabulary Data — WordNet + OMW ## 7. Vocabulary Data — WordNet + OMW
### Source ### Source
- **Princeton WordNet** — English words + synset IDs - **Princeton WordNet** — English words + synset IDs
- **Open Multilingual Wordnet (OMW)** — Italian translations keyed by synset ID - **Open Multilingual Wordnet (OMW)** — Italian translations keyed by synset ID
### Extraction process ### Extraction process
1. Run `scripts/extract_omw.py` once locally using NLTK 1. Run `scripts/extract_omw.py` once locally using NLTK
2. Filter to the 1 000 most common nouns (by WordNet frequency data) 2. Filter to the 1 000 most common nouns (by WordNet frequency data)
3. Output: `packages/db/src/seed.json` — committed to the repo 3. Output: `packages/db/src/seed.json` — committed to the repo
@ -244,7 +252,7 @@ The API validates the JWT from OpenAuth on every protected request. User rows ar
**Auth endpoint on the API:** **Auth endpoint on the API:**
| Method | Path | Description | | Method | Path | Description |
|---|---|---| | ------ | -------------- | --------------------------- |
| GET | `/api/auth/me` | Validate token, return user | | GET | `/api/auth/me` | Validate token, return user |
All other auth flows (login, callback, token refresh) are handled entirely by OpenAuth — the frontend redirects to `auth.yourdomain.com` and receives a JWT back. All other auth flows (login, callback, token refresh) are handled entirely by OpenAuth — the frontend redirects to `auth.yourdomain.com` and receives a JWT back.
@ -258,14 +266,14 @@ All endpoints prefixed `/api`. Request and response bodies validated with Zod on
### Vocabulary ### Vocabulary
| Method | Path | Description | | Method | Path | Description |
|---|---|---| | ------ | ---------------------------- | --------------------------------- |
| GET | `/language-pairs` | List active language pairs | | GET | `/language-pairs` | List active language pairs |
| GET | `/terms?pair=en-it&limit=10` | Fetch quiz terms with distractors | | GET | `/terms?pair=en-it&limit=10` | Fetch quiz terms with distractors |
### Rooms ### Rooms
| Method | Path | Description | | Method | Path | Description |
|---|---|---| | ------ | ------------------- | ----------------------------------- |
| POST | `/rooms` | Create a room → returns room + code | | POST | `/rooms` | Create a room → returns room + code |
| GET | `/rooms/:code` | Get current room state | | GET | `/rooms/:code` | Get current room state |
| POST | `/rooms/:code/join` | Join a room | | POST | `/rooms/:code/join` | Join a room |
@ -273,7 +281,7 @@ All endpoints prefixed `/api`. Request and response bodies validated with Zod on
### Users ### Users
| Method | Path | Description | | Method | Path | Description |
|---|---|---| | ------ | ----------------- | ---------------------- |
| GET | `/users/me` | Current user profile | | GET | `/users/me` | Current user profile |
| GET | `/users/me/stats` | Games played, win rate | | GET | `/users/me/stats` | Games played, win rate |
@ -288,7 +296,7 @@ All messages are JSON: `{ type: string, payload: unknown }`. The full set of typ
### Client → Server ### Client → Server
| type | payload | Description | | type | payload | Description |
|---|---|---| | ------------- | -------------------------- | -------------------------------- |
| `room:join` | `{ code }` | Subscribe to a room's WS channel | | `room:join` | `{ code }` | Subscribe to a room's WS channel |
| `room:leave` | — | Unsubscribe | | `room:leave` | — | Unsubscribe |
| `room:start` | — | Host starts the game | | `room:start` | — | Host starts the game |
@ -297,7 +305,7 @@ All messages are JSON: `{ type: string, payload: unknown }`. The full set of typ
### Server → Client ### Server → Client
| type | payload | Description | | type | payload | Description |
|---|---|---| | -------------------- | -------------------------------------------------- | ----------------------------------------- |
| `room:state` | Full room snapshot | Sent on join and on any player join/leave | | `room:state` | Full room snapshot | Sent on join and on any player join/leave |
| `game:question` | `{ id, prompt, options[], timeLimit }` | New question broadcast to all players | | `game:question` | `{ id, prompt, options[], timeLimit }` | New question broadcast to all players |
| `game:answer_result` | `{ questionId, correct, correctAnswerId, scores }` | Broadcast after all answer or timeout | | `game:answer_result` | `{ questionId, correct, correctAnswerId, scores }` | Broadcast after all answer or timeout |
@ -383,7 +391,7 @@ TanStack Query handles all server data fetching. Zustand handles ephemeral UI an
## 13. Testing Strategy ## 13. Testing Strategy
| Type | Tool | Scope | | Type | Tool | Scope |
|---|---|---| | ----------- | -------------------- | --------------------------------------------------- |
| Unit | Vitest | Services, QuizService distractor logic, Zod schemas | | Unit | Vitest | Services, QuizService distractor logic, Zod schemas |
| Component | Vitest + RTL | QuestionCard, OptionButton, auth forms | | Component | Vitest + RTL | QuestionCard, OptionButton, auth forms |
| Integration | Vitest | API route handlers against a test DB | | Integration | Vitest | API route handlers against a test DB |
@ -392,6 +400,7 @@ TanStack Query handles all server data fetching. Zustand handles ephemeral UI an
Tests are co-located with source files (`*.test.ts` / `*.test.tsx`). Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
**Critical paths to cover:** **Critical paths to cover:**
- Distractor generation (correct POS, no duplicates, never includes answer) - Distractor generation (correct POS, no duplicates, never includes answer)
- Answer validation (server-side, correct scoring) - Answer validation (server-side, correct scoring)
- Game session lifecycle (create → play → complete) - Game session lifecycle (create → play → complete)
@ -402,6 +411,7 @@ Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
## 14. Definition of Done ## 14. Definition of Done
### Functional ### Functional
- [ ] User can log in via Google or GitHub (OpenAuth) - [ ] User can log in via Google or GitHub (OpenAuth)
- [ ] User can play singleplayer: 10 rounds, score, result screen - [ ] User can play singleplayer: 10 rounds, score, result screen
- [ ] User can create a room and share a code - [ ] User can create a room and share a code
@ -410,6 +420,7 @@ Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
- [ ] 1 000 EnglishItalian words seeded from WordNet + OMW - [ ] 1 000 EnglishItalian words seeded from WordNet + OMW
### Technical ### Technical
- [ ] Deployed to Hetzner with HTTPS on all three subdomains - [ ] Deployed to Hetzner with HTTPS on all three subdomains
- [ ] Docker Compose running all services - [ ] Docker Compose running all services
- [ ] Drizzle migrations applied on container start - [ ] Drizzle migrations applied on container start
@ -417,6 +428,7 @@ Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
- [ ] pnpm workspace build pipeline green - [ ] pnpm workspace build pipeline green
### Documentation ### Documentation
- [ ] `SPEC.md` complete - [ ] `SPEC.md` complete
- [ ] `.env.example` files for all apps - [ ] `.env.example` files for all apps
- [ ] `README.md` with local dev setup instructions - [ ] `README.md` with local dev setup instructions
@ -425,12 +437,12 @@ Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
## 15. Out of Scope (MVP) ## 15. Out of Scope (MVP)
- Difficulty levels *(`frequency_rank` column exists, ready to use)* - Difficulty levels _(`frequency_rank` column exists, ready to use)_
- Additional language pairs *(schema already supports it — just add rows)* - Additional language pairs _(schema already supports it — just add rows)_
- Leaderboards *(`games_played`, `games_won` columns exist)* - Leaderboards _(`games_played`, `games_won` columns exist)_
- Streaks / daily challenges - Streaks / daily challenges
- Friends / private invites - Friends / private invites
- Audio pronunciation - Audio pronunciation
- CI/CD pipeline (manual deploy for now) - CI/CD pipeline (manual deploy for now)
- Rate limiting *(add before going public)* - Rate limiting _(add before going public)_
- Admin panel for vocabulary management - Admin panel for vocabulary management

View file

@ -6,7 +6,9 @@
"scripts": { "scripts": {
"dev": "concurrently \"pnpm --filter @glossa/web run dev\" \"pnpm --filter @glossa/api run dev\"", "dev": "concurrently \"pnpm --filter @glossa/web run dev\" \"pnpm --filter @glossa/api run dev\"",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint ." "lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
}, },
"packageManager": "pnpm@10.32.1", "packageManager": "pnpm@10.32.1",
"devDependencies": { "devDependencies": {
@ -14,6 +16,7 @@
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"eslint": "^10.0.3", "eslint": "^10.0.3",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.57.1" "typescript-eslint": "^8.57.1"
} }

View file

@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"outDir": "./dist", "outDir": "./dist"
}, },
"include": ["src"], "include": ["src"]
} }

View file

@ -3,7 +3,7 @@
"compilerOptions": { "compilerOptions": {
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"outDir": "./dist", "outDir": "./dist"
}, },
"include": ["src"], "include": ["src"]
} }

10
pnpm-lock.yaml generated
View file

@ -20,6 +20,9 @@ importers:
eslint-config-prettier: eslint-config-prettier:
specifier: ^10.1.8 specifier: ^10.1.8
version: 10.1.8(eslint@10.0.3) version: 10.1.8(eslint@10.0.3)
prettier:
specifier: ^3.8.1
version: 3.8.1
typescript: typescript:
specifier: ^5.9.3 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
@ -425,6 +428,11 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
prettier@3.8.1:
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
engines: {node: '>=14'}
hasBin: true
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -917,6 +925,8 @@ snapshots:
prelude-ls@1.2.1: {} prelude-ls@1.2.1: {}
prettier@3.8.1: {}
punycode@2.3.1: {} punycode@2.3.1: {}
require-directory@2.1.1: {} require-directory@2.1.1: {}

View file

@ -24,6 +24,6 @@
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"target": "es2022", "target": "es2022",
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true
}, }
} }

View file

@ -3,7 +3,7 @@
{ "path": "./packages/shared" }, { "path": "./packages/shared" },
{ "path": "./packages/db" }, { "path": "./packages/db" },
{ "path": "./apps/web" }, { "path": "./apps/web" },
{ "path": "./apps/api" }, { "path": "./apps/api" }
], ],
"files": [], "files": []
} }