Maîtriser les Monorepos : Optimisez votre Développement Web et Mobile
Maîtriser les Monorepos : Optimisez votre Développement Web et Mobile

Gestion des Dépendances et Partage de Code dans un Monorepo

Contexte du cours : Maîtriser les Monorepos : Optimisez votre Développement Web et Mobile


Un monorepo représente une approche puissante pour organiser et gérer un grand nombre de projets logiciels au sein d'un seul dépôt de code. Cependant, pour exploiter pleinement son potentiel, il est crucial de maîtriser deux aspects fondamentaux : la gestion des dépendances et le partage efficace de code. Cette leçon vous guidera à travers les concepts, les outils et les meilleures pratiques pour gérer ces éléments avec succès dans un environnement monorepo.

Introduction : Les Défis du Monorepo

Dans un monorepo, plusieurs applications (front-end web, mobile, back-end API) et bibliothèques partagées coexistent. Si cette centralisation offre des avantages indéniables en termes de visibilité et de cohérence, elle introduit également des défis spécifiques :

  • Gestion des versions de dépendances externes : Comment s'assurer que tous les projets utilisent les versions adéquates des bibliothèques tierces, sans conflits ni duplications inutiles ?
  • Gestion des dépendances internes : Comment faire en sorte que les projets puissent facilement consommer les bibliothèques partagées développées au sein du même monorepo ?
  • Facilitation du partage de code : Comment structurer le monorepo pour encourager la réutilisation de code, minimiser la duplication et maintenir une base de code saine ?

Cette leçon explorera ces défis et proposera des solutions concrètes pour y répondre.

1. Comprendre les Dépendances dans un Monorepo

Avant de plonger dans les outils, il est essentiel de bien comprendre la nature des dépendances dans un monorepo.

1.1. Types de Dépendances

On distingue principalement deux catégories de dépendances :

  1. Dépendances externes : Ce sont des paquets (libraries, frameworks, outils) provenant de registres publics (comme npmjs.com pour JavaScript/TypeScript) ou privés. Par exemple, React, Vue, Express, Lodash, etc. Leur gestion implique de s'assurer que chaque projet utilise la version compatible et stable requise.
  2. Dépendances internes : Ce sont des paquets ou des modules développés au sein même du monorepo et destinés à être partagés entre plusieurs projets. Il peut s'agir de :
    • Composants d'interface utilisateur (UI)
    • Utilitaires métier
    • Fonctions d'authentification
    • Types TypeScript partagés
    • Configurations de linters ou de builders

1.2. Les Défis Spécifiques au Monorepo

  • Cohérence des versions : Un monorepo aide à standardiser les versions des dépendances externes, mais il faut une stratégie pour gérer les mises à jour et les potentielles ruptures.
  • Lien symbolique (symlink) : Les dépendances internes ne sont pas installées depuis un registre ; elles doivent être liées localement pour être utilisables par d'autres projets du monorepo. C'est un aspect fondamental que les outils de monorepo gèrent automatiquement.
  • Duplication des node_modules : Sans une gestion adéquate, chaque projet dans un monorepo pourrait installer sa propre copie des dépendances, entraînant une utilisation excessive d'espace disque et des temps d'installation lents.
  • Graphe de dépendances : Les outils de monorepo construisent souvent un graphe des dépendances, interne et externe, pour optimiser les opérations de construction, de test et de déploiement.

2. Outils et Stratégies pour la Gestion des Dépendances

Plusieurs outils ont été développés pour simplifier la gestion des dépendances dans un monorepo.

2.1. Les Workspaces des Gestionnaires de Paquets (npm, Yarn, pnpm)

Les fonctionnalités de "workspaces" sont la pierre angulaire de la gestion des dépendances dans la plupart des monorepos JavaScript/TypeScript. Elles permettent de gérer plusieurs paquets au sein d'un seul dépôt racine.

  • Comment ça marche ? Le fichier package.json à la racine du monorepo déclare une liste de répertoires (les "workspaces") où se trouvent les paquets. Le gestionnaire de paquets (npm, Yarn, pnpm) installe ensuite toutes les dépendances à la racine du monorepo, créant des liens symboliques vers les node_modules de chaque workspace si nécessaire. Pour les dépendances internes, il les lie automatiquement entre elles.

  • Avantages :

    • Installation centralisée : Toutes les dépendances sont installées une seule fois à la racine, réduisant l'espace disque et les temps d'installation.
    • Liaison automatique : Les paquets internes peuvent se dépendre mutuellement sans avoir besoin d'être publiés sur un registre. Le gestionnaire de paquets crée les liens symboliques nécessaires.
    • Cohérence des versions : Facilite la mise à jour des dépendances communes à tous les projets.

