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

Ajout et Structuration des Applications et Librairies dans un Monorepo

Bienvenue dans cette leçon dédiée à un aspect fondamental de la gestion des monorepos : l'ajout et la structuration efficace de vos applications et librairies. Dans le cadre de notre cours "Maîtriser les Monorepos : Optimisez votre Développement Web et Mobile", comprendre comment organiser votre code est aussi crucial que de choisir les bons outils. Une structure bien pensée garantit la maintenabilité, la performance et la scalabilité de votre projet sur le long terme.

Cette leçon vous guidera à travers les principes, les bonnes pratiques et les étapes concrètes pour intégrer harmonieusement de nouveaux projets dans votre monorepo.

Introduction : L'Importance Cruciale de la Structure

Un monorepo, par définition, est un dépôt unique contenant le code de multiples projets. Sans une organisation claire et cohérente, il peut rapidement devenir un "big ball of mud", perdant tous les avantages qu'il est censé offrir. L'objectif principal de cette leçon est de vous fournir les clés pour structurer votre monorepo de manière à :

  • Améliorer la clarté et la découvrabilité des projets.
  • Faciliter la collaboration entre les équipes.
  • Optimiser les performances des outils de build et de test.
  • Simplifier la gestion des dépendances internes et externes.
  • Assurer la scalabilité de votre codebase à mesure qu'elle grandit.

Nous allons explorer les conventions de nommage, les outils d'aide à la structuration et, surtout, des exemples pratiques pour ajouter des applications web, mobiles ou des librairies partagées.

Pourquoi la Structure Compte dans un Monorepo ?

La structure de votre monorepo n'est pas qu'une question d'esthétique. Elle a un impact direct et profond sur l'efficacité de votre développement :

  • Clarté et Organisation

    Une arborescence logique rend instantanément clair ce qui est une application déployable et ce qui est une librairie réutilisable. Cela réduit la charge cognitive pour les nouveaux arrivants et les développeurs travaillant sur plusieurs projets.

  • Maintenabilité Accrue

    Des conventions claires minimisent les erreurs et facilitent la localisation du code pertinent pour une fonctionnalité ou une correction de bug. Un code bien organisé est plus facile à comprendre, à modifier et à tester.

  • Optimisation des Outils de Monorepo

    Des outils comme Turborepo, Nx ou Lerna s'appuient fortement sur une structure prévisible pour des fonctionnalités clés comme :

    • Caching intelligent : Identifier les projets qui n'ont pas changé pour éviter de les reconstruire.
    • Détection d'impact : Savoir quels projets sont affectés par une modification dans une librairie partagée.
    • Exécution parallèle : Orchestrer les tâches de build, de test ou de déploiement de manière efficiente.
  • Collaboration Simplifiée

    Lorsque plusieurs équipes travaillent sur des projets différents au sein du même monorepo, une structure cohérente évite les conflits et permet à chacun de trouver rapidement les ressources dont il a besoin, favorisant ainsi le partage de code et la réutilisation.

  • Scalabilité et Évolution

    Un monorepo est conçu pour grandir. Une bonne structure est la fondation qui permet d'ajouter de nouvelles applications et librairies sans que l'ensemble devienne un labyrinthe ingérable. Elle encourage un découpage logique et des responsabilités claires pour chaque module.

Principes Fondamentaux de Structuration

La plupart des monorepos modernes suivent des conventions de structuration assez similaires. Le principe directeur est de séparer les applications déployables des librairies réutilisables.

  • Découpage par Type (Convention apps/ et packages/ ou libs/)

    C'est la convention la plus courante et la plus recommandée.

    • apps/ (Applications) : Ce répertoire contient toutes les unités de code qui sont destinées à être déployées et exécutées indépendamment.

      • Exemples : web-dashboard, mobile-app, api-gateway, admin-panel, microservice-auth.
      • Chaque sous-dossier dans apps/ représente une application distincte.
    • packages/ ou libs/ (Librairies / Packages) : Ce répertoire contient le code réutilisable qui n'est pas destiné à être déployé seul, mais plutôt à être consommé par une ou plusieurs applications (ou d'autres librairies).

      • Exemples : ui-components (composants React/Vue partagés), utils (fonctions utilitaires génériques), design-system (tokens de style), api-client (clients HTTP pré-configurés), shared-types (interfaces TypeScript partagées).
      • Chaque sous-dossier dans packages/ ou libs/ représente une librairie/un package distinct.
  • Cohérence Nommage

    Adoptez une convention de nommage claire et suivez-la scrupuleusement.

    • Utilisez des tirets (kebab-case) pour les noms de dossiers et de packages (ex: web-dashboard, ui-components).
    • Les noms doivent être descriptifs et concis.
  • Dépendances Explicites

    Chaque application ou librairie doit explicitement déclarer ses dépendances dans son propre fichier package.json. Les outils de monorepo (comme Yarn Workspaces ou pnpm Workspaces) gèrent ensuite les liens symboliques (symlinks) pour que les dépendances internes soient résolues correctement.

  • Isolation des Responsabilités

    Chaque package ou application doit avoir une responsabilité unique et bien définie. Cela favorise la modularité, réduit les couplages et simplifie les tests. Si un package commence à prendre trop de responsabilités, il est peut-être temps de le diviser en plusieurs packages plus petits.

