Lerna et Verdaccio

Lerna est un outil de gestion pour des projets de type monorepo.

Monorepo: monolithic repository, un projet qui contient plusieurs packages ou apps rassemblés pour faciliter les interactions et opérations (partage de code, déploiement, suites de tests, etc.)

Il vous permet d’optimiser la gestion de vos packages en vous offrant du tooling plus ou moins opiniâtre.

Dans les features les plus intéressantes on retrouve :

  • Le versioning
  • La publication des packages
  • La génération de changelogs basés sur le contenu des messages de commits

Hands-on

Si vous aimez mettre les mains dans le camboui, forkez ce repo :

https://github.com/rednetio/monorepo-lerna-example

Et suivez les commandes données dans le tutoriel.

Si vous ne souhaitez pas le faire vous-même et voir directement le résultat de la section, suivez les commandes hands-on.

Si vous voulez juste regarder :

$ git clone git@github.com:rednetio/monorepo-lerna-example.git
$ git checkout step4

Lerna

Configuration basique

Hands-on : git checkout step1

Prenons un exemple de monorepo:

monorepo/
  apps/
    backend
    frontend
  libs/
    auth
    utils
  .gitignore
  .prettierrc.yml
  package.json
  yarn.lock

Tout d’abord, on installe Lerna, il est conseillé de l’installer en global.

$ yarn global add lerna

Puis, à la racine du monorepo, on initialise le projet lerna :

$ lerna init --independent --exact

Le flag --indepedent nous permet d’incrémenter les versions de nos packages indépendamment les unes des autres. Il est également possible d’être en mode fixe, où tous les packages possèdent la même version, et un changement de version implique que tous les packages sont bumpés. C’est de cette façon qu’opère le repository Babel.

On se retrouve avec :

monorepo/
  apps/
    backend
    frontend
  libs/
    auth
    utils
  .gitignore
  .prettierrc.yml
  _lerna.json_
  package.json
  yarn.lock

lerna.json contient :

{
  "packages": ["packages/*"],
  "version": "independent"
}

Whoop-de-doo.

Par défaut, Lerna essaye de chercher les packages dans packages/, mais vous pouvez lui préciser n’importe quel glob ou liste de globs.

Pour notre monorepo, ça donne :

{
  "packages": ["apps/*", "libs/*"],
  "version": "independent"
}

Si votre monorepo est configuré avec des workspaces yarn/npm, alors vous pouvez simplement mettre le flag useWorkspaces: true :

{
  "useWorkspaces": true,
  "version": "independent",
  "npmClient": "yarn"
}

Ici nous allons utiliser les workspaces.

Notez également que l’on précise yarn comme client npm.

Si vous ne savez pas comment faire : https://yarnpkg.com/en/docs/workspaces

Une fois l’emplacement des packages configuré, il est temps de bootstrapper :

$ lerna bootstrap --independent --exact
info cli using local version of lerna
lerna notice cli v3.14.1
lerna info Updating package.json
lerna info Updating lerna.json
lerna info Creating packages directory
lerna success Initialized Lerna files

--independent pour que chaque package ait une version individuelle, --exact pour fixer la version de lerna.

Votre monorepo est bien lerna-tized. Vous pouvez maintenant utiliser les différentes features de Lerna.

Nous allons voir plus bas le cœur de métier de lerna, la commande publish. Mais d’abord, faisons une interlude sur Verdaccio.

Verdaccio

Verdaccio est un registry de packages privé, local, et sans configuration nécessaire.

C’est une alternative à npm lorsque vous souhaitez mettre à disposition vos packages pour d’autres utilisateurs mais sans nécessairement vouloir les open sourcer.

Hébergez-le sur un serveur et boum, vous avez votre propre registry privé distant.

Ici nous allons utiliser Verdaccio en local.

Tout d’abord, installez-le en global :

$ yarn add global verdaccio --exact

Puis lancez-le :

$ verdaccio

Connectez-vous sur l’url par défaut : http://localhost:4873.

Pour l’instant il n’y a pas grand chose.

Avant de pouvoir publier un package, il faut créer un utilisateur pour pouvoir s’authentifier.

Note: Il faudrait faire la même chose pour npm. C’est exactement la même façon de procéder pour publier un package sur npm. La différence ici est qu’on précise notre registry local Verdaccio.

$ npm adduser --registry  http://localhost:4873

Vous pouvez vous connecter sur l’interface de Verdaccio avec les identifiants que vous avez choisis.

Nous allons tenter de publier notre librairie utils.

Si vous passez par HTTPS, vous devez paramétrer le Certificate Authority. Voir https://docs.npmjs.com/misc/config#ca pour plus d’informations sur le sujet.

