JS monorepos en prod 1 : initialization du projet

JS monorepos en prod 1 : initialization du projet

WORMS David

By WORMS David

5 janv. 2021

Catégories : DevOps & SRE, Front End | Tags : Git, JavaScript, GitOps, Monorepo, Node.js, Versions et évolutions [plus][moins]

Chaque projet commence par l’étape d’initialisation. Lorsque votre projet est composé de plusieurs sous-projets, il est tentant de créer un dépôt Git par sous-projet. Dans Node.js, un sous-projet se traduit par un package. Cependant, gérer trop de dépôts étroitement liés est déroutant et prend du temps.

Organiser vos projets dans un seul dépôt Git et utiliser un outil comme Lerna pour faciliter leur gestion pour leur gestion en vaut la peine. Cette architecture s’appelle un monorepo. Elle simplifie le versionnement et la publication de vos projets ainsi que leur manipulation et leur développement.

Chez Adaltas, nous développons et maintenons plusieurs monorepos depuis quelques années. Cet article est le premier d’une série de cinq dans laquelle nous partageons nos meilleures pratiques. Il couvre l’initialisation d’un projet à l’aide de Yarn et Lerna.

Création d’un nouveau projet

Le projet utilisé comme exemple vient de nos travaux antérieurs. Au fil des ans, nous avons accumulé plusieurs plugins Gatsby qui n’ont jamais été publiés et partagés avec la communauté open source. Ces plugins sont copiés/collés d’un site Gatsby à un autre, parfois avec des corrections de bogues et des améliorations. Étant donné que nous disposons de plusieurs exemplaires plus ou moins à jour entre eux, les sites Web plus anciens ne bénéficient pas de dernières modifications. L’idée est de centraliser le développement de ces plugins dans un référentiel unique et de les partager en les publiant sur NPM.

Un nouveau projet est créé à partir de zéro. Il s’appelle remark-gatsby-plugins et est hébergé sur GitHub. Ce référentiel est un conteneur pour plusieurs packages qui sont des plugins pour Gatsby et le plugin gatsby-transformer-remark.

# Repository initialization
mkdir remark-gatsby-plugins
cd remark-gatsby-plugins
git init
# Create and commit a new file
echo "# remark and Gatsby plugins by Adaltas" > README.md
git add README.md
git commit -m "docs: project creating"
# Define the GitHub remote server
git remote add origin https://github.com/adaltas/remark-gatsby-plugins.git
# Push commits to remote
git push -u origin master
# Next push commands will simply be `git push`

Le message du commit est préfixé par docs et ce n’est pas par hasard. Cet aspect est couvert plus tard dans le chapitre sur les Conventional Commits dans l’article suivant validation des commit et génération de changelogs.

Ignorer les fichiers de Git

Vous avez le choix entre deux stratégies :

  • Définir sélectivement des chemins à ignorer.
  • Définir des règles globales d’ignorance et exclure sélectivement des chemins de ces règles.

Je choisis généralement la dernière stratégie consistant à ignorer tous les fichiers cachés par défaut. Je commence par :

cat <<CONTENT > .gitignore
.*
node_modules
!.gitignore
CONTENT
git add .gitignore
git commit -m 'build: ignore hidden files and node modules'

Initialisation du projet

J’utilise personnellement Yarn au lieu de NPM. Les deux gestionnaires de packages fonctionnent parfaitement, mais j’ai eu des problèmes dans le passé en utilisant NPM avec des monorepos et des liens. Dans cette configuration, Yarn semble également être l’outil de choix dans la communauté. Son support natif pour les monorepos, appelés workspaces, fonctionne bien avec Lerna.

Pour initializer a package with yarn :