Les Outils pour Gérer la Structure des Monorepos

La gestion des monorepos n'est pas uniquement une affaire de convention ; des outils spécialisés facilitent grandement la tâche :

  • Yarn Workspaces / pnpm Workspaces / npm Workspaces

    Ce sont les gestionnaires de paquets natifs pour les monorepos. Ils permettent de :

    • Installer les dépendances de tous les projets en une seule commande depuis la racine du monorepo.
    • Lier les dépendances internes (entre les packages du monorepo) via des liens symboliques, évitant ainsi d'avoir à les publier séparément.
    • Dédupliquer les dépendances externes pour économiser de l'espace disque. Ils sont la fondation de tout monorepo moderne.
  • Turborepo

    Axé sur la performance, Turborepo excelle dans l'optimisation des tâches de build et de test grâce à un système de caching distribué et d'exécution parallèle. Il s'intègre parfaitement avec Yarn/pnpm Workspaces.

  • Nx (Nrwl Extensible)

    Un framework de développement complet pour les monorepos. Nx offre :

    • Génération de code (schematics) pour ajouter facilement de nouvelles applications/librairies.
    • Graphes de dépendances pour visualiser les relations entre les projets.
    • Détection d'impact pour savoir exactement quels projets sont affectés par un changement.
    • Optimisation des tâches similaire à Turborepo, mais avec une approche plus prescriptive.
  • Lerna

    Historiquement l'un des premiers outils de monorepo, Lerna est excellent pour la publication de multiples packages depuis un seul dépôt. Bien que certains de ses rôles (comme la gestion des dépendances) aient été repris par les gestionnaires de paquets natifs, il reste utile pour des workflows de versioning et de publication complexes.

Mise en Pratique : Ajout d'une Nouvelle Application ou Librairie

Maintenant, voyons comment ajouter concrètement un nouveau projet à votre monorepo. Nous utiliserons pnpm comme gestionnaire de paquets pour nos exemples, mais les principes sont similaires avec Yarn ou npm Workspaces.

Prérequis : Un Monorepo Initialisé

Assurez-vous que votre monorepo est déjà configuré avec un gestionnaire de paquets supportant les workspaces. Typiquement, cela signifie avoir un fichier pnpm-workspace.yaml (pour pnpm) ou un champ "workspaces" dans votre package.json (pour Yarn/npm) à la racine du monorepo.

Voici à quoi pourrait ressembler un pnpm-workspace.yaml typique à la racine de votre monorepo :

# monorepo-root/pnpm-workspace.yaml
packages:
  - 'apps/*'       # Inclut tous les dossiers directs dans 'apps/'
  - 'packages/*'   # Inclut tous les dossiers directs dans 'packages/'
  # Vous pouvez ajouter d'autres patterns si nécessaire, ex: 'tools/*', 'tests/*'

Structure Exemple d'un Monorepo

Pour mieux visualiser, voici une structure de monorepo que nous allons manipuler :

monorepo-root/
├── apps/
│   ├── web-app/         # Application web Next.js principale
│   └── docs-site/       # Site de documentation (ex: Docusaurus, VitePress)
├── packages/
│   ├── ui-components/   # Librairie de composants UI React
│   ├── utils/           # Librairie de fonctions utilitaires génériques
│   └── api-client/      # Client API pour communiquer avec les services backend
├── pnpm-workspace.yaml  # Configuration pnpm pour les workspaces
└── package.json         # Fichier package.json racine (scripts globaux, etc.)

Étape 1: Créer le Répertoire du Nouveau Projet

