📐 Construire une app en JS
23. Le monorepo

Le monorepo

Pourquoi React ?

Parce qu'il faut bien commencer à créer notre page avec quelque chose.

D'autres choix seraient tout aussi légitimes. Mais nous voulons aller vite.

Mono-repo

Mais mieux vaut ne pas tout mélanger. Gardons précautionneusement notre code React à l'écart de notre App. Séparons donc les 2 environnements, comme s'il s'agissait de 2 dépôts de code distincts. Pour cela, nous allons utiliser les workspaces du gestionnaire de packages yarn.

NB : là aussi, d'autres choix seraient tout aussi légitimes. Faisons juste en sorte que ces outils soient faciles à remplacer.

À la racine de notre projet, écrivons ce fichier package.json :

{
  "name": "projet-booking",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

(Nous utilisons ici Yarn 1. Avec Yarn 2+ ou pnpm, la syntaxe des dépendances internes diffère légèrement — nous y revenons plus bas.)

Avec ces quelques lignes, nous avons juste signifié à yarn que le répertoire packages contenait plusieurs dépôts de code indépendants.

Puis transférons notre code du backend dans un sous-répertoire core de packages. Notre arborescence ressemble désormais à cela :

projet-booking
 |- package.json              <-- celui que nous venons de créer
 |- packages
    |- core
       |- package.json        <-- celui qui existait déjà (avec 1 dépendance : vitest)
       |- index.js            <-- nouveau : le point d'entrée du package
       |- domain
          |- app
             |- App.js
             |- Context.js
          |- values
             |- Result.js
             |- CalendarDay.js
             |- Stay.js
             |- Occupancy.js
          |- tests
             |- book.test.js
             |- values.test.js
          |- usecases
             |- book.js
             |- login.js
       |- infra
          |- MemoryBookingRepository.js
          |- MemoryUserRepository.js
          |- systemDateProvider.js
          |- testDependencies.js

Important : supprimons le fichier yarn.lock et le répertoire node_modules. Ils seront recréés automatiquement, à la racine cette fois.

Nous allons en profiter pour modifier un peu notre fichier package.json dans le répertoire packages/core. Attribuons-lui un nom @booking/core pour le distinguer :

{
  "name": "@booking/core",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "test": "vitest"
  },
  "devDependencies": {
    "vitest": "^1.6.0"
  }
}

Deux lignes méritent qu'on s'y arrête, parce qu'un oubli ici produit des erreurs incompréhensibles au premier import.

"type": "module" : notre code utilise import / export. Sans cette ligne, Node considère les .js comme du CommonJS.

"main": "index.js" : c'est le fichier que Node ira chercher quand la webapp écrira import { App } from "@booking/core". Or ce fichier n'existe pas encore ! Créons-le, et faisons-en la façade publique de notre domaine — ce que le package expose, et rien d'autre :

// packages/core/index.js
export { App } from "./domain/app/App.js";
export { book } from "./domain/usecases/book.js";
export { login } from "./domain/usecases/login.js";
export { Stay } from "./domain/values/Stay.js";
export { CalendarDay } from "./domain/values/CalendarDay.js";
export { testDependencies } from "./infra/testDependencies.js";

Ce fichier est plus qu'une formalité technique : c'est un contrat. Tout ce qui n'y figure pas reste privé, et pourra être renommé ou supprimé sans prévenir personne.

Création du package React

Créons désormais notre package React avec la commande :

cd packages
yarn create vite webapp --template react
cd ../..   # on remonte à la racine du monorepo
yarn       # l'installation se fait TOUJOURS à la racine, jamais dans un package

Cette commande ajoute un second répertoire webapp dans packages.

Attention à la dernière ligne. Dans un monorepo, les dépendances sont mutualisées à la racine : lancer yarn depuis packages/webapp créerait un node_modules local et un second yarn.lock, et vous passeriez la soirée à vous demander pourquoi @booking/core reste introuvable.

projet-booking
 |- package.json   <-- celui que nous venons de créer
 |- packages
    |- core
        |- package.json   <-- celui qui existait déjà (avec 1 dépendance : vitest)
        |- domain
        |- infra
    |- webapp
        |- package.json   <-- nouveau, l'application React
        ...

Lancez ensuite yarn dev depuis packages/webapp : l'application React démarre et le navigateur s'ouvre sur la page par défaut.

Ensuite, nous indiquons à notre package webapp qu'il va utiliser core.

Et surtout pas l'inverse ! C'est tout le principe d'une clean architecture : le coeur de l'application (le "domaine") est au centre. Les couches qui se trouvent autour, comme l'UI, utilisent le domaine. Mais le domaine ne dépend jamais de quelque chose situé dans une couche supérieure.

Dans les dépendances du package de la webapp, ajoutons "@booking/core": "*". (Avec Yarn 2+ ou pnpm, on écrirait plutôt "workspace:*", qui interdit explicitement d'aller chercher un homonyme sur npm.)

Le fichier package.json devrait contenir à peu près cela :

{
  "name": "@booking/webapp",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "@booking/core" : "*"
  },
  "devDependencies": {
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.1",
    "eslint": "^8.57.0",
    "eslint-plugin-react": "^7.34.2",
    "eslint-plugin-react-hooks": "^4.6.2",
    "eslint-plugin-react-refresh": "^0.4.7",
    "vite": "^5.3.1"
  }
}

Ouf, ça fait pas mal de changements dans l'organisation de notre dépôt de code. Pour vous aider, le code complet de ce passage en monorepo est disponible ici (opens in a new tab).

Nous voilà prêts à faire le premier écran de notre application !