À la découverte des monorepos pour partager son code JS
Introduction
Partager efficacement le code d’une grande application est une optimisation souvent envisagée et quelques fois développée.
Au sein de l’Usine Digitale Données, par exemple, nous avons eu besoin d’une solution facile et évolutive de partage de petits utilitaires et autres outils pour nos projets. Après quelques recherches, nous avons décidé d’introduire un monorepo JS multi-packages.
Mécanique actuelle
Depuis que npm est apparu, la vie est devenue plus facile en termes de réutilisation.
Nous avons toujours besoin de certains modules que nous voulons réutiliser dans d’autres projets.
Pour ce faire, on doit publier un module npm dans un registry npm like pour que nos projets le consomment.
S’il s’agit de modules limités, c’est la meilleure approche, mais plus le nombre de modules augmente, plus le nombre de repo git associés au module npm augmente et ainsi, plus on risque d’avoir des difficultés à les maintenir et à les gérer.
C’est ici qu’intervient l’approche monorepo.
C’est le moyen le plus simple de gérer nos petits morceaux de code distribuables sans les frais qu’imposent les nombreux repo git et leurs configurations.
Évidemment, cela ne veut pas dire qu’un monorepo n’a pas ses propres inconvénients, mais ils peuvent être réduits grâce à divers outils et approches.
Inconvénient des modules uniques
La publication de quelques fichiers de notre projet dans un registry npm like (Nexus, npm, etc…), nous aurait obligé à diviser notre repo et à en créer de nouveaux juste pour partager ce code.
Lorsqu’il s’agit de centaines de composants, cela signifie qu’il faut maintenir et apporter des modifications dans des centaines de repo.
Nous devons également remanier notre codebase, en supprimant les packages nouvellement créés de leurs repo d’origine, en mettant en place les packages dans les nouveaux repo, etc.
Bref, pas mal de travail et de complexité pour un bénéfice pas très conséquent.
Monorepo
Un monorepo signifie que vous vous retrouverez avec plusieurs projets sous forme de packages dans le même dépôt git.
De nombreux frameworks et librairies open source comme babel, lodash, etc., utilisent la stratégie du dépôt unique pour leurs projets.
Ce que l’on appelle « monorepo » vous aide à synchroniser vos changements entre les projets et les bibliothèques afin d’augmenter votre vitesse et votre évolution continue.
Les révisions de code sont plus rapides car il suffit d’une seule demande de modification pour modifier plusieurs projets.
Une autre fonctionnalité intéressante est le partage des mêmes fichiers de configuration pour tous vos projets (les fichiers dot, la configuration de jest, Eslint ou TypeScript, etc…) qui peuvent être définis au niveau de la racine de votre dépôt.
La construction de paquets npm à travers de nombreux repo uniques rend les changements importants difficiles à faire, à tester et à publier.
En utilisant un monorepo, nous pouvons résoudre beaucoup de ces problèmes et bien d’autres encore 😉
Réflexions
Notre framework à partager tire parti de npm afin d’avoir des packages versionnés indépendamment pour chaque composant ou utilitaire afin qu’ils puissent être partagés entre les applications.
Cependant, d’après l’approche sans réflexion préalable, nous construirions ces paquets dans des repo git séparés.
Cela entraîne une certaine surcharge :
- La configuration de tous les packages localement est un long processus qui prend une grande quantité d’espace parce que vous installez les mêmes outils de développement 50 fois
- Il est difficile de travailler sur des changements qui touchent un tas de packages différents, les lier avec npm est un casse-tête au mieux, et impossible au pire
- Exécuter des tests sur tous les packages signifie exécuter chacun d’entre eux individuellement, ce qui pourrait tout aussi bien être impossible
- Nous n’avons pas une idée précise de la couverture du code dans tous les packages
Au lieu d’avoir une flopée de dépôts séparés, nous pourrions suivre l’approche que Babel (et d’autres) ont fait et utiliser un monorepo utilisant Lerna.
Lerna
Le dépôt serait toujours un dépôt git normal, mais avec un répertoire packages/
qui contient tous nos composants, utilitaires, etc. qui sont construits dans des paquets npm séparés.
Il y a un tas d’avantages à cela :
- La mise en place d’un environnement de développement pour chaque paquet n’exige plus que 3 commandes :
git clone
,npm install
, etnpm run bootstrap
- Les dépendances croisées dans le dépôt sont automatiquement liées entre elles par npm afin de faciliter le développement
- Vous pouvez maintenant faire des changements de dépendances croisées dans une seule demande de commit/pull
- Vous pouvez exécuter des tests sur tous les paquets en même temps (notez que vous pouvez toujours tester un seul paquet avec
npm test -- --testPathPattern cf-component-button
) - Nous n’avons à installer qu’une seule fois les ~300 modules qui composent le système de construction
- Nous n’avons qu’un seul système de construction pour tous les modules
- Nous pouvons avoir une idée de haut niveau de la couverture du code pour tous les paquets
- Nos packages existent de manière distincte sur le repository NPM like choisi
- Ils peuvent évoluer à rythme complètement détaché bien qu’ils soient sous le même repo git
Mise en oeuvre
Pour démarrer un nouveau repo avec lerna, il suffit de créer un repo git qui sera dédié à contenir tous les packages que l’on souhaite publier et partager.
Une fois ceci fait:
git clone [notre repo] cd [notre repo] npx lerna init //pour exécuter la commande init de lerna sur notre repo
Cela va nous créer deux fichiers à la racine de notre repo:
- un fichier
lerna.json
contenant toute la configuration associée à Lerna - un dossier
packages/
destiné à contenir tous nos futurs packages
Option intéressante
Si l’on souhaite avoir des versions de nos packages indépendante les unes des autres
npx lerna init --independent
Ainsi à chaque fois que l’on fera un publish
de nos packages on sera invité à dire quelles sont les évolutions de version des différents packages.
Commandes utiles
lerna publish
Publie les packages du projet
lerna publish # publish packages that have changed since the last release lerna publish from-git # explicitly publish packages tagged in the current commit lerna publish from-package # explicitly publish packages where the latest version is not present in the registry
Cette commande :
- Publie les packages mis à jour depuis la dernière version (en appelant
lerna version
) - Publie les packages marqués dans le commit actuel (
from-git
) - Publie les packages du dernier commit lorsque leur version n’est pas présente dans le registry (
from-package
)
⚠ Lerna ne publiera jamais les paquets qui sont marqués comme privés ("private" : true
dans le package.json)
La documentation du lerna publish
lerna version
Détermine les nouvelles versions des packages du projet depuis la dernière release
lerna version 1.0.1 # explicit lerna version patch # semver keyword lerna version # select from prompt(s)
Cette commande :
- Identifie les packages ayant subi une update depuis la dernière release
- Affiche les nouvelles versions
- Modifie les fichiers de configuration des packages mis à jour
- Commit les changements dû à la montée de version et crée des tags de chaque version
- Pousse le changement de version à la branche distante de notre repo git
La documentation du lerna version
lerna boostrap
Lie les packages du projet ensemble et installe les dépendances restantes de chaque package
lerna bootstrap
Cette commande :
npm install
sur chaque package du projet- Créé les liens symboliques entre chaque package du projet si besoin
npm run prepublish
dans tous les packages bootstrapped (sauf si--ignore-prepublish
est passé).npm run prepare
dans tous les packages bootstrapped
lerna bootstrap
supporte tous les filter flags
La documentation du lerna bootstrap
lerna changed
Liste les packages qui ont changé depuis la dernière release
lerna changed
La sortie de lerna changed
est une liste de packages qui seront les sujets de la prochaine lerna version
ou lerna publish
La documentation du lerna changed
lerna run [script]
Run un script npm chaque package qui contient ce script
lerna run [script] -- [..args] # runs npm run [script] dans tous les packages qui l'ont lerna run test lerna run build # surveille tous les packages et les transpile à chaque changement, stream la sortie de chaque run avec le prefix de nom de package lerna run --parallel watch
Un double tiret (--
) est nécessaire pour passer des arguments au script
lerna run
supporte tous les filter flags
lerna list
Liste les packages du repo git
lerna list
La commandelist
est un alias de plusieurs autres commandes (un peu comme npm ls
):
lerna ls
=lerna list
lerna ll
=lerna ls -l
, montre une liste plus fournielerna la
=lerna ls -la
, montre tous les packages, même lesprivates
Mise en place chez DARVA
On vient de voir la mise en place de lerna
d’un point de vue relativement simpliste et le pourquoi un monorepo peut-être une super solution pour partager son code efficacement entre les différents projets qu’on peut avoir.
Mais comment l’utiliser au sein de DARVA ?
Chez DARVA, nous avons à disposition un repository privé qui a été mis en place par le passé mais qui est encore peu utilisé.
Pour héberger nos packages dessus, il nous a juste fallu configurer le repo correctement via la configuration lerna
et nos process Jenkins
.
Une bonne pratique est de préfixer ses packages par un prefix d’organisation (scope), ex: @darva-datalab/connect, @darva-datalab/request-builder, etc…
Configuration Jenkins
Tout d’abord, il faut paramétrer le job Jenkins pour qu’il se connecte sur le repository (les credentials étant déjà stockés sur le serveur Jenkins) en saisissant la commande npm-ci-login
(qui est un package externe permettant de se logger dans un contexte CI)
NPM_REGISTRY=[url du registry] NPM_SCOPE=[scope d'organisation] NPM_USERNAME=${LOGIN} NPM_PASSWORD=${PASSWORD} npm-ci-login
Une fois cette commande exécutée, le terminal sera authentifié au registry et nous pourrons déclencher un lerna publish
avec les options désirées.
Configuration Lerna
Une fois la partie CI/CD paramétrée, il faut préciser à Lerna vers quel registry publier nos packages (par défaut ce sera npm).
Pour ce faire, il suffit d’ajouter dans le lerna.json
:
{ ... command: { ... publish: { registry: [url du registry] } } }
Enfin, il faut aller dans les packages et ajouter le scope
dans chacun des package.json
{ "name": "[scope]/[nom du package]", ... }
Configurer la récupération de nos packages sur le nexus
Nous avons mis en place la partie
publish
des packages dans le nexus de DARVA, mais comment utiliser ces packages dans nos différents projets?
Rien de plus simple, dans chaque projet utilisant nos packages, il faut préciser à npm ou yarn le registry sur lequel aller chercher nos packages (et c’est là que rentre en compte l’avantage des scopes d’organisation).
Suivant si c’est npm ou yarn il faut faire un fichier:
- pour npm :
.npmrc
avec à l’intérieurstrict-ssl=false //contrainte du registry Darva @[scope ou package precis]:registry=[url du registry]
- pour yarn :
.yarnrc
avec à l’intérieur"strict-ssl" false "@[scope ou package precis]:registry" "[url du registry]"
Un yarn
ou npm install
permettra de récupérer via le registry nos packages correspondant.
⚠ Pour le registry Darva il faudra être sur le réseau Darva (VPN ou présence sur site)
D'autres solutions
Pour utiliser une notion de monorepo dans vos projets, il y a d’autres solutions envisageables que je ne présenterai pas ici car elles me semblaient moins pertinentes.
On peut trouver notamment les git submodules, le yarn workspace ou encore bit.dev
À propos de l'auteur. Nicolas REMISE est TechLead JS/TS au sein de l'Usine Digitale Données de DARVA. Passionné par les technos web, il aime partager les nouveautés qu'il met en œuvre au quotidien.