Monorepo avec JavaScript : organisation du code en pratique
Dans le développement web moderne en JavaScript (Node.js), la question se pose de plus en plus : comment organiser efficacement le code lorsque vous avez plusieurs packages, bibliothèques ou microservices qui dépendent les uns des autres ? L'approche traditionnelle avec des dépôts séparés (multirepo) peut entraîner une duplication du code, des difficultés de synchronisation des versions et un CI/CD long. La solution — le monorepo. Dans cet article, nous allons voir ce qu'est un monorepo, pourquoi il est utile et comment l'organiser en pratique avec JavaScript.
Qu'est-ce qu'un monorepo et pourquoi en a-t-on besoin ?
Un monorepo est une approche dans laquelle le code de plusieurs projets (packages, bibliothèques, services) est stocké dans un seul dépôt. Ce n'est pas simplement « tout mettre dans un seul dossier », mais une architecture réfléchie qui résout de nombreux problèmes :
- Version unique des dépendances : vous évitez les situations où différents packages utilisent des versions différentes d'une même bibliothèque.
- Refactorisation facile : les modifications dans une bibliothèque commune sont immédiatement visibles dans tous les projets qui l'utilisent.
- CI/CD simplifié : un seul dépôt — une seule configuration de build et de test.
- Partage de code : les utilitaires, les types et les configurations sont accessibles à tous les packages sans publication sur npm.
Cependant, le monorepo ne convient pas à tout le monde. Il nécessite de la discipline dans l'équipe et de bons outils. Pour JavaScript (Node.js), les principaux outils sont npm workspaces (intégré dans npm 7+) et Lerna (un outil plus avancé).
Configuration d'un monorepo de base avec npm workspaces
Commençons par un exemple simple. Supposons que nous ayons un projet composé de deux packages : @myapp/core (le noyau avec la logique métier) et @myapp/server (un serveur Express).
Créons la structure de dossiers :
my-monorepo/├── packages/│ ├── core/│ │ ├── src/│ │ │ └── index.js│ │ ├── package.json│ │ └── ...│ └── server/│ ├── src/│ │ └── index.js│ ├── package.json│ └── ...├── package.json└── ...Le package.json racine doit contenir le champ workspaces :
{ "name": "my-monorepo", "private": true, "workspaces": [ "packages/*" ], "scripts": { "start": "node packages/server/src/index.js", "test": "npm run test --workspaces" }}Maintenant, dans packages/core/package.json :
{ "name": "@myapp/core", "version": "1.0.0", "main": "src/index.js"}Et dans packages/server/package.json, nous ajoutons une dépendance vers core :
{ "name": "@myapp/server", "version": "1.0.0", "dependencies": { "@myapp/core": "*", "express": "^4.18.0" }}Après avoir exécuté npm install à la racine, npm crée des liens symboliques entre les packages. Maintenant, dans le code du serveur, nous pouvons importer core directement :
// packages/server/src/index.jsconst express = require('express');const { calculate } = require('@myapp/core');
const app = express();app.get('/', (req, res) => { res.send(calculate(2, 3));});app.listen(3000);Ceci est un exemple de base, mais il montre déjà le principal avantage : les modifications dans @myapp/core sont immédiatement visibles dans @myapp/server sans publication dans le registre.
Organisation avancée avec Lerna
Lorsque le nombre de packages dépasse 5 à 10, npm workspaces peut ne pas suffire. C'est là que Lerna intervient — un outil qui ajoute des fonctionnalités puissantes :
- Exécution de commandes dans tous les packages ou dans des packages sélectionnés.
- Gestion automatique des versions (mode fixed ou independent).
- P