chore: configure prettier with ignore rules and format scripts + running format
This commit is contained in:
parent
22bb8a1e4c
commit
ce42eb1811
12 changed files with 150 additions and 89 deletions
19
.prettierignore
Normal file
19
.prettierignore
Normal 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
13
.prettierrc
Normal 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"
|
||||
}
|
||||
|
|
@ -2,11 +2,8 @@
|
|||
"extends": "../../tsconfig.base.json",
|
||||
"references": [
|
||||
{ "path": "../../packages/shared" },
|
||||
{ "path": "../../packages/db" },
|
||||
{ "path": "../../packages/db" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
},
|
||||
"include": ["src"],
|
||||
"compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" },
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"references": [{ "path": "../../packages/shared" }],
|
||||
"references": [{ "path": "../../packages/shared" }]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 0 — Foundation
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -23,6 +24,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 1 — Vocabulary Data
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -39,6 +41,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 2 — Auth
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -57,6 +60,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 3 — Single-player Mode
|
||||
|
||||
**Goal**: A logged-in user can complete a full solo quiz session.
|
||||
**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)
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -92,6 +97,7 @@ Each phase produces a working, deployable increment. Nothing is built speculativ
|
|||
---
|
||||
|
||||
## Phase 5 — Multiplayer Game
|
||||
|
||||
**Goal**: Host starts a game; all players answer simultaneously in real time; a winner is declared.
|
||||
**Done when**: 2–4 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
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -14,33 +14,36 @@ A multiplayer English–Italian vocabulary trainer with a Duolingo-style quiz in
|
|||
|
||||
## 2. Technology Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|---|---|
|
||||
| Monorepo | pnpm workspaces |
|
||||
| Frontend | React 18, Vite, TypeScript |
|
||||
| Routing | TanStack Router |
|
||||
| Server state | TanStack Query |
|
||||
| Client state | Zustand |
|
||||
| Styling | Tailwind CSS + shadcn/ui |
|
||||
| Backend | Node.js, Express, TypeScript |
|
||||
| Realtime | WebSockets (`ws` library) |
|
||||
| Database | PostgreSQL 16 |
|
||||
| ORM | Drizzle ORM |
|
||||
| Cache / Queue | Valkey 8 |
|
||||
| Auth | OpenAuth (Google + GitHub) |
|
||||
| Validation | Zod (shared schemas) |
|
||||
| Testing | Vitest, React Testing Library |
|
||||
| Linting / Formatting | ESLint, Prettier |
|
||||
| Containerisation | Docker, Docker Compose |
|
||||
| Hosting | Hetzner VPS |
|
||||
| Layer | Technology |
|
||||
| -------------------- | ----------------------------- |
|
||||
| Monorepo | pnpm workspaces |
|
||||
| Frontend | React 18, Vite, TypeScript |
|
||||
| Routing | TanStack Router |
|
||||
| Server state | TanStack Query |
|
||||
| Client state | Zustand |
|
||||
| Styling | Tailwind CSS + shadcn/ui |
|
||||
| Backend | Node.js, Express, TypeScript |
|
||||
| Realtime | WebSockets (`ws` library) |
|
||||
| Database | PostgreSQL 16 |
|
||||
| ORM | Drizzle ORM |
|
||||
| Cache / Queue | Valkey 8 |
|
||||
| Auth | OpenAuth (Google + GitHub) |
|
||||
| Validation | Zod (shared schemas) |
|
||||
| Testing | Vitest, React Testing Library |
|
||||
| Linting / Formatting | ESLint, Prettier |
|
||||
| Containerisation | Docker, Docker Compose |
|
||||
| Hosting | Hetzner VPS |
|
||||
|
||||
### Why `ws` over Socket.io
|
||||
|
||||
`ws` is the raw WebSocket library. For rooms of 2–4 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
|
||||
|
||||
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
|
||||
|
||||
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.yaml` declares:
|
||||
|
||||
```
|
||||
packages:
|
||||
- 'apps/*'
|
||||
|
|
@ -89,6 +93,7 @@ packages:
|
|||
### Root scripts
|
||||
|
||||
The root `package.json` defines convenience scripts that delegate to workspaces:
|
||||
|
||||
- `dev` — starts `api` and `web` in parallel
|
||||
- `build` — builds all packages in dependency order
|
||||
- `test` — runs Vitest across all workspaces
|
||||
|
|
@ -123,23 +128,23 @@ Each layer only communicates with the layer directly below it. Business logic li
|
|||
|
||||
### Domain structure
|
||||
|
||||
| Subdomain | Service |
|
||||
|---|---|
|
||||
| `app.yourdomain.com` | React frontend |
|
||||
| `api.yourdomain.com` | Express API + WebSocket |
|
||||
| `auth.yourdomain.com` | OpenAuth service |
|
||||
| Subdomain | Service |
|
||||
| --------------------- | ----------------------- |
|
||||
| `app.yourdomain.com` | React frontend |
|
||||
| `api.yourdomain.com` | Express API + WebSocket |
|
||||
| `auth.yourdomain.com` | OpenAuth service |
|
||||
|
||||
### Docker Compose services (production)
|
||||
|
||||
| Container | Role |
|
||||
|---|---|
|
||||
| `postgres` | PostgreSQL 16, named volume |
|
||||
| `valkey` | Valkey 8, ephemeral (no persistence needed) |
|
||||
| `openauth` | OpenAuth service |
|
||||
| `api` | Express + WS server |
|
||||
| `web` | Nginx serving the Vite build |
|
||||
| `nginx-proxy` | Automatic reverse proxy |
|
||||
| `acme-companion` | Let's Encrypt certificate automation |
|
||||
| Container | Role |
|
||||
| ---------------- | ------------------------------------------- |
|
||||
| `postgres` | PostgreSQL 16, named volume |
|
||||
| `valkey` | Valkey 8, ephemeral (no persistence needed) |
|
||||
| `openauth` | OpenAuth service |
|
||||
| `api` | Express + WS server |
|
||||
| `web` | Nginx serving the Vite build |
|
||||
| `nginx-proxy` | Automatic reverse proxy |
|
||||
| `acme-companion` | Let's Encrypt certificate automation |
|
||||
|
||||
```
|
||||
nginx-proxy (:80/:443)
|
||||
|
|
@ -155,6 +160,7 @@ SSL is fully automatic via `nginx-proxy` + `acme-companion`. No manual Certbot n
|
|||
## 6. Data Model
|
||||
|
||||
### Design principle
|
||||
|
||||
Words are modelled as language-neutral **terms** with one or more **translations** per language. Adding a new language pair (e.g. English–French) requires **no schema changes** — only new rows in `translations` and `language_pairs`. The flat `english/italian` column pattern is explicitly avoided.
|
||||
|
||||
### Core tables
|
||||
|
|
@ -222,10 +228,12 @@ CREATE INDEX ON room_players (user_id);
|
|||
## 7. Vocabulary Data — WordNet + OMW
|
||||
|
||||
### Source
|
||||
|
||||
- **Princeton WordNet** — English words + synset IDs
|
||||
- **Open Multilingual Wordnet (OMW)** — Italian translations keyed by synset ID
|
||||
|
||||
### Extraction process
|
||||
|
||||
1. Run `scripts/extract_omw.py` once locally using NLTK
|
||||
2. Filter to the 1 000 most common nouns (by WordNet frequency data)
|
||||
3. Output: `packages/db/src/seed.json` — committed to the repo
|
||||
|
|
@ -243,9 +251,9 @@ The API validates the JWT from OpenAuth on every protected request. User rows ar
|
|||
|
||||
**Auth endpoint on the API:**
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/auth/me` | Validate token, return user |
|
||||
| Method | Path | Description |
|
||||
| ------ | -------------- | --------------------------- |
|
||||
| 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.
|
||||
|
||||
|
|
@ -257,25 +265,25 @@ All endpoints prefixed `/api`. Request and response bodies validated with Zod on
|
|||
|
||||
### Vocabulary
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/language-pairs` | List active language pairs |
|
||||
| GET | `/terms?pair=en-it&limit=10` | Fetch quiz terms with distractors |
|
||||
| Method | Path | Description |
|
||||
| ------ | ---------------------------- | --------------------------------- |
|
||||
| GET | `/language-pairs` | List active language pairs |
|
||||
| GET | `/terms?pair=en-it&limit=10` | Fetch quiz terms with distractors |
|
||||
|
||||
### Rooms
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | `/rooms` | Create a room → returns room + code |
|
||||
| GET | `/rooms/:code` | Get current room state |
|
||||
| POST | `/rooms/:code/join` | Join a room |
|
||||
| Method | Path | Description |
|
||||
| ------ | ------------------- | ----------------------------------- |
|
||||
| POST | `/rooms` | Create a room → returns room + code |
|
||||
| GET | `/rooms/:code` | Get current room state |
|
||||
| POST | `/rooms/:code/join` | Join a room |
|
||||
|
||||
### Users
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/users/me` | Current user profile |
|
||||
| GET | `/users/me/stats` | Games played, win rate |
|
||||
| Method | Path | Description |
|
||||
| ------ | ----------------- | ---------------------- |
|
||||
| GET | `/users/me` | Current user profile |
|
||||
| GET | `/users/me/stats` | Games played, win rate |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -287,22 +295,22 @@ All messages are JSON: `{ type: string, payload: unknown }`. The full set of typ
|
|||
|
||||
### Client → Server
|
||||
|
||||
| type | payload | Description |
|
||||
|---|---|---|
|
||||
| `room:join` | `{ code }` | Subscribe to a room's WS channel |
|
||||
| `room:leave` | — | Unsubscribe |
|
||||
| `room:start` | — | Host starts the game |
|
||||
| `game:answer` | `{ questionId, answerId }` | Player submits an answer |
|
||||
| type | payload | Description |
|
||||
| ------------- | -------------------------- | -------------------------------- |
|
||||
| `room:join` | `{ code }` | Subscribe to a room's WS channel |
|
||||
| `room:leave` | — | Unsubscribe |
|
||||
| `room:start` | — | Host starts the game |
|
||||
| `game:answer` | `{ questionId, answerId }` | Player submits an answer |
|
||||
|
||||
### Server → Client
|
||||
|
||||
| type | payload | Description |
|
||||
|---|---|---|
|
||||
| `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:answer_result` | `{ questionId, correct, correctAnswerId, scores }` | Broadcast after all answer or timeout |
|
||||
| `game:finished` | `{ scores[], winner }` | End of game summary |
|
||||
| `error` | `{ message }` | Protocol or validation error |
|
||||
| type | payload | Description |
|
||||
| -------------------- | -------------------------------------------------- | ----------------------------------------- |
|
||||
| `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:answer_result` | `{ questionId, correct, correctAnswerId, scores }` | Broadcast after all answer or timeout |
|
||||
| `game:finished` | `{ scores[], winner }` | End of game summary |
|
||||
| `error` | `{ message }` | Protocol or validation error |
|
||||
|
||||
### Multiplayer game mechanic — simultaneous answers
|
||||
|
||||
|
|
@ -382,16 +390,17 @@ TanStack Query handles all server data fetching. Zustand handles ephemeral UI an
|
|||
|
||||
## 13. Testing Strategy
|
||||
|
||||
| Type | Tool | Scope |
|
||||
|---|---|---|
|
||||
| Unit | Vitest | Services, QuizService distractor logic, Zod schemas |
|
||||
| Component | Vitest + RTL | QuestionCard, OptionButton, auth forms |
|
||||
| Integration | Vitest | API route handlers against a test DB |
|
||||
| E2E | Out of scope for MVP | — |
|
||||
| Type | Tool | Scope |
|
||||
| ----------- | -------------------- | --------------------------------------------------- |
|
||||
| Unit | Vitest | Services, QuizService distractor logic, Zod schemas |
|
||||
| Component | Vitest + RTL | QuestionCard, OptionButton, auth forms |
|
||||
| Integration | Vitest | API route handlers against a test DB |
|
||||
| E2E | Out of scope for MVP | — |
|
||||
|
||||
Tests are co-located with source files (`*.test.ts` / `*.test.tsx`).
|
||||
|
||||
**Critical paths to cover:**
|
||||
|
||||
- Distractor generation (correct POS, no duplicates, never includes answer)
|
||||
- Answer validation (server-side, correct scoring)
|
||||
- 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
|
||||
|
||||
### Functional
|
||||
|
||||
- [ ] User can log in via Google or GitHub (OpenAuth)
|
||||
- [ ] User can play singleplayer: 10 rounds, score, result screen
|
||||
- [ ] 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 English–Italian words seeded from WordNet + OMW
|
||||
|
||||
### Technical
|
||||
|
||||
- [ ] Deployed to Hetzner with HTTPS on all three subdomains
|
||||
- [ ] Docker Compose running all services
|
||||
- [ ] 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
|
||||
|
||||
### Documentation
|
||||
|
||||
- [ ] `SPEC.md` complete
|
||||
- [ ] `.env.example` files for all apps
|
||||
- [ ] `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)
|
||||
|
||||
- Difficulty levels *(`frequency_rank` column exists, ready to use)*
|
||||
- Additional language pairs *(schema already supports it — just add rows)*
|
||||
- Leaderboards *(`games_played`, `games_won` columns exist)*
|
||||
- Difficulty levels _(`frequency_rank` column exists, ready to use)_
|
||||
- Additional language pairs _(schema already supports it — just add rows)_
|
||||
- Leaderboards _(`games_played`, `games_won` columns exist)_
|
||||
- Streaks / daily challenges
|
||||
- Friends / private invites
|
||||
- Audio pronunciation
|
||||
- CI/CD pipeline (manual deploy for now)
|
||||
- Rate limiting *(add before going public)*
|
||||
- Rate limiting _(add before going public)_
|
||||
- Admin panel for vocabulary management
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
"scripts": {
|
||||
"dev": "concurrently \"pnpm --filter @glossa/web run dev\" \"pnpm --filter @glossa/api run dev\"",
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"lint": "eslint ."
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check ."
|
||||
},
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"devDependencies": {
|
||||
|
|
@ -14,6 +16,7 @@
|
|||
"concurrently": "^9.2.1",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.57.1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
|
|||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -20,6 +20,9 @@ importers:
|
|||
eslint-config-prettier:
|
||||
specifier: ^10.1.8
|
||||
version: 10.1.8(eslint@10.0.3)
|
||||
prettier:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
|
|
@ -425,6 +428,11 @@ packages:
|
|||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
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:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -917,6 +925,8 @@ snapshots:
|
|||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@3.8.1: {}
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,6 @@
|
|||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "es2022",
|
||||
"verbatimModuleSyntax": true,
|
||||
},
|
||||
"verbatimModuleSyntax": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{ "path": "./packages/shared" },
|
||||
{ "path": "./packages/db" },
|
||||
{ "path": "./apps/web" },
|
||||
{ "path": "./apps/api" },
|
||||
{ "path": "./apps/api" }
|
||||
],
|
||||
"files": [],
|
||||
"files": []
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue