/ Articles
Les avantages d'un monorepo 📦
Le monorepo a le vent en poupe depuis plusieurs années. Certaines grandes boîtes comme Google, Meta ou Microsoft l'ont même adopté de longue date pour centraliser leurs différents projets en un seul dépôt Git. L'apparition d'outils dédiés à leur gestion facilite aujourd'hui grandement leur utilisation et permettent de tirer un maximum profit de cette organisation tout en contrebalançant certains de leurs défauts.
Un monorepo, qu'est-ce que c'est ?
À mesure qu'un projet se développe et se complexifie, on se retrouve à devoir travailler sur une multitude de composantes couplées entre elles : un serveur backend, de multiples interfaces web ou mobiles, des scripts de déploiement, des outils de gestion de contenu, des API REST ou GraphQL, des bases de données, des tests, des librairies, des configurations en pagaille, des pipelines CI/CD, etc.
En développant chacune de ces composantes dans un dépôt Git séparé (ce qu'on pourrait qualifier d'approche polyrepo), on finit par devoir installer et configurer plusieurs dépôts si on veut embrasser la totalité du projet. La situation se corse d'autant plus lorsque certaines composantes se trouvent partagées par d'autres. Par exemple, une même logique d'authentification gérée par un backend mais appliquée à plusieurs sites ou applications front d'un même projet gagne généralement à être extraite sous forme de librairie interne séparée (selon le fameux principe DRY, "Don't Repeat Yourself") qui pourra dès lors être consommée par plusieurs clients. Reste à pouvoir rendre cette librairie accessible, ce qui oblige à la publier sur un registre de packages en ligne comme npm puis à l'installer individuellement pour chaque client à l'aide de npm publish nom-de-la-librairie et npm install nom-de-la-librairie. Par suite, il faudra régulièrement veiller à ce que toutes les composantes consommatrices de cette librairie soient mises à jour à chaque fois que nous en publierons une nouvelle version afin de conserver une certaine consistance entre toutes nos applications.
Bien d'autres tâches rébarbatives tendent également à se répéter d'un dépôt à l'autre que l'on peut citer en vrac : l'accès aux variables d'environnement, les innombrables fichiers de configuration pour le linter, le formatter, la CI/CD, les scripts de déploiement ou de maintenance, etc. Bref la corvée de la configuration et de la maintenance s'alourdit rapidement lorsqu'un projet prend de l'envergure et se diversifie puisqu'il faut harmoniser les paramètres communs entre les différents dépôts tout en conservant parfois des spécificités locales qui doivent venir surcharger d'autres.
Et s'il existait un moyen de se délester en grande partie de ce travail fastidieux et de s'assurer que différentes composantes interconnectées de notre projet restent parfaitement synchronisées en temps réel sans avoir à recharger une nouvelle version de telle ou telle librairie ? Pour que nos préférences et nos configurations s'appliquent universellement sans devoir être répétées entre chaque dépôt apparenté ? Cette solution a un nom : le monorepo.
La recette d'un bon monorepo 🧑🍳
Tout dépôt contenant plusieurs composantes (librairies ou applications) peut être considéré comme un monorepo. La différence cruciale va plutôt se jouer sur la manière de gérer ces composantes et leurs interactions entre eux. On peut en effet se contenter d'entasser ces composantes dans un seul dépôt et de profiter de leur voisinage pour importer directement en interne les parties qui intéressent chacun d'entre eux. Mais ce serait passer à côté des gains de productivité et de gestion simplifiée que peut apporter un monorepo correctement orchestré.
Prenons un exemple courant : un monorepo dont plusieurs applications frontend se partagent une même librairie UI interne. Par cette simple action, un graph de dépendances se dessine entre ces différents projets :
Imaginons maintenant que vous commit ou push la dernière version de votre dépôt ce qui enclenchera probablement un pipeline CI/CD avec des tâches de build et de test. Il est évident que le build de l'app #1 ou de l'app #2 ne pourra être exécuté qu'après celui de la librairie interne dont chacune d'elle dépend, de sorte que toute modification de la librairie UI devra entraîner un re-build de toutes les applications qui l'utilisent.
Cependant, une modification de l'app #1 ou de l'app #2 n'aura aucun impact sur la librairie interne et ne nécessitera pas de recompiler celle-ci. De même, votre linter n'a aucun intérêt à s'exécuter sur l'ensemble du dépôt si les dernières modifications concernent uniquement une seule application. C'est là tout l'intérêt d'outils de gestion de monorepo comme Nx ou Turborepo qui optimisent l'exécution de ces différentes tâches à l'intérieur de votre monorepo en se servant du graph de dépendances qui émerge spontanément des relations nouées dans le code entre vos différentes composantes. Vous pouvez même configurer manuellement jusqu'à atteindre la précision parfaite pour votre projet.
Si vous laissez donc grossir votre monorepo sans outils capable de mettre en cache et de paralléliser les tâches récurrentes, vous risquez de voir celui-ci devenir un amas de code mal organisé et de rapidement constater une dégradation des performances et une augmentation des temps de build. Cela exige un travail de configuration supplémentaire mais qui se révèlera très vite payant. En écartant ce risque inhérent aux monorepo non orchestré, vous pouvez à présent savourer pleinement ses avantages 😎
L'alignement des dépendances (Single Version Policy)
L'un des aspects les plus importants dans un projet bien trop souvent négligé est celui de la gestion des dépendances externes et de leurs montées de version. Beaucoup de projets manquent de cette discipline dans les mises à jour et se retrouvent progressivement bloqués par des versions obsolètes de dépendances externes qui ouvrent des failles de sécurité et interdisent de profiter des dernières améliorations.
La raison de cette négligence si répandue tient moins à l'ignorance de la gravité des risques qu'encourt une application aux dépendances externes insuffisamment actualisées et dont certaines fonctionnalités cessent d'être supportées en raison de la dépréciation de celles-ci, qu'à la paresse naturelle qui saisit tout et chacun devant le travail de mise à niveau qui exige parfois de se replonger dans le code pour rattraper certains breaking changes introduits par des changements de versions majeures. Le monorepo peut largement atténuer ce problème de motivation en permettant de traiter en un seul coup l'ensemble des dépendances externes de toutes les composantes d'un projet ou en forçant l'adoption de versions identiques pour les librairies les plus importantes. On peut par exemple s'assurer que toutes nos librairies utilitaires utilisent la même version de Typescript ou que nos applications front s'en tiennent à la même version de React.
Un commit pour tous vos changements (Atomic Changes)
Dans le cas d'un monorepo contenant une API consommée par une seule application frontend, l'approche monorepo vous permet de modifier chaque extrémité de cette chaîne de communication et de tester simultanément cette combinaison avant de tout sauvegarder en un seul commit. Au contraire, si votre projet est divisé en plusieurs dépôts, notamment pour la partie front et back, il vous faudra attendre de devoir récupérer une modification faite sur un côté avant de pouvoir entamer l'autre ce qui vous impose des allers-retours entre les différents dépôts. L'évolution recherchée ne pourra apparaître qu'au terme d'une série de commits répartis sur différents dépôts. Au contraire,un monorepo vous donne la capacité de stocker l'intégralité des changements liés à une fonctionnalité dans un seul commit ce qui favorise la lisibilité et la maintenance de votre code, surtout en cas de git revert ou pour faciliter la relecture d'une pull request.
Des configurations harmonisées
L'outillage des projets frontend moderne est généralement toujours le même : un linter comme ESLint ou Biome, un formatter comme Prettier, un type checker comme Typescript, des hooks de pré-commit ou pré-pull comme Husky, une configuration Postcss ou Tailwind etc. Chacun de ces outils dispose de son propre fichier de configuration pour activer ou désactiver certains réglages placé à la racine du dépôt dans le cas d'un dépôt simple. Mais si on dispose de plusieurs applications React, par exemple, ces configurations tendent à être les mêmes d'un dépôt à un autre. En fonctionnant avec des dépôts séparés, on risque de voir progressivement apparaître et se creuser des écarts alors qu'une même équipe travaille sur cette famille d'applications.
Quand tous ces projets sont rassemblés dans un monorepo, il devient plus aisé de placer une configuration de base commune à tous les projets ou à certains sous-ensembles (toutes les applications de type React par exemple) afin de maintenir une plus forte cohérence. Cela n'interdit pas des adaptations spécifiques : la plupart de ces outils autorise des fichiers de configuration complémentaires pour chaque composante qui viennent étendre la configuration de base partagée en ajoutant ou en surchargeant certains réglages.