Exemple de Configuration de Workspaces (npm/Yarn)

Voici comment configurer les workspaces dans le package.json racine de votre monorepo :

// monorepo-root/package.json
{
  "name": "mon-super-monorepo",
  "version": "1.0.0",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "build": "npm run build --workspaces",
    "test": "npm run test --workspaces"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

Explication du code :

  • "name": "mon-super-monorepo" : Le nom du monorepo.
  • "private": true : Indique que ce package racine n'est pas destiné à être publié. C'est une bonne pratique pour les monorepos.
  • "workspaces": ["apps/*", "packages/*"] : C'est la clé ! Elle indique que tous les sous-répertoires dans apps/ et packages/ sont des "workspaces" (des projets ou bibliothèques distinctes) que le gestionnaire de paquets doit prendre en compte.
  • "scripts" : Des scripts peuvent être définis pour exécuter des commandes sur tous les workspaces. Par exemple, npm run build --workspaces lancera le script build dans chaque package.json de chaque workspace.
  • "devDependencies" : Les dépendances globales ou les outils partagés par plusieurs projets peuvent être listés ici, comme TypeScript.

Chaque sous-projet (par exemple, apps/web ou packages/ui) aura son propre package.json déclarant ses dépendances spécifiques :

// monorepo-root/packages/ui/package.json
{
  "name": "@mon-super-monorepo/ui",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc"
  },
  "dependencies": {
    "react": "^18.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

Dans cet exemple, @mon-super-monorepo/ui est un package interne, dépendant de React.

2.2. pnpm et les Workspaces

pnpm est un gestionnaire de paquets alternatif qui gagne en popularité pour les monorepos. Il utilise une approche unique pour l'installation des dépendances :

  • Lien symbolique non plat : Contrairement à npm/Yarn qui "aplatissent" les node_modules, pnpm utilise des liens symboliques pour créer une structure de node_modules imbriquée mais dédupliquée au niveau du système de fichiers.
  • Avantages spécifiques :
    • Déduplication optimale : pnpm stocke les modules dans un répertoire global et crée des liens symboliques depuis là. Cela signifie que si dix projets dépendent de React, il n'est stocké qu'une seule fois sur votre disque.
    • Performance accrue : Les installations sont souvent plus rapides grâce à la déduplication et à l'approche de liens symboliques.
    • Stricte : pnpm est plus strict quant aux dépendances déclarées, aidant à éviter le "phantom dependencies" (dépendances utilisées mais non déclarées).

Pour utiliser pnpm avec les workspaces, la configuration est similaire, mais vous ajoutez un fichier pnpm-workspace.yaml à la racine :

# monorepo-root/pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

2.3. Lerna

Lerna est un outil populaire (historiquement très utilisé) qui ajoute des fonctionnalités de gestion de workflow au-dessus des workspaces de npm/Yarn. Il aide à :

  • Initialiser un monorepo (lerna init).
  • Installer les dépendances (lerna bootstrap - équivalent à npm install avec workspaces mais avec des options supplémentaires).
  • Exécuter des commandes sur des paquets spécifiques ou tous les paquets (lerna run build).
  • Gérer le versioning et la publication des paquets (lerna publish).

Bien que les gestionnaires de paquets modernes aient absorbé certaines de ses fonctionnalités, Lerna reste pertinent pour des workflows de publication et de versioning complexes dans des monorepos.

2.4. Nx (Nrwl Extensible)

Nx est un smart build system et une suite d'outils plus complète, conçue spécifiquement pour les grands monorepos. Il va au-delà de la simple gestion des dépendances :

  • Graphe de dépendances intelligent : Nx analyse les dépendances internes et externes de votre monorepo pour construire un graphe. Cela lui permet de savoir exactement quels projets sont affectés par un changement.
  • Détection d'impact : Nx peut identifier les projets qui doivent être reconstruits ou testés suite à une modification de code.
  • Cache de compilation : Il cache les résultats des tâches de construction et de test, évitant de refaire le travail si le code n'a pas changé.
  • Génération de code : Des schémas puissants pour générer des applications, des bibliothèques et des composants.
  • Intégration : Fonctionne avec différents frameworks (React, Angular, Next.js, NestJS, etc.) et gestionnaires de paquets.

Nx peut être utilisé conjointement avec npm, Yarn ou pnpm workspaces. Il ajoute une couche d'intelligence et d'optimisation par-dessus.

3. Partage de Code et Réutilisation

Le partage de code est l'un des principaux moteurs de l'adoption d'un monorepo. Il permet de maintenir la cohérence, de réduire la duplication et d'accélérer le développement.

3.1. Pourquoi Partager du Code ?

  • Consistance : Assurer que les composants UI, les logiques métier et les conventions de code sont uniformes à travers toutes les applications.
  • Réduction de la duplication : Éviter de réécrire le même code pour différentes applications, réduisant ainsi la taille totale de la base de code et la charge de maintenance.
  • Maintenabilité améliorée : Un bug corrigé dans un module partagé est corrigé pour toutes les applications qui l'utilisent.
  • Rapidité de développement : Les développeurs peuvent se concentrer sur les fonctionnalités spécifiques de l'application en utilisant des blocs de construction déjà validés et testés.

3.2. Comment Partager du Code via les Packages Internes

La méthode la plus courante pour partager du code dans un monorepo est de créer des packages internes (souvent appelés "bibliothèques" ou "libs" dans les monorepos Nx).

  1. Création du package interne :

    • Créez un nouveau répertoire sous packages/ (ou libs/ pour Nx), par exemple packages/ui pour une bibliothèque de composants UI ou packages/utils pour des fonctions utilitaires.
    • Chaque package interne aura son propre package.json définissant son nom (souvent scopé, ex: @mon-super-monorepo/ui), sa version, ses scripts de build et ses dépendances spécifiques.
  2. Exposition du code :

    • Le package interne expose ses fonctionnalités via un fichier d'entrée (souvent src/index.ts ou src/main.ts) qui export les éléments que les autres packages peuvent utiliser.
  3. Consommation du package interne :

    • N'importe quel autre projet ou package dans le monorepo peut déclarer une dépendance à ce package interne dans son package.json. Grâce aux workspaces, le gestionnaire de paquets liera ce package localement.
    • Pour les versions des packages internes, on utilise souvent *, latest, ou workspace: (avec Yarn 2+, npm 7+, pnpm) pour indiquer que le package doit être lié à la version locale du monorepo.

Exemple de Partage et de Consommation de Code

Imaginez que nous ayons une bibliothèque de composants UI dans packages/ui et une application web dans apps/web.

// monorepo-root/packages/ui/src/Button.tsx
import React from 'react';

interface ButtonProps {
  onClick: () => void;
  children: React.ReactNode;
}

export const Button: React.FC<ButtonProps> = ({ onClick, children }) => {
  return (
    <button
      onClick={onClick}
      style={{
        padding: '10px 20px',
        backgroundColor: '#007bff',
        color: 'white',
        border: 'none',
        borderRadius: '5px',
        cursor: 'pointer',
      }}
    >
      {children}
    </button>
  );
};

Explication du code : Ce fichier Button.tsx définit un simple composant React de bouton. Il est exporté pour être utilisable par d'autres modules. Pour que ce package soit consommable, son package.json devrait avoir un main ou exports pointant vers son fichier de sortie après compilation (ex: dist/index.js).

Maintenant, voyons comment notre application web peut utiliser ce composant :

// monorepo-root/apps/web/src/App.tsx
import React from 'react';
// Import du composant Button depuis le package interne @mon-super-monorepo/ui
import { Button } from '@mon-super-monorepo/ui'; 

function App() {
  const handleClick = () => {
    alert('Bouton cliqué depuis l\'application web !');
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Mon Application Web</h1>
      <Button onClick={handleClick}>
        Cliquez-moi !
      </Button>
    </div>
  );
}

export default App;

Explication du code :

  • L'application web importe le composant Button directement depuis @mon-super-monorepo/ui.
  • Lors de l'installation des dépendances du monorepo, le gestionnaire de paquets (grâce aux workspaces) détecte que @mon-super-monorepo/ui est un package interne et crée un lien symbolique vers son emplacement physique dans le monorepo. Cela permet à l'application web d'utiliser le code du package UI sans avoir à le publier sur un registre de paquets.

3.3. Architectures Modulaires

Le monorepo encourage une architecture modulaire. Chaque bibliothèque ou application doit avoir une responsabilité claire et des interfaces bien définies. Pensez à ces packages comme à des microservices, mais au sein du même dépôt :

  • Faible couplage : Les packages devraient être aussi indépendants que possible.
  • Haute cohésion : Le code à l'intérieur d'un package doit être étroitement lié et servir un objectif commun.
  • Types partagés : Pour les projets TypeScript, la création d'un package packages/types est une excellente pratique pour centraliser les interfaces et les types de données utilisés par plusieurs projets.

4. Bonnes Pratiques et Pièges à Éviter

Pour maximiser les avantages de la gestion des dépendances et du partage de code dans un monorepo, suivez ces bonnes pratiques et soyez conscient des pièges.

4.1. Bonnes Pratiques

  • Définir des frontières claires : Chaque package doit avoir une responsabilité unique et bien définie. Évitez les packages "fourre-tout".
  • Documentation : Documentez l'objectif de chaque package, ses interfaces publiques et son utilisation.
  • Tests unitaires et d'intégration : Testez rigoureusement les packages partagés. Un bug dans un package partagé peut affecter de nombreuses applications.
  • Versioning sémantique (SemVer) : Appliquez le SemVer même pour les packages internes. Cela aide à communiquer les changements majeurs, mineurs ou correctifs. Pour les dépendances internes, l'utilisation de workspace:* ou file: dans package.json est une pratique courante pour lier à la version locale du monorepo.
    // apps/web/package.json
    {
      "name": "@mon-super-monorepo/web",
      "version": "1.0.0",
      "dependencies": {
        "@mon-super-monorepo/ui": "workspace:*", // Ou "1.0.0" si vous gérez des versions explicites
        "react": "^18.0.0"
      }
    }
    
  • Utiliser un linter/formatter commun : Standardisez le style de code à travers tout le monorepo pour une meilleure lisibilité et maintenabilité.
  • Audits de sécurité des dépendances : Utilisez des outils comme npm audit ou des services comme Snyk pour surveiller les vulnérabilités dans vos dépendances externes.

4.2. Pièges à Éviter

  • Couplage fort et dépendances circulaires : Évitez que les packages dépendent trop les uns des autres, surtout de manière circulaire (A dépend de B, et B dépend de A). Cela rend le monorepo difficile à maintenir et à faire évoluer.
  • Duplication de code non intentionnelle : Bien que le monorepo vise à réduire la duplication, il peut arriver que du code similaire soit écrit dans différents packages par manque de communication ou de visibilité. Les outils comme Nx avec leur graphe de dépendances peuvent aider à visualiser la réutilisation.
  • Mélange des préoccupations : Ne mélangez pas la logique métier, les composants UI et les utilitaires dans un seul package. Séparez-les en modules distincts pour une meilleure organisation.
  • Oublier de compiler les packages internes : Les packages TypeScript/JSX doivent souvent être compilés en JavaScript avant d'être utilisés par d'autres projets. Assurez-vous que votre workflow de build gère cela (par exemple, en ayant un script build pour chaque package ou en utilisant un runner de tâches comme Nx/TurboRepo).
  • Ne pas nettoyer les node_modules : Les problèmes d'installation peuvent parfois être résolus en supprimant le répertoire node_modules et le fichier de verrouillage (package-lock.json, yarn.lock, pnpm-lock.yaml) à la racine, puis en relançant l'installation.

Conclusion

La gestion des dépendances et le partage de code sont des piliers fondamentaux d'un monorepo réussi. En tirant parti des workspaces des gestionnaires de paquets comme npm, Yarn ou pnpm, en exploitant des outils de workflow comme Lerna, ou en adoptant des systèmes de build intelligents comme Nx, vous pouvez créer un environnement de développement cohérent, performant et hautement réutilisable.

Une architecture modulaire, des frontières claires entre les packages et l'adhésion aux bonnes pratiques sont essentielles pour exploiter pleinement le potentiel de votre monorepo et faire de la collaboration et de l'innovation une réalité au sein de votre équipe. En maîtrisant ces aspects, vous poserez les bases d'un développement efficace et pérenne pour vos applications web et mobiles.