Gestion de Monorepo
Construisez des monorepos efficaces et scalables qui permettent le partage de code, des outils cohérents et des changements atomiques entre plusieurs packages et applications.
Quand utiliser cette skill
- Configurer de nouveaux projets monorepo
- Migrer d'une structure multi-repo à monorepo
- Optimiser les performances de build et test
- Gérer les dépendances partagées
- Implémenter des stratégies de partage de code
- Configurer CI/CD pour les monorepos
- Versionnage et publication de packages
- Déboguer les problèmes spécifiques aux monorepos
Concepts fondamentaux
1. Pourquoi les monorepos ?
Avantages :
- Partage de code et dépendances
- Commits atomiques entre projets
- Outils et standards cohérents
- Refactoring plus facile
- Gestion simplifiée des dépendances
- Meilleure visibilité du code
Défis :
- Performance de build à grande échelle
- Complexité CI/CD
- Contrôle d'accès
- Large dépôt Git
2. Outils monorepo
Gestionnaires de packages :
- pnpm workspaces (recommandé)
- npm workspaces
- Yarn workspaces
Systèmes de build :
- Turborepo (recommandé pour la plupart)
- Nx (riche en fonctionnalités, complexe)
- Lerna (ancien, mode maintenance)
Configuration Turborepo
Configuration initiale
# Créer un nouveau monorepo
npx create-turbo@latest my-monorepo
cd my-monorepo
# Structure :
# apps/
# web/ - App Next.js
# docs/ - Site de documentation
# packages/
# ui/ - Composants UI partagés
# config/ - Configurations partagées
# tsconfig/ - Configs TypeScript partagées
# turbo.json - Configuration Turborepo
# package.json - package.json racine
Configuration
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"type-check": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
// package.json (racine)
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"test": "turbo run test",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"clean": "turbo run clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^1.10.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
},
"packageManager": "pnpm@8.0.0"
}
Structure de package
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./button": {
"import": "./dist/button.js",
"types": "./dist/button.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts",
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
"lint": "eslint src/",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*",
"tsup": "^7.0.0",
"typescript": "^5.0.0"
},
"dependencies": {
"react": "^18.2.0"
}
}
Workspaces pnpm
Configuration
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tools/*"
// .npmrc
# Hoist des dépendances partagées
shamefully-hoist=true
# Peer dependencies strictes
auto-install-peers=true
strict-peer-dependencies=true
# Performance
store-dir=~/.pnpm-store
Gestion des dépendances
# Installer une dépendance dans un package spécifique
pnpm add react --filter @repo/ui
pnpm add -D typescript --filter @repo/ui
# Installer une dépendance workspace
pnpm add @repo/ui --filter web
# Installer dans tous les packages
pnpm add -D eslint -w
# Mettre à jour toutes les dépendances
pnpm update -r
# Supprimer une dépendance
pnpm remove react --filter @repo/ui
Scripts
# Exécuter un script dans un package spécifique
pnpm --filter web dev
pnpm --filter @repo/ui build
# Exécuter dans tous les packages
pnpm -r build
pnpm -r test
# Exécuter en parallèle
pnpm -r --parallel dev
# Filtrer par motif
pnpm --filter "@repo/*" build
pnpm --filter "...web" build # Build web et ses dépendances
Monorepo Nx
Configuration
# Créer un monorepo Nx
npx create-nx-workspace@latest my-org
# Générer des applications
nx generate @nx/react:app my-app
nx generate @nx/next:app my-next-app
# Générer des libraries
nx generate @nx/react:lib ui-components
nx generate @nx/js:lib utils
Configuration
// nx.json
{
"extends": "nx/presets/npm.json",
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"test": {
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
"cache": true
},
"lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
"cache": true
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json"
],
"sharedGlobals": []
}
}
Exécuter des tâches
# Exécuter une tâche pour un projet spécifique
nx build my-app
nx test ui-components
nx lint utils
# Exécuter pour les projets affectés
nx affected:build
nx affected:test --base=main
# Visualiser les dépendances
nx graph
# Exécuter en parallèle
nx run-many --target=build --all --parallel=3
Configurations partagées
Configuration TypeScript
// packages/tsconfig/base.json
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true,
"declaration": true
},
"exclude": ["node_modules"]
}
// packages/tsconfig/react.json
{
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"]
}
}
// apps/web/tsconfig.json
{
"extends": "@repo/tsconfig/react.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Configuration ESLint
// packages/config/eslint-preset.js
module.exports = {
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier",
],
plugins: ["@typescript-eslint", "react", "react-hooks"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2022,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
settings: {
react: {
version: "detect",
},
},
rules: {
"@typescript-eslint/no-unused-vars": "error",
"react/react-in-jsx-scope": "off",
},
};
// apps/web/.eslintrc.js
module.exports = {
extends: ["@repo/config/eslint-preset"],
rules: {
// Règles spécifiques à l'app
},
};
Modèles de partage de code
Motif 1 : Composants UI partagés
// packages/ui/src/button.tsx
import * as React from 'react';
export interface ButtonProps {
variant?: 'primary' | 'secondary';
children: React.ReactNode;
onClick?: () => void;
}
export function Button({ variant = 'primary', children, onClick }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
>
{children}
</button>
);
}
// packages/ui/src/index.ts
export { Button, type ButtonProps } from './button';
export { Input, type InputProps } from './input';
// apps/web/src/app.tsx
import { Button } from '@repo/ui';
export function App() {
return <Button variant="primary">Click me</Button>;
}
Motif 2 : Utilitaires partagés
// packages/utils/src/string.ts
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function truncate(str: string, length: number): string {
return str.length > length ? str.slice(0, length) + "..." : str;
}
// packages/utils/src/index.ts
export * from "./string";
export * from "./array";
export * from "./date";
// Utilisation dans les apps
import { capitalize, truncate } from "@repo/utils";
Motif 3 : Types partagés
// packages/types/src/user.ts
export interface User {
id: string;
email: string;
name: string;
role: "admin" | "user";
}
export interface CreateUserInput {
email: string;
name: string;
password: string;
}
// Utilisé dans le frontend et le backend
import type { User, CreateUserInput } from "@repo/types";
Optimisation de build
Caching Turborepo
// turbo.json
{
"pipeline": {
"build": {
// Build dépend des dépendances construites en premier
"dependsOn": ["^build"],
// Mettre en cache ces outputs
"outputs": ["dist/**", ".next/**"],
// Mettre en cache selon ces inputs (défaut : tous les fichiers)
"inputs": ["src/**/*.tsx", "src/**/*.ts", "package.json"]
},
"test": {
// Exécuter les tests en parallèle, ne pas dépendre de build
"cache": true,
"outputs": ["coverage/**"]
}
}
}
Remote Caching
# Turborepo Remote Cache (Vercel)
npx turbo login
npx turbo link
# Custom remote cache
# turbo.json
{
"remoteCache": {
"signature": true,
"enabled": true
}
}
CI/CD pour les monorepos
GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Pour les commandes Nx affected
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm turbo run build
- name: Test
run: pnpm turbo run test
- name: Lint
run: pnpm turbo run lint
- name: Type check
run: pnpm turbo run type-check
Déployer seulement les changements
# Déployer uniquement les apps modifiées
- name: Deploy affected apps
run: |
if pnpm nx affected:apps --base=origin/main --head=HEAD | grep -q "web"; then
echo "Deploying web app"
pnpm --filter web deploy
fi
Bonnes pratiques
- Versionnage cohérent : Verrouiller les versions de dépendances dans le workspace
- Configurations partagées : Centraliser les configs ESLint, TypeScript, Prettier
- Graphe de dépendances : Le garder acyclique, éviter les dépendances circulaires
- Caching efficace : Configurer les inputs/outputs correctement
- Sécurité des types : Partager les types entre frontend/backend
- Stratégie de test : Tests unitaires dans les packages, E2E dans les apps
- Documentation : README dans chaque package
- Stratégie de release : Utiliser changesets pour le versionnage
Pièges courants
- Dépendances circulaires : A dépend de B, B dépend de A
- Dépendances fantômes : Utiliser des dépendances non listées dans package.json
- Inputs de cache incorrects : Fichiers manquants dans les inputs Turborepo
- Partage excessif : Partager du code qui devrait être séparé
- Partage insuffisant : Dupliquer du code entre packages
- Monorepos volumineux : Sans les bons outils, les builds ralentissent
Publishing de packages
# Utiliser Changesets
pnpm add -Dw @changesets/cli
pnpm changeset init
# Créer un changeset
pnpm changeset
# Versionner les packages
pnpm changeset version
# Publier
pnpm changeset publish
# .github/workflows/release.yml
- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
publish: pnpm release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}