Décidez si votre nouveau projet est une application (apps/) ou une librairie (packages/) et créez le dossier correspondant.

  • Exemple : Ajout d'une nouvelle application web admin-dashboard
    # Depuis la racine du monorepo
    mkdir -p apps/admin-dashboard
    cd apps/admin-dashboard
    
  • Exemple : Ajout d'une nouvelle librairie shared-types
    # Depuis la racine du monorepo
    mkdir -p packages/shared-types
    cd packages/shared-types
    

Étape 2: Initialiser le Projet

Chaque projet au sein du monorepo doit être un package Node.js à part entière, avec son propre package.json.

  • Pour une application (ex: Next.js pour admin-dashboard) Utilisez le CLI du framework si disponible. Cela créera le package.json et les fichiers de base.

    # Dans le répertoire apps/admin-dashboard/
    npx create-next-app@latest . --typescript --eslint --tailwind --app --use-pnpm
    # Suivez les instructions, choisissez le nom du projet "admin-dashboard"
    
  • Pour une librairie (ex: shared-types) Initialisez simplement un package.json.

    # Dans le répertoire packages/shared-types/
    pnpm init
    # Répondez aux questions, le nom doit être "shared-types"
    

    Ou plus simplement pour initialiser avec des valeurs par défaut :

    # Dans le répertoire packages/shared-types/
    pnpm init -y
    

Étape 3: Déclarer le Projet dans la Configuration du Monorepo