yarn init
yarn init v1.22.5
question name (remark-gatsby-plugins): 
question version (1.0.0): 0.0.0
question description: A selection of remark and Gatsby plugins developed and used by Adaltas
question entry point (index.js): 
question repository url (https://github.com/adaltas/remark-gatsby-plugins.git): 
question author (David Worms <david@adaltas.com>): 
question license (MIT): 
question private: 
git add package.json
git commit -m "build: package initialization"

Le résultat est le fichier package.json qui est ensuite commité.

Monorepo avec Lerna

Le projet contient un fichier package.json. Suivant la terminologie Node.js, le projet est désormais un package Node.js. Cependant, il ne sera pas publié sur NPM, le registre officiel pour Node.js. Seuls les packages contenus à l’intérieur de ce package seront publiés.

Au lieu de créer un référentiel Git pour chaque package, il est plus facile de gérer un référentiel unique stockant plusieurs packages Node.js. Puisque plusieurs packages sont gérés dans le même référentiel, nous appelons cela un monorepo.

Plusieurs outils existent pour gérer les monorepos. Lerna est un choix populaire mais pas le seul. Chez Adaltas, nous l’utilisons depuis un certain temps et nous continuons pour cet article.

En plus de n’avoir qu’un seul référentiel Git à gérer, il existe des avantages supplémentaires pour légitimer l’utilisation des monorepos :

  • Lorsque plusieurs packages sont développés, de nombreuses dépendances dupliquées sont déclarées dans le fichier package.json. Déclarer les dépendances à l’intérieur du projet parent géré avec Lerna réduit l’espace et les temps de téléchargement. Cela s’appelle le “hoisting” des dépendances.
  • Lorsque les packages dépendent les uns des autres, les changements dans un package doivent souvent être instantanément reflétés dans les autres packages. Une seule fonctionnalité peut couvrir plusieurs packages. La publication des modifications des packages dépendants n’est pas possible, cela prend trop de temps et il peut y avoir trop de modifications ne justifiant pas une version. La solution est de lier les dépendances en créant des liens symboliques. Pour les grands projets, c’est une tâche fastidieuse. Un outil comme Lerna automatise la création de ces liens.
  • Avoir un emplacement central fédère l’exécution de vos commandes. Par exemple, vous installez toutes les dépendances de tous vos packages avec une seule commande, yarn install. Pour les tests, la commande lerna test exécute tous vos tests.

De plus, Lerna nous aide à gérer nos versions par rapport à la spécification Semantic Versioning (SemVer).

La commande pour initialiser Lerna est :

yarn add lerna
yarn lerna init --independent

Le paramètre --independent indique à Lerna de gérer la version de chaque paquet indépendamment. Sans cela, Lerna aligne les versions des packages qu’il gère.

Ces commandes ajoutent la dépendance lerna au package.json et créent un nouveau fichier lerna.json :

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

Ensuite, nous commitons les changements en cours :

git add lerna.json package.json
git commit -m 'build: lerna initialization'

Publication ou ignorance des fichiers de lock

La commande yarn add a généré un fichier yarn.lock. Avec NPM, le fichier aurait été package-lock.json.

Mon approche consiste à publier des fichiers de lock dans les applications finales. Je ne publie pas les fichiers de lock des packages destinés à être utilisés comme dépendances. Certaines personnes sont d’accord avec mon opinion. Cependant, la documentation de Yarn indique le contraire :

Tous les fichiers yarn.lock doivent être archivés dans le gestionnaire de code source (par exemple, git ou mercurial). Cela permet à Yarn d’installer exactement la même arborescence de dépendances sur toutes les machines, qu’il s’agisse de l’ordinateur portable de votre collègue ou d’un serveur CI.

Les auteurs de framework et de bibliothèques devraient également vérifier yarn.lock dans le contrôle de code source. Ne vous inquiétez pas pour la publication du fichier yarn.lock car il n’aura aucun effet sur les utilisateurs de la bibliothèque.

Je suis perplexe. S’il n’est pas utilisé, alors pourquoi commettre un gros fichier. Quoi qu’il en soit, ignorons-les pour l’instant. Le résultat final est que ces fichiers de verrouillage seront ignorés de Git :

echo 'package-lock.json' >> .gitignore
echo 'yarn.lock' >> .gitignore
git add .gitignore
git commit -m "build: ignore lock files"

Intégration Yarn

Puisque nous utilisons Yarn au lieu de NPM, ajoutez ces propriétés à lerna.json :

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

La propriété useWorkspaces indique à Lerna de ne pas utiliser lerna.json#packages mais plutôt de rechercher packages.json#workspaces. Selon la documentation Lerna Bootstrap, les deux sont similaires, sauf que Yarn ne prend pas en charge les globes récursifs **.

Mettez à jour Lerna pour supprimer la propriété packages de lerna.json, elle ne contient désormais que :

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

Mettez à jour le fichier packages.json pour ressembler à :

{
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

La propriété private est requise. Toute tentative d’enregistrement d’une nouvelle dépendance sans celle-ci génère une erreur de Yarn sous la forme “Les espaces de travail ne peuvent être activés que dans des projets privés”. Note, il était possible de définir le projet comme «privé» lors de son initialisation avec yarn init. Maintenant que notre projet est un monorepo, c’est le bon moment pour marquer le paquet racine comme privé car il ne sera pas publié dans NPM. Seuls les packages qu’il contient sont destinés à la publication.

Remarquez que l’exécution de lerna init maintenant synchronisera les packages.json#workspaces dans lerna.json#packages avec les nouvelles valeurs.

Maintenant, enregistrez les modifications :

git commit -a -m 'build: activate yarn usage'

Si vous n’êtes pas familier avec Git, le paramètre -a ajoute tous les fichiers modifiés au commit. Les nouveaux fichiers ne sont pas pris en compte.

Emplacement du package

Par défaut, Lerna gère les paquets dans le dossier “packages”. La majorité des projets utilisant Lerna utilise cette convention. C’est une bonne idée de la respecter. Mais dans notre cas, nous avons deux types de plugins :

  • Les plugins Gatsby
  • Les plugins Gatsby Remark qui étendent le plugin gatsby-transformer-remarque

Ainsi, je modifie le tableau workspaces dans le fichier packages.json pour qu’il soit :

{
  "workspaces": [
    "gatsby/*",
    "gatsby-remark/*"
  ]
}

L’emplacement des packages est enregistré :

git commit -a -m 'build: workspaces declaration'

Création de packages

Importons deux packages à des fins de test. Ils se trouvent actuellement dans mon dossier /tmp :

ls -l /tmp/gatsby-caddy-redirects-conf
total 16
-rw-r--r--@ 1 david  staff   981B Nov 26 21:20 gatsby-node.js
-rw-r--r--@ 1 david  staff   239B Nov 26 21:19 package.json
ls -l /tmp/gatsby-remark-title-to-frontmatter
total 16
-rw-r--r--  1 david  staff   1.2K Nov 26 11:35 index.js
-rw-r--r--@ 1 david  staff   309B Nov 26 21:14 package.json

Pour importer les packages et valider :

mkdir gatsby gatsby-remark
# Import first plugin
mv /tmp/gatsby-caddy-redirects-conf gatsby/caddy-redirects-conf
git add gatsby/caddy-redirects-conf
# Import second plugin
mv /tmp/gatsby-remark-title-to-frontmatter gatsby-remark/title-to-frontmatter
git add gatsby-remark/title-to-frontmatter
# Commit the changes
git commit -m 'build: import project'

Aide-mémoire

Initialisation du package :

yarn init

Initialisation du monorepo :

yarn add lerna
yarn lerna init
# ou
yarn lerna init --independent
# ensuite
git add lerna.json package.json
git commit -m 'build: lerna initialization'

Ignorance de fichier de lock (en option) :

echo 'package-lock.json' >> .gitignore
echo 'yarn.lock' >> .gitignore
git add .gitignore
git commit -m "build: ignore lock files"

Intégration Yarn (sauf si vous utilisez NPM), supprimez la propriété package de lerna.json et :

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

Mettez à jour le fichier packages.json pour qu’il contienne :

{
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

Ensuite

L’article suivant couvre les stratégies de versionnement et de publication des packages avec Lerna.

Canada - Morocco - France

International locations

10 rue de la Kasbah
2393 Rabbat
Canada

Nous sommes une équipe passionnée par l'Open Source, le Big Data et les technologies associées telles que le Cloud, le Data Engineering, la Data Science le DevOps…

Nous fournissons à nos clients un savoir faire reconnu sur la manière d'utiliser les technologies pour convertir leurs cas d'usage en projets exploités en production, sur la façon de réduire les coûts et d'accélérer les livraisons de nouvelles fonctionnalités.

Si vous appréciez la qualité de nos publications, nous vous invitons à nous contacter en vue de coopérer ensemble.