Depuis la racine, lancez :

$ npm publish ./libs/utils --registry http://localhost:4873

Si tout s’est bien passé, vous devriez avoir sur Verdaccio le package utils en version 1.0.0 déployé à l’instant.

Avant de passer à la suite, on va dépublier le package (c’était juste pour tester).

$ npm unpublish --force utils --registry http://localhost:4873

Et voilà, rien de bien compliqué. Ce n’est ni plus ni moins qu’un registry.

Back to Lerna

Configuration avec Lerna

Hands-on : git checkout step2

Verdaccio étant prêt, on peut maintenant tenter de publier nos packages en passant par Lerna.

Pour ça, un peu de configuration est nécessaire.

Tout d’abord, il faut connaître l’intérêt de la propriété private: true que l’on peut trouver dans le package.json.

Si vous travaillez avec des workspaces yarn, vous avez sûrement du l’apercevoir dans le package.json à la racine.

Cette propriété permet d’empêcher la publication du package lors d’un npm publish. Comme ça n’a pas de sens de publier la racine, il est automatiquement mis à true.

Si vous avez suivi, vous aurez compris qu’il faut mettre private: true sur les packages que l’on ne souhaite pas publier.

Concernant notre monorepo, nous n’avons pas besoin de publier nos apps. Puisque ce que nous voulons, c’est exposer le code qui peut être réutilisé. Ce n’est pas le cas pour les apps.

Une fois que c’est fait, nous allons configurer Lerna pour la commande publish.

On souhaite que nos libs soient publiées sur notre registry local. Pour cela, il suffit de paramétrer la propriété registry dans lerna.json.

  "command": {
    […],
    "publish": {
      "registry": "http://localhost:4873/",
    }
  }

Afin d’optimiser la taille de nos packages, on va aussi dire à notre registry quels fichiers on veut publier. On fait ça avec la propriété files.

Pour la librairie auth par exemple :

  "files": [
    "dist",
    "index.js"
  ],

On a besoin de nos fichiers minifiés, et notre index.js pour exposer les composants.

On va également donner quelques petites informations à Lerna :

  "command": {
    […],
    "publish": {
      "message": "chore(packages): release",
      "conventionalCommits": true,
      "npmClient": "npm",
      "registry": "http://localhost:4873/",
      "verifyAccess": false
    }
  }
  1. message : le message de commit utilisé à la publication
  2. conventionalCommits : on gère les versions suivant les conventional commits, voir prochaine section
  3. npmClient : le client que nous utilisons pour effectuer la publication
  4. registry : le registry sur lequel seront publiés les packages. On peut aussi le conifgurer individuellement dans les packages.
  5. verifyAccess : lors de la publication d’un package, npm tente de faire npm access ls-packages pour vérifier à quels packages la team ou l’utilisateur ont accès, cette commande n’est pas supportée par Verdaccio, alors on met verifyAccess à false pour ne pas l’exécuter

Versioning et Conventional commits

Hands-on : git checkout step3

Le versioning des packages suit la convention SemVer, et est fait de façon automatique en fonction des types de commits.

Ici, on utilise les conventional commits, popularisé par Angular.js.

En savoir plus sur les conventional commits

Le principe est :

  • Un commit de type fix: déclenchera une montée de version PATCH.
  • Un commit de type feat: déclenchera une montée de version MINOR.
  • Un commit qui contient un BREAKING CHANGE déclenchera une montée de version MAJOR.

Pour éviter les écueils, vous pouvez utiliser un commit linter.

$ yarn add -WED @commitlint/{config-conventional,cli} husky

Puis rajoutez dans votre package.json à la racine :

  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }

On rajoute un fichier .commitlintrc.yml à la racine :

extends:
  - '@commitlint/config-conventional'

Il faut maintenant modifier nos packages pour tester la montée de version et la publication.

Lerna publish

On arrive enfin à la partie intéressante, la publication des packages.

Il nous faut d’abord committer des modifications afin de provoquer une nouvelle version pour chaque package.

Vous pouvez tenter de faire des modifications vous-même ou alors passer directement à l’étape suivante avec la commande hands-on :

Hands-on: git checkout step4

Vous pouvez faire un git log pour voir les commits qui ont été faits.

On a un :

  • Un fix sur le frontend
  • Rien sur le backend
  • Un breaking change sur auth
  • Une feature sur utils

On devrait alors passer aux versions :

  • 1.0.1 pour frontend
  • 1.0.1 pour backend
  • 2.0.0 pour auth
  • 1.1.0 pour utils

Ici backend est bumpé parce que c’est notre premier publish.

On vérifie d’abord quels packages ont été updatés :