Si vous avez utilisé des glob patterns comme apps/* et packages/* dans votre pnpm-workspace.yaml (ce qui est le cas dans notre exemple), le nouveau répertoire sera automatiquement inclus. Sinon, vous devrez ajouter manuellement le chemin du nouveau projet.

Pas besoin de modifier pnpm-workspace.yaml si apps/* et packages/* sont déjà présents et couvrent votre nouveau projet.

Étape 4: Gérer les Dépendances

C'est là que la puissance des workspaces entre en jeu.

  • Installer les dépendances externes : Allez à la racine du monorepo et exécutez votre commande d'installation :

    # Depuis la racine du monorepo
    pnpm install
    # Ou pour un package spécifique
    pnpm add react-query --filter admin-dashboard
    

    Cela installera toutes les dépendances listées dans tous les package.json des projets du monorepo, en les dédupliquant et en gérant les symlinks.

  • Ajouter des dépendances internes (entre projets du monorepo) : C'est le scénario le plus courant et le plus puissant. Supposons que notre admin-dashboard veuille utiliser la librairie ui-components et api-client.

    # Depuis la racine du monorepo
    pnpm add ui-components api-client --filter admin-dashboard
    

    Cette commande va mettre à jour le package.json de apps/admin-dashboard pour y inclure ui-components et api-client comme dépendances. Le gestionnaire de paquets utilisera un lien symbolique pour pointer vers les versions locales des packages, au lieu de les télécharger depuis un registre npm.

    Voici à quoi ressemblerait le package.json de apps/admin-dashboard après l'ajout :

    // apps/admin-dashboard/package.json
    {
      "name": "admin-dashboard",
      "version": "0.1.0",
      "private": true,
      "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "lint": "next lint"
      },
      "dependencies": {
        "next": "14.x.x",
        "react": "^18.2.0",
        "react-dom": "^18.2.0",
        "ui-components": "workspace:^1.0.0", // <-- Dépendance interne vers packages/ui-components
        "api-client": "workspace:^1.0.0"     // <-- Dépendance interne vers packages/api-client
      },
      "devDependencies": {
        "@types/node": "^20",
        "@types/react": "^18",
        "@types/react-dom": "^18",
        "autoprefixer": "^10.0.1",
        "eslint": "^8",
        "eslint-config-next": "14.x.x",
        "postcss": "^8",
        "tailwindcss": "^3.3.0",
        "typescript": "^5"
      }
    }
    

    Notez la syntaxe workspace:^1.0.0. Elle indique à pnpm (et Yarn) que c'est une dépendance interne au monorepo et qu'il faut utiliser la version locale (symlink) du package ui-components avec une version compatible avec ^1.0.0.

Étape 5: Scripts de Construction et de Test

Chaque projet doit avoir ses propres scripts build, test, dev (ou start) dans son package.json. Ces scripts définissent comment le projet est construit ou exécuté.

  • Exemple : packages/ui-components/package.json

    // packages/ui-components/package.json
    {
      "name": "ui-components",
      "version": "1.0.0",
      "main": "dist/index.js",
      "module": "dist/index.mjs",
      "types": "dist/index.d.ts",
      "files": ["dist"],
      "scripts": {
        "build": "tsc --build",
        "test": "jest",
        "lint": "eslint .",
        "dev": "tsc --watch"
      },
      "dependencies": {
        "react": "^18.2.0",
        "react-dom": "^18.2.0"
      },
      "devDependencies": {
        "@types/react": "^18.2.0",
        "@types/react-dom": "^18.2.0",
        "typescript": "^5.0.0",
        "jest": "^29.0.0",
        "@swc/jest": "^0.2.29",
        "@types/jest": "^29.5.10",
        "eslint": "^8.56.0",
        "eslint-config-custom": "workspace:^1.0.0"
      }
    }
    

    Ici, ui-components a des scripts pour sa propre compilation TypeScript, ses tests Jest, etc.

  • Exécution de scripts depuis la racine du monorepo Les outils de monorepo vous permettent d'exécuter ces scripts pour un projet spécifique ou pour tous les projets qui en ont un :

    # Exécuter le script 'build' pour le package 'ui-components'
    pnpm --filter ui-components build
    
    # Exécuter le script 'dev' pour l'application 'admin-dashboard'
    pnpm --filter admin-dashboard dev
    
    # Exécuter le script 'test' pour tous les packages qui l'ont
    pnpm test # (si un script 'test' est défini dans le package.json racine pour tous les workspaces)
    # Ou avec Turborepo/Nx pour une meilleure performance
    # turbo run test
    # nx run-many --target=test
    

Bonnes Pratiques et Pièges à Éviter

Une fois vos projets ajoutés, il est essentiel de suivre certaines pratiques pour maintenir l'ordre et l'efficacité de votre monorepo.

  • Granularité Adaptée

    Ne découpez pas trop vos librairies. Un package pour trois fonctions utilitaires simples peut être excessif. L'objectif est un découpage logique et réutilisable, pas une micro-segmentation à outrance. Commencez plus large et découpez quand le besoin de réutilisation ou de responsabilité unique apparaît clairement.

  • Pas de Dépendances Cycliques

    Un projet A ne doit pas dépendre d'un projet B, qui dépendrait à son tour du projet A. Cela crée des boucles de dépendances qui rendent le code difficile à comprendre, à tester et à maintenir. Les outils de monorepo (comme Nx) peuvent souvent détecter ces cycles.

  • Versions Cohérentes et Gestion du Versioning

    Pour les dépendances internes, utilisez la syntaxe workspace:* (ou workspace:^1.0.0, workspace:~1.0.0) pour indiquer que vous voulez la version locale du package. Pour les dépendances externes, assurez-vous de maintenir une cohérence. Des outils comme changesets sont excellents pour gérer le versioning et la publication de multiples packages dans un monorepo.

  • Documentation Claire

    Chaque package/application doit avoir une documentation minimale dans son propre README.md expliquant son rôle, comment l'exécuter et comment y contribuer.

  • Conventions de Code Unifiées

    Implémentez des outils comme ESLint, Prettier, et TypeScript (avec des configurations partagées) à la racine du monorepo. Cela garantit que tout le code suit les mêmes standards, quel que soit le contributeur ou le projet.

  • Tests Robustes

    Chaque package doit avoir sa propre suite de tests (unitaires, d'intégration). Les outils de monorepo permettent d'exécuter ces tests de manière sélective ou parallèle, ce qui est crucial pour la fiabilité.

  • Gabarits (Templates)

    Pour accélérer l'ajout de nouveaux projets, créez des gabarits ou utilisez des outils de génération de code (comme les schematics de Nx) pour initialiser des applications ou des librairies avec une structure et des configurations prédéfinies.

Conclusion

L'ajout et la structuration des applications et des librairies dans un monorepo sont des pierres angulaires de son succès. En adoptant des conventions claires, en tirant parti des gestionnaires de paquets avec des workspaces et en utilisant des outils comme Turborepo ou Nx, vous transformez un simple dépôt en un environnement de développement hautement optimisé et collaboratif.

Rappelez-vous : la cohérence est reine. Une structure bien pensée réduit la friction, améliore la productivité et prépare votre codebase à une croissance future. N'hésitez pas à expérimenter ces concepts dans votre propre environnement et à adapter les meilleures pratiques aux besoins spécifiques de vos projets. La maîtrise de ces techniques est un pas essentiel pour optimiser votre développement web et mobile avec les monorepos.