Sous ce nom de jeu de société, la technique du monorepo est une technique d’organisation des projets extrêmement simple : on met tout dans le même dépôt. Car monorepo est, comme vous l’aurez deviné, le diminutif de mono-repository, que l’on peut traduire par mono-dépôt. Si ça peut paraître assez primitif comme technique (en nous rappelant étrangement ces fichiers de code de 6000 lignes contenant à boire et à manger), le monorepo est pourtant une technique réduisant certains désavantages non négligeables du travail avec plusieurs dépôts. Par la suite, nous verrons comment mettre en place un monorepo sur une stack en Javascript. Enfin, je vous ferai part d’un petit retour d’expérience sur la pratique.

Un dépôt pour les gouverner tous

Un seul corps, 42 têtes. L'hydre est un monorepo.
Un seul corps, 42 têtes. L'hydre est un monorepo.

Un monorepo consiste, à la base, à déterminer une hiérarchie de fichiers permettant de contenir plusieurs projets, de préférence liés par un but commun, comme par exemple : les services d’une application, les projets d’une solution logicielle, le coeur et les plug-ins d’une bibliothèque. Dans la suite de l’article, j’appellerai “entité” l’entité qui doit être organisée par un monorepo, donc une application, une solution logicielle, une bibliothèque ou autre.

Soyons clair sur une chose : le monorepo est efficace pour des entités de tailles moyennes à gigantesques. Vous pouvez l’utiliser sur des petites entités, mais vous n’obtiendrez pas l’efficacité escomptée de la technique. Les problèmes gérés par cette technique n’arrivent qu’à partir d’une certaine échelle.

Avantages

Un exemple de hiérarchie de fichiers que nous utilisons sur l’un de nos projets est la suivante :

root
|- packages
  |- lib-1
  |- lib-2
  |- lib-3
|- services
  |- microservice-1
  |- microservice-2
  |- microservice-3

Dans notre cas, nous avons décidé de séparer les bibliothèques (packages) contenant toutes les fonctions métiers des différents micro-services (services). De fait, vous pouvez avoir des services qui utilisent certaines bibliothèques communes, et d’autres plus spécifiques.

Outre la non-duplication de code, les outils de gestion des monorepos ont aussi la capacité de gérer les dépendances inter-bibliothèques et inter-services. Dans le cas de lerna pour les monorepos JS/Node.js, l’outil va gérer les liens symboliques automatiquement entre les différentes sources de code. Même mieux, il gère automatiquement l’installation de toutes les dépendances par sous-projet et évite la duplication de dépendance au maximum.

De fait, l’installation d’un dépôt d’une entité est beaucoup plus simple, car un outil s’occupe, pour vous, d’effectuer les actions sur l’intégralité des sous-projets de l’entité.

La gestion de projet est aussi simplifiée, car là où vous deviez gérer une fusion de branches par dépôt, vous n’avez maintenant plus qu’une seule fusion par entité. Vous pouvez donc facilement tracer des modifications pour de nouvelles fonctionnalités sur l’ensemble de votre code sans devoir chercher à travers les 5, 10, 20 et plus dépôts de votre entité.

Enfin, vous pouvez organiser vos configurations de build ou de dev pour l’ensemble du projet. Dans notre cas, nous avons un script fourni avec le projet qui lance l’intégralité des micro-services nécessaires pour pouvoir développer sur le projet. On réduit, de fait, la complexité à l’arrivée sur le projet pour de nouveaux·elles développeur·euses.

Problèmes

Tout n’est pas magique, et l’installation d’un monorepo vient avec son lot de problèmes. Dans les problèmes majeurs, si vous gérez un projet avec plusieurs équipes qui ont des droits d’accès différents, vous ne pourrez pas mettre en place un monorepo dû à la structure de Git et du projet.

De plus, votre dépôt Git devenant beaucoup plus dense en terme de code et de commits, il est possible que votre dépôt devienne plus lent pour des consultations d’historique ou autres.

Certain·es développeur·euses ont aussi levé le problème de build plus longs. Cependant, avec l’installation de dépendances communes automatiquement et les outils n’effectuant les builds que pour les sous-projets spécifiés, le temps de build semble plutôt être en faveur des monorepos.

Mise en place d’un monorepo pour une stack Javascript

Attachez votre ceinture, on est parti !
Attachez votre ceinture, on est parti !

Après la théorie, la mise en pratique ! Le monorepo étant maintenant plus populaire, les outils de gestion des monorepos se sont aussi multipliés. De fait, je vous invite à consulter cette liste pour voir les outils maintenant disponibles. Personnellement, j’ai eu l’occasion de tester fin 2018 Rush de Microsoft (mais l’outil manquait de maturité et de praticité, chose qui semble maintenant résolu) et lerna que nous utilisons encore aujourd’hui. De fait, j’utiliserai lerna pour cette article, mais n’hésitez pas à tester de nouveaux outils (NX et Bit ont l’air vraiment impressionants).

