|
|
||
|---|---|---|
| .claude | ||
| .github/workflows | ||
| apps | ||
| packages | ||
| specs | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .nvmrc | ||
| biome.json | ||
| docker-compose.yml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.base.json | ||
batchCooking
Structure
Monorepo pnpm workspaces :
apps/api— backend Express/TypeScript (squelette générique : healthcheck, config env, Prisma non modélisé, tests Mocha + Cucumber/BDD)apps/web— frontend React/Vite/TypeScript, prêt à être embarqué par Capacitor plus tard. Page de connexion/inscription en place ; le reste est encore un squelette générique.packages/shared— code partagé entreapietweb: schémas zod (signupSchema,loginSchema), types (SafeUserProfile), et le contrat d'erreurs (ErrorCodenumérique,ApiErrorResponse, voir specs/error-handling.md) — même règles des deux côtés, pas de risque de dérive entre front et back.packages/error-tools— gestion des erreurs, indépendante de tout framework HTTP (n'importe pasexpress) :HttpError,ErrorHandlerService. Séparé d'express-toolsprécisément parce que rien ici ne dépend d'Express. Détail : specs/error-handling.md.packages/express-tools— outillage Express générique et réutilisable :ExpressServer(init serveur, routes, middlewares),wrapAsyncHandler,createErrorMiddleware(adapteErrorHandlerServicedeerror-toolsà Express) — séparé d'apps/api, pas de logique métier. Détail : specs/backend-architecture.md.
packages/shared, packages/error-tools et packages/express-tools ont un vrai
build (tsc → dist/, voir leur package.json) : consommés en JS compilé, pas en
TS brut — nécessaire pour un runtime Node pur (Docker, pas de transpilation à la
volée), voir la note dans
specs/frontend-architecture.md.
Prérequis
- Node.js 22 (voir
.nvmrc) - pnpm 10 (
corepack enablepuiscorepack use pnpm@10.12.4, ou installation manuelle) - Docker (pour Postgres en local)
Installation
pnpm install
cp .env.example .env
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env
Puis édite ces deux .env pour renseigner de vrais POSTGRES_USER/POSTGRES_PASSWORD
(et la DATABASE_URL correspondante dans apps/api/.env) : les fichiers .env.example
ne contiennent volontairement aucun identifiant réel (juste changeme), et
docker-compose.yml refuse de démarrer tant que POSTGRES_USER/PASSWORD/DB ne
sont pas définis dans .env — pas de valeur par défaut en dur dans les fichiers commités.
Même règle pour apps/api/.env : JWT_SECRET est requis, sans défaut (génère le
tien, voir le commentaire dans apps/api/.env.example).
Cypress : téléchargement du binaire
pnpm install installe le package cypress mais pas forcément son binaire (le
téléchargement du .exe/binaire natif peut être ignoré selon l'environnement où
pnpm install a été lancé — ex. un environnement sandboxé/CI dont le cache ne
correspond pas à celui de ta machine). Si pnpm --filter web e2e échoue avec une
erreur du type :
No version of Cypress is installed in: ...\AppData\Local\Cypress\Cache\...
Please reinstall Cypress by running: cypress install
lance simplement, depuis ta machine :
pnpm --filter web exec cypress install
(à faire une seule fois par machine ; le binaire est mis en cache localement, hors du repo).
Développement
# Base de données Postgres locale
docker compose up -d
# Applique le schéma (première fois / après un changement de prisma/schema.prisma)
pnpm --filter api exec prisma migrate dev
# Backend (http://localhost:3000)
pnpm dev:api
# Frontend (http://localhost:5173)
pnpm dev:web
Conflit de port possible sur
5432: si tu as déjà un Postgres natif installé sur ta machine (service Windows, Homebrew, etc.), il peut occuper le port 5432 et intercepter les connexions à la place du conteneur Docker (symptôme : Prisma renvoieP1000: Authentication failedalors que les identifiants sont corrects). Dans ce cas, metsPOSTGRES_PORT=5433(ou autre) dans ton.envet adapte le port dans laDATABASE_URLdeapps/api/.env.
Qualité / Tests
pnpm lint # Biome (lint + format check)
pnpm lint:fix # Biome --write
pnpm test # tests unitaires/intégration (Mocha, apps/api)
pnpm --filter api test:bdd # tests d'intégration BDD (Cucumber/Gherkin, apps/api)
pnpm --filter web e2e # tests e2e (Cypress, démarre le serveur dev automatiquement)
pnpm build # build de tous les workspaces
La CI GitHub Actions (.github/workflows/ci.yml) exécute lint + tests + build sur chaque push/PR vers main, puis les tests e2e Cypress.
Cucumber (apps/api)
Tests d'intégration lisibles en Gherkin, en complément de Mocha (qui reste pour les tests unitaires purs) :
apps/api/features/*.feature— scénarios en Given/When/Then (health.featuresert d'exemple)apps/api/features/step-definitions/*.steps.ts— implémentation des stepsapps/api/features/support/world.ts— contexte partagé entre les steps d'un scénario (instancie l'app Express in-process viacreateApp(), comme le fait déjà supertest côté Mocha — pas besoin de lancer un vrai serveur)apps/api/cucumber.cjs— config (extension.cjsvolontaire, voir la remarque TypeScript/ESM ci-dessous)
Pour ajouter un scénario : écrire le .feature, lancer pnpm --filter api test:bdd,
implémenter les steps manquants (Cucumber affiche des snippets tout prêts pour ceux
qui n'existent pas encore).
Piège TypeScript/ESM à connaître (déjà rencontré avec
cypress.config.ts) : les fichiers de config d'outils tiers qui font du chargement dynamique de TS (cucumber.cjs,cypress.config.ts…) sont sensibles au"type": "module"dupackage.json.cucumber.cjsévite le problème pour sa propre config en étant explicitement CommonJS ; les steps/world restent en.tsESM classique et sont chargés viatsx(NODE_OPTIONS=--import=tsx, voir le scripttest:bdd).
Auth (apps/api)
Inscription (création de profil + foyer) et connexion, JWT dans un cookie httpOnly.
POST /auth/signup—{ firstName, lastName, email, password }→ crée le foyer (house) et le profil (user_profiles) en une transaction, pose le cookie de session, renvoie le profil (201)POST /auth/login—{ email, password }→ pose le cookie de session, renvoie le profil (200) ; message d'erreur volontairement générique (401) que ce soit l'email ou le mot de passe qui soit incorrectPOST /auth/logout— efface le cookie (204)GET /auth/me— profil courant, nécessite le cookie de session (401 sinon)
Mots de passe hachés avec argon2. Le hash est indépendant du foyer : un profil crée toujours son propre foyer à l'inscription (rejoindre un foyer existant n'est pas encore implémenté).
argon2 : version pinnée à
0.31.2, pas de^. La version0.45.1(dernière au moment de l'écriture) segfault au runtime sur au moins une configuration Windows — reproduit de façon stable (bash sandboxé, bash non-sandboxé, PowerShell), alors que0.31.2fonctionne parfaitement avec la même API. Si tu montes la version, revérifie concrètement (argon2.hash(...)dans unnode -e) avant de merger, unpnpm buildqui passe ne suffit pas à détecter un crash runtime.
Les tests (Mocha + Cucumber) tournent avec un coût argon2 réduit
(NODE_ENV=test, voir auth.service.ts) — le coût par défaut est volontairement
élevé (sécurité), ce qui rendrait la suite de tests lente/instable sinon. La CI
provisionne un vrai Postgres de service (.github/workflows/ci.yml) et exécute
prisma migrate deploy avant les tests.
Les tests automatisés et
pnpm dev:apipartagent la même base Postgres locale. Lancerpnpm test/test:bddvideuser_profiles/house(TRUNCATE ... CASCADE, voirtest-support/reset-db.ts) — si tu es en train de tester manuellement à la main (via le navigateur ou curl) contre le serveur de dev, un run de tests en parallèle efface tes données de test sans prévenir. Pas un bug, juste à savoir.
Planning (apps/api)
GET /planning/current— nécessite le cookie de session (401 sinon). Renvoie le planning du foyer de l'utilisateur connecté qui couvre la date du jour (Planningdontstart_date <= aujourd'hui <= finish_date), items inclus avec leur recette résolue en{ id, name }— ounulls'il n'y en a aucun (foyer sans planning en cours, ou profil sans foyer).nullest une réponse valide (200), pas une erreur : aujourd'hui rien ne permet encore de créer un planning (le module « Calcul batch-cooking », voir specs/batch-cooking-architecture.md, reste à construire), donc c'est l'état attendu tant que ce module n'existe pas.- Type de réponse partagé :
PlanningView(packages/shared/src/types/planning.ts), consommé tel quel parapps/web.
Détail de AsyncRequestHandler/wrapAsyncHandler (packages/express-tools) —
premier endpoint à combiner requireAuth/AuthLocals avec un handler async, ce qui
a mis au jour une contrainte générique trop stricte, corrigée à la source :
specs/backend-architecture.md.
Page de connexion / inscription (apps/web)
src/api/client.ts—ApiClient(classe, instance unique exportéeapiClient) : enveloppefetchvers l'API (credentials: "include", requis pour que le cookie de session httpOnly parte/revienne — l'API et le front sont sur des origines différentes). URL configurable viaVITE_API_URL(voir.env.example).src/features/auth/AuthContext.tsx— état d'auth global ; appelleGET /auth/meau chargement pour restaurer la session depuis le cookie.src/features/auth/RequireAuth.tsx/RedirectIfAuthenticated.tsx— gardes de route (react-router-dom) :/exige d'être connecté,/loginet/signupredirigent vers/si on l'est déjà.src/pages/{Login,Signup,Home}Page.tsx— validation client instantanée via les schémas zod partagés (packages/shared), erreurs API traduites viaErrorMessageService(voir ci-dessous).
Détail de l'organisation complète (dossiers, routing, SCSS/theming) : specs/frontend-architecture.md.
Accueil, sidebar & sections (apps/web)
Une fois connecté, l'utilisateur atterrit sur src/layouts/AppLayout.tsx — sidebar
(nav Planning/Recettes/Liste de courses/Foyer & profil + nom/déconnexion en pied) et
<Outlet /> pour la route active — montée une seule fois comme route parente de tout
l'espace authentifié (App.tsx), pas dupliquée par page. src/pages/HomePage.tsx
(routée sur /) affiche le planning de la semaine du foyer (GET /planning/current,
voir plus haut) avec ses états chargement/erreur/vide/rempli ; Recettes, Liste de courses et Foyer & profil n'ont pas encore de backend dédié et rendent pour
l'instant le même composant ComingSoonPage. Détail complet (pourquoi une seule
route parente, pourquoi un composant stub partagé) :
specs/frontend-architecture.md.
Tests Cypress (apps/web/cypress/e2e/) : smoke.cy.ts + auth.cy.ts +
home-planning.cy.ts mockent l'API via cy.intercept plutôt que de dépendre d'un
vrai backend — le job e2e de la CI ne provisionne pas de Postgres/API, seulement le
serveur de dev Vite. Le comportement réel de l'API est couvert par les suites
Mocha/Cucumber d'apps/api (contre une vraie base).
Cypress ne peut pas tourner en local dans un environnement Windows sandboxé : Chromium/Electron headless plante au lancement du process GPU (
GPU process isn't usable), reproductible surmainaussi bien que sur une branche de feature — pas un problème introduit par une modification du code.pnpm --filter web e2efonctionne normalement en CI (GitHub Actions) et sur une machine de dev classique ; dans cet environnement précis, vérifier manuellement via le serveur de dev (pnpm dev:web+pnpm dev:apien local, pas les conteneurs Docker dont leCORS_ORIGINciblelocalhost:8080, paslocalhost:5173).
Gestion des erreurs (API ↔ web)
Contrat d'erreurs partagé via packages/shared (ErrorCode, énumération
numérique groupée par famille — 4000 validation, 401x auth, 404x not
found, 500x interne — et ApiErrorResponse) : l'API renvoie toujours
{ code, message, details? } (message en anglais, dev-facing — jamais affiché tel
quel), et le client traduit code en libellé français via i18next
(ErrorMessageService, apps/web/src/services/error-message.service.ts →
apps/web/src/locales/fr/translation.json). Côté API, ErrorHandlerService
(packages/error-tools) et createErrorMiddleware (packages/express-tools)
centralisent la transformation de toute erreur levée en réponse HTTP conforme —
aucune valeur ErrorCode codée en dur nulle part (toujours ErrorCode.XXX, y
compris dans les mocks Cypress).
Détail complet (schéma, exemples, comment ajouter un nouveau code d'erreur) : specs/error-handling.md.
Le profil authentifié (requireAuth) passe par res.locals.userProfile
(typé via AuthLocals), pas par une augmentation du namespace global Express —
voir specs/backend-architecture.md pour le détail
et le pourquoi.
packages/shared fournit aussi assertIsNever (vérification d'exhaustivité de
switch/if-chain sur une union, erreur de compilation si un cas est oublié) —
voir specs/backend-architecture.md.
i18n
i18next + react-i18next — tout le texte affiché (formulaires, boutons,
erreurs) vient de fichiers de locale JSON (apps/web/src/locales/<lng>/translation.json),
jamais codé en dur dans un composant. Une seule langue existe aujourd'hui (fr) ;
en ajouter une est une question de fichier de locale, pas de code. Détail :
specs/frontend-architecture.md.
Données de test (faker.js)
apps/api utilise @faker-js/faker pour toutes les données
de test dans test/auth.test.ts (Mocha) et le "bruit" (prénom/nom de remplissage)
des steps Cucumber — jamais de nom/email qui ressemble à une vraie personne en dur
dans un fixture. Les valeurs littérales des scénarios .feature eux-mêmes
(ex. alice@example.com) restent volontairement statiques : c'est le point des
scénarios Gherkin lisibles (exemples illustratifs conventionnels en BDD, pas des
données réelles) — seules les données de remplissage hors du texte lisible du
scénario sont générées.