Contacter DARVA

À la découverte des monorepos pour partager son code JS

02 mars 2023
Code
Tech

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, et npm 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 :

  1. Identifie les packages ayant subi une update depuis la dernière release
  2. Affiche les nouvelles versions
  3. Modifie les fichiers de configuration des packages mis à jour
  4. Commit les changements dû à la montée de version et crée des tags de chaque version
  5. 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 :

  1. npm install sur chaque package du projet
  2. Créé les liens symboliques entre chaque package du projet si besoin
  3. npm run prepublish dans tous les packages bootstrapped (sauf si --ignore-prepublish est passé).
  4. 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

La documentation du lerna run

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 lslerna list
  • lerna lllerna ls -l, montre une liste plus fournie
  • lerna la = lerna ls -la, montre tous les packages, même les privates

La documentation du lerna list

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érieur
            strict-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

Nicolas REMISEÀ 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.