$ lerna changed
info cli using local version of lerna
lerna notice cli v3.14.1
lerna info versioning independent
lerna info Assuming all packages changed
auth
utils
lerna success found 2 packages ready to publish

Les deux packages auth et utils (ceux que l’on veut publier) ont bien été détectés.

On va bientôt pouvoir passer à la publication.

Tout d’abord, sachez que lerna publish fait beaucoup de choses.

Il va :

  • Exécuter la montée de version
  • Mettre à jour les versions des packages que l’on vient de mettre à jour au sein du monorepo
  • Générer le changelog de chaque package
  • Committer les changements avec le message configuré dans le lerna.json
  • Créer des tags de version pour chaque package (au format : `auth@1.0.0`)
  • Pusher toutes les références git
  • Publier les packages sur le registry

:warning: Si votre publish ne fonctionne pas, vous vous retrouverez avec tous les tags et commits de créés et poussés sur le serveur git.

Dans la pratique, vous risquez d’avoir des problèmes seulement à la mise en place de Lerna et Verdaccio. Une fois que tout est configuré vous n’aurez plus de problèmes.

Avant de pouvoir exécuter le publish, il faut pousser la branche sur laquelle on travaille.

Ici, on travaille sur la branche release.

Hands-on : pour créer la branche release, mergeons d’abord la branche step4 dans master.

# HANDS-ON ONLY
$ git checkout master
$ git merge step4

On créé et on pousse la branche release :

$ git checkout -b release
$ git push

Allez, on passe enfin au publish :

$ lerna publish
info cli using local version of lerna
lerna notice cli v3.14.1
lerna info versioning independent
lerna info Assuming all packages changed
lerna info getChangelogConfig Successfully resolved preset "conventional-changelog-angular"

Changes:
 - backend: 1.0.0 => 1.0.1 (private)
 - frontend: 1.0.0 => 1.0.1 (private)
 - auth: 1.0.0 => 2.0.0
 - utils: 1.0.0 => 1.1.0

? Are you sure you want to publish these packages? Yes
lerna info execute Skipping GitHub releases
lerna info git Pushing tags...
lerna info publish Publishing packages to npm...
lerna notice Skipping all user and access validation due to third-party registry
lerna notice Make sure you're authenticated properly ¯\_(ツ)_/¯
lerna WARN ENOLICENSE Packages auth and utils are missing a license.
lerna WARN ENOLICENSE One way to fix this is to add a LICENSE.md file to the root of this repository.
lerna WARN ENOLICENSE See https://choosealicense.com for additional guidance.
lerna success published utils 1.1.0
lerna notice
lerna notice 📦  utils@1.1.0
lerna notice === Tarball Contents ===
lerna notice 168B  package.json
lerna notice 354B  CHANGELOG.md
lerna notice 469B  config/webpack.config.js
lerna notice 1.1kB dist/utils.min.js
lerna notice 167B  src/utils.js
lerna notice === Tarball Details ===
lerna notice name:          utils
lerna notice version:       1.1.0
lerna notice filename:      utils-1.1.0.tgz
lerna notice package size:  1.3 kB
lerna notice unpacked size: 2.2 kB
lerna notice shasum:        e6bc61dff306830bef2ebaa67fd7ef0861e21738
lerna notice integrity:     sha512-NVPWgFpMN9RjY[...]9zvwE6rE95uHA==
lerna notice total files:   5
lerna notice
lerna http fetch PUT 201 http://localhost:4873/utils 95ms
lerna success published auth 2.0.0
lerna notice
lerna notice 📦  auth@2.0.0
lerna notice === Tarball Contents ===
lerna notice 285B  package.json
lerna notice 420B  CHANGELOG.md
lerna notice 106B  index.js
lerna notice 1.3kB dist/probe.min.js
lerna notice === Tarball Details ===
lerna notice name:          auth
lerna notice version:       2.0.0
lerna notice filename:      auth-2.0.0.tgz
lerna notice package size:  1.2 kB
lerna notice unpacked size: 2.1 kB
lerna notice shasum:        2f008d37cccfbc84909aafad41a32638be9e408f
lerna notice integrity:     sha512-5XA5UWM9gavxn[...]3e83VBU5bwuEw==
lerna notice total files:   4
lerna notice
lerna http fetch PUT 201 http://localhost:4873/auth 6238ms
Successfully published:
 - auth@2.0.0
 - utils@1.1.0
lerna success published 2 packages

On vérifie vite fait sur verdaccio : http://localhost:4873/.

Nos packages ont bien été déployés.

Vous pouvez aussi vérifier sur votre repository, vos tags et votre commit de release ont bien été poussés.

Congratuwelldone !