Choisir sa hiérarchie de fichier

Comme nous l’avons vu tout à l’heure, il est possible de gérer de différentes façons sa hiérarchie de fichiers en fonction de votre méthodologie et de votre projet. Celle que nous avons vu tout à l’heure est adaptée à des projets ayant de multiples endpoints indépendants, tandis que dans le cas d’une bibliothèque, vous risquez de travailler de la même façon que babel, c’est-à-dire :

root
|- packages
  |- lib-core
  |- lib-1
  |- lib-2
  |- lib-3

Mettre en place chaque sous-projet

Il faut bien comprendre que, d’un point de vue micro, chaque sous-entité semble être totalement indépendante. Nous pouvons aller dans le dossier de n’importe laquelle et éxecuter le code sans souci.

Cette spécificité est due au fait qu’effectivement, les sous-entités sont des projets à part entière. De fait, elles ont toutes besoin de la structure classique d’un projet, c’est-à-dire un package.json, un dossier test, un dossier src, etc.

Ce qui nous donne :

root
|- packages
  |- lib-1
    |- src
      |- index.js
    |- test
      |- index.js
    |- package.json
  |- lib-2
    |- src
      |- index.js
    |- test
      |- index.js
    |- package.json
|- services
  |- microservice-1
    |- src
      |- index.js
    |- test
      |- index.js
    |- package.json
  |- microservice-2
    |- src
      |- index.js
    |- test
      |- index.js
    |- package.json
|- package.json

Remarquez le package.json à la racine de notre projet qui va contenir les dépendances essentielles du projet, donc notre outil de gestion de monorepo (ici, lerna).

Ajouter lerna

À partir de la racine du projet, vous pouvez faire un petit npm i -s lerna afin d’ajouter lerna à votre projet. Une fois que c’est fait, rajouter un fichier lerna.json à la racine du projet avec la forme :

{
  "npmClient": "npm",
  "packages": [
    "packages/lib-1",
    "packages/lib-2",
    "services/microservice-1",
    "services/microservice-2"
  ]
}

Une fois ce fichier sauvegardé, vous pouvez démarrer la machine avec npx lerna bootstrap. Si tout est bien configuré, lerna va installer toutes les dépendances de vos sous-entités automatiquement.

Si, maintenant, vous souhaitez que microservice-1 utilise lib-1, il vous suffit de modifier services/microservice-1/package.json en ajoutant, dans les dépendances (en considérant que, dans le fichier services/lib-1/package.json, le champ name soit lib-1) :

"lib-1": "^0.0.1"

Pour mettre à jour les dépendances, il suffit de faire un npx lerna bootstrap. Et hop, si vous allez dans services/microservice-1/node_modules, vous allez retrouver un dossier lib-1 qui est un lien symbolique vers packages/lib-1.

Vous pouvez ensuite ajouter vos différents scripts de démarrage dans root/package.json afin de pouvoir démarrer différentes configurations du projet.

Est-ce que c’est vraiment efficace ?

En interne, notre seul projet en monorepo est un projet de plus de 250000 lignes de code, avec 9 packages et 7 services. Nos services contiennent tous les endpoints de notre solution, comprenant aussi bien des micro-services en Typescript/Node.js que des applications mobiles en Typescript/React Native ou des clients desktop avec Electron.

Le changement vers le monorepo a été fait à l’origine pour gérer uniquement les micro-services de notre solution vers fin 2018. Cependant, avec le temps, nous avons remarqué qu’il était possible de gérer absolument tous les endpoints via cette méthode.

Dans les avantages les plus importants de notre côté, il y a le partage de type à la volée entre toutes les sous-entités. Notre application mobile travaille avec les mêmes types que la base de données, que l’API et que la web-app, ce qui simplifie grandement la maintenance du code. De plus, il est possible de partager des bibliothèques d’UI entre l’application mobile et l’application web, ou encore partager les logiques métier entre tous les micro-services sans jamais faire de duplication de code. Enfin, le fait de n’avoir qu’une revue de code à faire est beaucoup plus simple du côté de la gestion de projet.

Clairement, le changement au monorepo relativement tôt sur le projet a été une excellente idée. Ajouter une nouvelle sous-entité au projet se fait extrêmement rapidement. Il faut cependant faire attention avec des développeur·euses junior, car la surcouche amenée par un outil de gestion amène une sorte “d’obfuscation” des process de build, donc il est nécessaire de prendre le temps de leur expliquer le fonctionnement derrière ces outils magiques.


Merci de nous avoir lu ! N’hésitez pas à nous faire des retours sur vos prochains monorepos ! Vous pouvez nous contacter sur le Twitter ou sur le LinkedIn de l’entreprise. Bonne rentrée ! :)

Merci de votre lecture ! <3
Merci de votre lecture ! <3

Mis à jour :