Turborepo, des builds plus rapides en monorepo

Turborepo accélère les builds en monorepo grâce au cache des résultats et au parallélisme. Configuration des tâches, cache distant et erreurs courantes.

L'équipe CodeWorlds24 min de lecture ·

Pratique JavaScript et TypeScript dans des leçons interactives avec un éditeur de code.

Crée un compte gratuit→
Dans cet article18

Turborepo est un outil qui réduit les temps de build dans un dépôt contenant de nombreux paquets. Il s'y prend de deux façons : il se souvient des résultats des tâches et ignore celles dont l'entrée n'a pas changé, et il exécute en parallèle tout ce qui ne dépend de rien d'autre. Vercel le développe sous licence MIT, et la version actuelle de l'outil est 2.10.9.

Le problème qu'il résout

Un monorepo porte un avantage évident et un inconvénient moins évident, et cet outil ne s'occupe que du second.

L'avantage est bien connu : le code partagé vit en un seul endroit, un changement touchant une bibliothèque et une application voyage dans une seule pull request, et les versions des dépendances internes ne divergent jamais. L'inconvénient apparaît à la première croissance sérieuse : tout construire prend de plus en plus de temps, car chaque changement reconstruit tout, y compris les paquets que personne n'a touchés.

L'idée est simple et repose sur deux observations. D'abord, si l'entrée d'une tâche n'a pas changé, sa sortie non plus, donc elle peut être restaurée depuis le cache plutôt que recalculée. Ensuite, les paquets d'un monorepo forment un graphe de dépendances, et les tâches sans lien entre elles peuvent s'exécuter en parallèle plutôt qu'en séquence.

L'effet peut être spectaculaire, précisément parce que dans un dépôt typique, disons un dépôt contenant plusieurs applications Next.js, la plupart des changements touchent une petite partie du code. Un changement dans une application n'exige pas de reconstruire les quatre autres, ni les bibliothèques dont cette application ne dépend pas, et sans un outil de ce genre, cela se produirait quand même.

Cela vaut la peine de savoir ce qu'il ne fait pas, cependant. Il ne gère pas les dépendances, puisque les espaces de travail du gestionnaire de paquets s'en occupent. Il ne publie pas de paquets et ne gère pas le versionnage. Il n'accélère pas non plus le build d'un paquet unique, il se contente de sauter ce build quand c'est possible. Si ton build unique prend huit minutes, il prendra toujours huit minutes après, simplement moins souvent.

La clé du cache, le cœur du sujet

L'efficacité repose entièrement sur ce que l'outil considère comme l'entrée d'une tâche, et c'est de là que viennent la plupart des problèmes.

La clé est un hash qui couvre les fichiers source du paquet, ses dépendances, les versions des outils et les variables d'environnement déclarées dans la configuration. Si tout cela correspond, le résultat vient du cache. Si quoi que ce soit a changé, la tâche s'exécute à nouveau.

La source de problème la plus fréquente en découle : les variables d'environnement non déclarées. Si un build dépend d'une variable contenant une adresse d'API et que cette variable n'est pas listée dans la configuration, l'outil traite deux entrées différentes comme identiques et renvoie un résultat construit pour un autre environnement. Le symptôme, c'est une application de production qui interroge un serveur de test, et la cause, c'est une seule ligne de configuration manquante.

Le problème inverse existe aussi et il est moins dangereux, quoique plus irritant. Des entrées définies de façon trop large font que le cache ne touche jamais, car n'importe quel changement n'importe où dans le dépôt change la clé avec lui. Restreindre les entrées aux fichiers qui influencent vraiment le résultat peut faire passer l'outil d'inutile à efficace.

La troisième chose, ce sont les sorties. Une tâche doit déclarer les fichiers qu'elle produit, car seuls ceux-là entrent dans le cache et seuls ceux-là sont restaurés en cas de succès. Un répertoire de sortie oublié se traduit par une tâche qui se termine instantanément sans aucun de ses résultats, ce qui est souvent pris à tort pour un bug de l'outil lui-même.

À savoir également : l'outil a un mode qui explique pourquoi le cache a raté. Exécuter une tâche avec un niveau de verbosité plus élevé montre le hash calculé et ce qui le compose, donc plutôt que de deviner, tu peux comparer deux exécutions et voir quelle entrée diffère. C'est la première chose à faire quand les temps de build ne s'améliorent pas après l'adoption, et ça localise généralement la cause en quelques minutes.

Une question à part concerne les tâches qui ne produisent aucun fichier, la vérification des types ou l'exécution des tests par exemple. Leur résultat est le simple fait d'avoir réussi, donc elles se déclarent avec une liste de sorties vide. Le cache fonctionne toujours et garde tout son sens, car ce qui est évité, c'est l'exécution elle-même, et c'est généralement la partie coûteuse.

Qu'est-ce que Turborepo ?

Turborepo est un système de build haute performance conçu spécifiquement pour les monorepos JavaScript et TypeScript. Racheté par Vercel en 2021, il est devenu l'un des outils les plus populaires pour gérer de grandes bases de code. L'idée centrale de Turborepo est d'accélérer les builds grâce à un cache intelligent, à l'exécution parallèle et aux builds incrémentaux.

Dans une approche traditionnelle, construire un monorepo signifie exécuter les mêmes tâches de façon répétée, même quand le code n'a pas changé. Turborepo résout ce problème en se souvenant des résultats des builds précédents et en évitant le travail inutile. Le résultat ? Des builds qui prenaient des minutes se terminent maintenant en quelques secondes.

Pourquoi Turborepo ?

Avantages clés de Turborepo

  1. Cache intelligent - Ne reconstruit pas ce qui est déjà construit
  2. Exécution parallèle - Utilise tous les cœurs du CPU
  3. Cache distant - Partage le cache entre l'équipe et la CI
  4. Builds incrémentaux - Construit seulement ce qui a changé
  5. Définition de pipeline - Définit les dépendances entre les tâches
  6. Zéro configuration - Fonctionne avec les workspaces npm, pnpm, yarn
  7. Intégration Vercel - Intégration native avec Vercel
  8. Sous-ensembles élagués - Déploie seulement les paquets nécessaires

Turborepo face aux autres systèmes de build

CaractéristiqueTurborepoNxLernaRush
CacheLocal + distantLocal + distantLocal + distant (via Nx)Local + distant
ConfigurationMinimaleComplexeSimpleComplexe
Courbe d'apprentissageFacileMoyenneFacileDifficile
Cache distantVercel (gratuit)Nx CloudNx CloudAzure Blob ou S3
ÉcosystèmeEn croissanceGrandMaintenu par l'équipe NxEnterprise
Exécuteur de tâchesIntégréIntégrénpm/yarnSur mesure
PrixGratuit + paliers VercelGratuit + paliers CloudGratuitGratuit

Quand choisir Turborepo ?

  • La vitesse est une priorité - le temps le plus rapide jusqu'à un build fonctionnel
  • Tu utilises déjà Vercel - intégration native
  • Configuration simple - configuration minimale
  • Tu utilises des workspaces npm/pnpm/yarn - aucun effort de migration
  • Tu as besoin d'un cache distant - partage entre la CI et les développeurs

Installation et configuration

Nouveau projet

Bash
# Crée un nouveau monorepo avec Turborepo
npx create-turbo@latest my-monorepo

# Ou avec un template spécifique
npx create-turbo@latest my-monorepo --example with-tailwind
npx create-turbo@latest my-monorepo --example kitchen-sink

# Avec pnpm
pnpm dlx create-turbo@latest my-monorepo

Ajout à un projet existant

Bash
# Installe Turborepo
npm install turbo --save-dev

# Ou globalement
npm install turbo --global

# Avec pnpm
pnpm add turbo --save-dev --workspace-root

Structure du projet

my-monorepo/
├── apps/
│   ├── web/                    # Application Next.js
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── docs/                   # Documentation
│   │   ├── src/
│   │   └── package.json
│   └── admin/                  # Panneau d'administration
│       └── package.json
├── packages/
│   ├── ui/                     # Composants UI partagés
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── config-eslint/          # Config ESLint partagée
│   │   └── package.json
│   ├── config-typescript/      # Config TS partagée
│   │   └── package.json
│   └── utils/                  # Utilitaires partagés
│       ├── src/
│       └── package.json
├── package.json                # package.json racine
├── turbo.json                  # Config Turborepo
├── pnpm-workspace.yaml         # Config des workspaces (pnpm)
└── .gitignore

package.json racine

JSON
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "build": "turbo build",
    "dev": "turbo dev",
    "lint": "turbo lint",
    "test": "turbo test",
    "clean": "turbo clean",
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  },
  "devDependencies": {
    "turbo": "^2.0.0",
    "prettier": "^3.0.0"
  },
  "packageManager": "pnpm@8.15.0"
}

turbo.json - Configuration principale

JSON
{
  "$schema": "https://turborepo.dev/schema.json",
  "globalDependencies": [".env"],
  "globalEnv": ["NODE_ENV", "CI"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"],
      "env": ["DATABASE_URL", "API_KEY"]
    },
    "lint": {
      "dependsOn": ["^lint"],
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"],
      "inputs": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts"]
    },
    "clean": {
      "cache": false
    }
  }
}

Pipelines et tâches

Comprendre le pipeline

Le pipeline définit les relations entre les tâches dans un monorepo :

JSON
{
  "tasks": {
    "build": {
      // ^ signifie "construis d'abord les dépendances"
      "dependsOn": ["^build"],
      // Fichiers de sortie à mettre en cache
      "outputs": ["dist/**", ".next/**"]
    },
    "test": {
      // Dépend du build local (sans ^)
      "dependsOn": ["build"],
      // Fichiers d'entrée qui influencent le cache
      "inputs": ["src/**", "test/**"]
    },
    "deploy": {
      // Dépend du build et du test du même paquet
      "dependsOn": ["build", "test"]
    }
  }
}

Types de dépendances

JSON
{
  "tasks": {
    "build": {
      // ^build - construit d'abord les dépendances (topologique)
      "dependsOn": ["^build"]
    },
    "test": {
      // build - construit CE paquet en premier (même paquet)
      "dependsOn": ["build"]
    },
    "deploy": {
      // Les deux types ensemble
      "dependsOn": ["^build", "test", "lint"]
    },
    "e2e": {
      // Dépendance envers un paquet spécifique
      "dependsOn": ["web#build"]
    }
  }
}

Sorties et mise en cache

JSON
{
  "tasks": {
    "build": {
      "outputs": [
        "dist/**",
        ".next/**",
        "!.next/cache/**",  // Exclut le cache
        "build/**"
      ]
    },
    "lint": {
      // Pas de sorties = la tâche ne produit pas de fichiers
      "outputs": []
    },
    "test": {
      "outputs": ["coverage/**"]
    }
  }
}

Inputs - contrôler l'invalidation du cache

JSON
{
  "tasks": {
    "build": {
      // Seuls ces fichiers influencent le cache
      "inputs": [
        "src/**/*.ts",
        "src/**/*.tsx",
        "package.json",
        "tsconfig.json"
      ]
    },
    "lint": {
      "inputs": [
        "src/**/*.ts",
        "src/**/*.tsx",
        ".eslintrc.js",
        "package.json"
      ]
    },
    "test": {
      "inputs": [
        "$TURBO_DEFAULT$",  // Inputs par défaut
        "jest.config.js",
        "test/**"
      ]
    }
  }
}

Variables d'environnement

JSON
{
  // Env globale - affecte toutes les tâches
  "globalEnv": ["CI", "NODE_ENV"],

  // Dépendances globales - fichiers qui affectent tout
  "globalDependencies": [".env", "tsconfig.base.json"],

  "tasks": {
    "build": {
      // Env spécifique à la tâche
      "env": ["DATABASE_URL", "API_KEY", "NEXT_PUBLIC_*"],
      "passThroughEnv": ["AWS_SECRET_KEY"]  // N'affecte pas le cache
    }
  }
}

Exécuter les tâches

Commandes de base

Bash
# Exécute la tâche dans tous les paquets
turbo build
turbo lint
turbo test

# Mode dev (sans cache, persistant)
turbo dev

# Sortie détaillée
turbo build --verbosity=2

# Dry run - montre ce qui sera exécuté
turbo build --dry-run

# Montre le graphe de dépendances
turbo build --graph
turbo build --graph=graph.svg

Filtrer les paquets

Bash
# Seulement un paquet spécifique
turbo build --filter=web
turbo build --filter=@repo/ui

# Le paquet et ses dépendances
turbo build --filter=web...

# Le paquet et ses dépendants (paquets qui en dépendent)
turbo build --filter=...@repo/ui

# Paquets dans un dossier spécifique
turbo build --filter="./apps/*"
turbo build --filter="./packages/*"

# Exclure des paquets
turbo build --filter="!docs"

# Combinaisons
turbo build --filter="web..." --filter="!docs"

# Seulement ce qui a changé depuis le dernier commit
turbo build --filter="[HEAD^1]"
turbo build --filter="...[main...HEAD]"

Filtres avancés

Bash
# Paquets modifiés dans la PR
turbo build --filter="[origin/main...HEAD]"

# Paquets qui dépendent du ui modifié
turbo build --filter="...@repo/ui[HEAD^1]"

# Toutes les apps
turbo build --filter="./apps/**"

# Paquets avec un tag spécifique dans package.json
turbo build --filter="@repo/*"

Options d'exécution

Bash
# Limite les tâches parallèles
turbo build --concurrency=4
turbo build --concurrency=50%  # 50% des cœurs CPU

# Continue malgré les erreurs
turbo build --continue

# Force la reconstruction (ignore le cache)
turbo build --force

# Logs de sortie
turbo build --output-logs=full
turbo build --output-logs=hash-only
turbo build --output-logs=new-only
turbo build --output-logs=errors-only

Cache distant

Configuration avec Vercel

Bash
# Connexion à Vercel
npx turbo login

# Lie ton projet
npx turbo link

# Maintenant le cache est partagé !
turbo build

Vérifier le cache distant

Bash
# Écrit un résumé de l'exécution dans un fichier JSON
turbo build --summarize

# À la fin de l'exécution, la console affiche :
#  Tâches :    7 réussies, 7 au total
# En cache :    5 en cache, 7 au total
#   Temps :    1.2s
# Résumé :    .turbo/runs/<id>.json

La répartition entre les succès locaux et distants n'apparaît que dans ce fichier : chaque tâche porte un champ cache avec local, remote et status, donc c'est là que tu confirmes qu'un résultat vient bien du stockage partagé.

Cache distant auto-hébergé

TypeScript
// Serveur de cache personnalisé (exemple avec Express)
import express from 'express'
import { createHash } from 'crypto'

const app = express()
const cache = new Map<string, Buffer>()

// GET de l'artefact
app.get('/v8/artifacts/:hash', (req, res) => {
  const artifact = cache.get(req.params.hash)
  if (artifact) {
    res.send(artifact)
  } else {
    res.status(404).send('Not found')
  }
})

// PUT de l'artefact
app.put('/v8/artifacts/:hash', express.raw({ limit: '50mb' }), (req, res) => {
  cache.set(req.params.hash, req.body)
  res.status(200).send('OK')
})

app.listen(3001)
Bash
# Utilise un serveur personnalisé
turbo build --api="http://localhost:3001" --token="secret"

Configuration CI/CD

.github/workflows/ci.yml
# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install

      - run: pnpm build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

Paquets de workspace

Paquet de composants UI

packages/ui/package.json
// packages/ui/package.json
{
  "name": "@repo/ui",
  "version": "0.0.0",
  "private": true,
  "exports": {
    ".": "./src/index.ts",
    "./button": "./src/button.tsx",
    "./card": "./src/card.tsx",
    "./styles.css": "./styles.css"
  },
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
    "lint": "eslint src/",
    "test": "vitest run"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  },
  "devDependencies": {
    "@repo/config-typescript": "workspace:*",
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}
TSpackages/ui/src/index.ts
// packages/ui/src/index.ts
export { Button } from './button'
export { Card } from './card'
export { Input } from './input'
export type { ButtonProps, CardProps, InputProps } from './types'

Paquets de configuration partagés

packages/config-eslint/package.json
// packages/config-eslint/package.json
{
  "name": "@repo/config-eslint",
  "version": "0.0.0",
  "private": true,
  "main": "index.js",
  "dependencies": {
    "@typescript-eslint/eslint-plugin": "^7.0.0",
    "@typescript-eslint/parser": "^7.0.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-react": "^7.0.0",
    "eslint-plugin-react-hooks": "^4.0.0"
  }
}
JSpackages/config-eslint/index.js
// packages/config-eslint/index.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'prettier'
  ],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'react'],
  rules: {
    'react/react-in-jsx-scope': 'off',
    '@typescript-eslint/no-unused-vars': 'warn'
  },
  settings: {
    react: { version: 'detect' }
  }
}

Utiliser les paquets partagés

apps/web/package.json
// apps/web/package.json
{
  "name": "web",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "lint": "eslint . --ext .ts,.tsx"
  },
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*",
    "next": "^14.0.0",
    "react": "^18.0.0"
  },
  "devDependencies": {
    "@repo/config-eslint": "workspace:*",
    "@repo/config-typescript": "workspace:*"
  }
}
JSapps/web/.eslintrc.js
// apps/web/.eslintrc.js
module.exports = {
  extends: ['@repo/config-eslint'],
  parserOptions: {
    project: './tsconfig.json'
  }
}

Partage de la configuration TypeScript

packages/config-typescript/base.json
// packages/config-typescript/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true
  }
}
packages/config-typescript/nextjs.json
// packages/config-typescript/nextjs.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "ES2022"],
    "module": "ESNext",
    "target": "ES2022",
    "jsx": "preserve",
    "plugins": [{ "name": "next" }]
  }
}
apps/web/tsconfig.json
// apps/web/tsconfig.json
{
  "extends": "@repo/config-typescript/nextjs.json",
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

Tâches spécifiques à un paquet

Surcharger les tâches par paquet

turbo.json
// turbo.json
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    // Surcharge pour un paquet spécifique
    "web#build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**"],
      "env": ["NEXT_PUBLIC_API_URL"]
    },
    // Surcharge pour docs
    "docs#build": {
      "dependsOn": ["^build"],
      "outputs": [".docusaurus/**", "build/**"]
    }
  }
}

turbo.json local dans un paquet

apps/web/turbo.json
// apps/web/turbo.json
{
  "$schema": "https://turborepo.dev/schema.json",
  "extends": ["//"],  // Hérite du turbo.json racine
  "tasks": {
    "build": {
      // Ne surcharge que des options spécifiques
      "env": ["ANALYZE", "NEXT_PUBLIC_SENTRY_DSN"]
    },
    "dev": {
      "persistent": true,
      "cache": false
    }
  }
}

Générateurs (Codegen)

Créer un générateur

Bash
# Crée le dossier des générateurs
mkdir -p turbo/generators
TSturbo/generators/config.ts
// turbo/generators/config.ts
import type { PlopTypes } from '@turbo/gen'

export default function generator(plop: PlopTypes.NodePlopAPI): void {
  // Générateur de nouveau paquet
  plop.setGenerator('package', {
    description: 'Crée un nouveau paquet',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: 'Nom du paquet :'
      },
      {
        type: 'list',
        name: 'type',
        message: 'Type de paquet :',
        choices: ['lib', 'config', 'tool']
      }
    ],
    actions: [
      {
        type: 'add',
        path: 'packages/{{name}}/package.json',
        templateFile: 'templates/package.json.hbs'
      },
      {
        type: 'add',
        path: 'packages/{{name}}/src/index.ts',
        templateFile: 'templates/index.ts.hbs'
      },
      {
        type: 'add',
        path: 'packages/{{name}}/tsconfig.json',
        templateFile: 'templates/tsconfig.json.hbs'
      }
    ]
  })

  // Générateur de nouvelle application
  plop.setGenerator('app', {
    description: 'Crée une nouvelle application',
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: "Nom de l'application :"
      },
      {
        type: 'list',
        name: 'framework',
        message: 'Framework :',
        choices: ['next', 'remix', 'astro']
      }
    ],
    actions: (data) => {
      const actions: PlopTypes.ActionType[] = []

      if (data?.framework === 'next') {
        actions.push({
          type: 'addMany',
          destination: 'apps/{{name}}',
          templateFiles: 'templates/next/**/*',
          base: 'templates/next'
        })
      }

      return actions
    }
  })
}

Utiliser le générateur

Bash
# Liste les générateurs disponibles
turbo gen

# Lance un générateur spécifique
turbo gen package
turbo gen app

# Avec les réponses aux questions du générateur
turbo gen package --args my-lib lib

Templates

HANDLEBARS
{{!-- turbo/generators/templates/package.json.hbs --}}
{
  "name": "@repo/{{name}}",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
    "lint": "eslint src/",
    "test": "vitest run"
  },
  "devDependencies": {
    "@repo/config-typescript": "workspace:*",
    "tsup": "^8.0.0",
    "typescript": "^5.0.0"
  }
}

Déploiements élagués

Docker avec un monorepo élagué

DOCKERFILE
# Dockerfile
FROM node:20-alpine AS base

# Étape de pruning
FROM base AS pruner
RUN npm install -g turbo
WORKDIR /app
COPY . .
RUN turbo prune web --docker

# Étape d'installation
FROM base AS installer
WORKDIR /app

# Installe seulement les dépendances nécessaires
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN corepack enable pnpm && pnpm install --frozen-lockfile

# Étape de build
FROM base AS builder
WORKDIR /app
COPY --from=installer /app/ .
COPY --from=pruner /app/out/full/ .

RUN corepack enable pnpm && pnpm turbo build --filter=web

# Étape d'exécution
FROM base AS runner
WORKDIR /app

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs

COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public

CMD ["node", "apps/web/server.js"]

Pruning pour la CI

Bash
# Crée un sous-ensemble élagué
turbo prune web --docker

# Résultat dans out/ :
# out/
# ├── json/           # fichiers package.json
# ├── full/           # Code source complet
# └── pnpm-lock.yaml  # Lockfile élagué

Diagnostiquer et résoudre les problèmes

Déboguer le cache

Bash
# Montre pourquoi une tâche a été exécutée
turbo build --summarize

# Montre le calcul du hash
turbo build --dry-run=json | jq

# Force un cache miss
turbo build --force

# Vide le cache local (.turbo/cache par défaut)
rm -rf .turbo/cache

Graphe des tâches

Bash
# Génère le graphe de dépendances
turbo build --graph=graph.html
turbo build --graph=graph.svg
turbo build --graph=graph.mermaid

# Affiche dans la console
turbo build --graph

Journalisation détaillée

Bash
# Niveaux de verbosité
turbo build --verbosity=0  # Silencieux
turbo build --verbosity=1  # Par défaut
turbo build --verbosity=2  # Détaillé

# Affiche tous les logs
turbo build --output-logs=full

# Seulement les erreurs
turbo build --output-logs=errors-only

Problèmes courants

Bash
# Problème : le cache ne fonctionne pas
# Solution : vérifie les inputs et les outputs

# Problème : la tâche s'exécute malgré l'absence de changements
# Vérifie les variables d'env dans le résumé écrit dans .turbo/runs
turbo build --summarize
jq '.tasks[].environmentVariables' .turbo/runs/*.json

# Problème : dépendance circulaire
turbo build --graph  # Visualise les dépendances

# Problème : builds lents
turbo build --profile=profile.json
# Analyse dans Chrome DevTools

Bonnes pratiques

Structure des paquets

packages/
├── core/            # Logique métier (pas d'UI)
├── ui/              # Composants React partagés
├── hooks/           # Hooks React partagés
├── utils/           # Aides, formateurs
├── types/           # Types TypeScript partagés
├── config-*/        # Configs partagées (eslint, ts, prettier)
└── api-client/      # Client API généré

Conventions de nommage

JSON
// Utilise un scope pour les paquets
{
  "name": "@repo/ui",        // ok
  "name": "@mycompany/ui",   // ok
  "name": "ui",              // mauvais, entre en conflit avec des paquets npm
}

Gestion des dépendances

package.json
// package.json racine - devDependencies partagées
{
  "devDependencies": {
    "turbo": "^2.0.0",
    "typescript": "^5.0.0",
    "prettier": "^3.0.0"
  }
}

// Paquet - seulement des deps spécifiques
{
  "dependencies": {
    "@repo/ui": "workspace:*"  // Dépendance interne
  },
  "devDependencies": {
    "@repo/config-eslint": "workspace:*"
  }
}

Organisation des tâches

JSON
{
  "tasks": {
    // Pipeline de build
    "build": { "dependsOn": ["^build"] },
    "build:production": { "dependsOn": ["^build", "lint", "test"] },

    // Dev
    "dev": { "cache": false, "persistent": true },

    // Qualité
    "lint": { "outputs": [] },
    "lint:fix": { "outputs": [], "cache": false },
    "typecheck": { "outputs": [] },
    "test": { "dependsOn": ["build"] },
    "test:watch": { "cache": false, "persistent": true },

    // Maintenance
    "clean": { "cache": false }
  }
}

Intégrations

Déploiement sur Vercel

vercel.json
// vercel.json
{
  "buildCommand": "pnpm turbo build --filter=web",
  "installCommand": "pnpm install",
  "framework": "nextjs",
  "outputDirectory": "apps/web/.next"
}

GitHub Actions

.github/workflows/ci.yml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2  # Nécessaire pour --filter=[HEAD^1]

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Installer les dépendances
        run: pnpm install

      - name: Build
        run: pnpm turbo build

      - name: Test
        run: pnpm turbo test

      - name: Lint
        run: pnpm turbo lint

Changesets (gestion de version)

Bash
npm install @changesets/cli -D
npx changeset init
.changeset/config.json
// .changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@repo/config-*"]
}

FAQ - questions fréquentes

Turborepo nécessite-t-il Vercel ?

Non ! Turborepo est open source et fonctionne sans Vercel. Le cache distant peut être auto-hébergé, ou tu peux utiliser Vercel comme option pratique.

Comment migrer depuis Lerna/Nx ?

Turborepo est additif : tu peux ajouter turbo.json à un monorepo existant sans retirer les autres outils. Déplace progressivement les tâches vers Turborepo.

Puis-je utiliser npm à la place de pnpm ?

Oui ! Turborepo fonctionne avec les workspaces npm, pnpm et yarn. pnpm est recommandé pour des raisons de performance.

Comment déboguer des problèmes de cache ?

Utilise turbo build --summarize pour voir ce qui influence le hash du cache. Vérifie les variables d'env, les inputs et les outputs dans turbo.json.

Turborepo prend-il en charge les monorepos multi-langages ?

Turborepo est optimisé pour JavaScript/TypeScript, mais peut orchestrer n'importe quelle tâche. Pour un monorepo polyglotte, envisage Nx ou Bazel.

Quelle est la différence entre dependsOn: ["^build"] et dependsOn: ["build"] ?

  • ^build - construit d'abord les dépendances (les paquets dont on dépend)
  • build - construit d'abord dans le même paquet

Comment optimiser le temps de build en CI ?

  1. Active le cache distant
  2. Utilise --filter pour les paquets modifiés
  3. Jobs parallèles en CI
  4. Optimise les inputs/outputs

Dois-je utiliser le cache distant de Vercel ?

Non. Le mécanisme est ouvert, et il existe des implémentations indépendantes de serveur de cache que tu peux héberger toi-même ou adosser à ton propre stockage d'objets. Le confort d'un service géré est réel, mais ce n'est pas une condition pour utiliser l'outil.

Le cache distant, et pourquoi il change le plus de choses

Un cache local accélère le travail d'une seule personne. Un cache partagé accélère toute l'équipe et le serveur de build, et c'est une différence qualitative plutôt que quantitative.

Le mécanisme est simple : le résultat d'une tâche part dans un stockage partagé, décrit par la même clé qu'en local. Quand quelqu'un d'autre, ou le serveur de build, exécute la même tâche sur la même entrée, il récupère le résultat déjà prêt plutôt que de le recalculer.

Le plus grand gain apparaît dans le pipeline d'intégration continue, par exemple autour des déploiements via Vercel, pour une raison facile à oublier. Un serveur de build démarre généralement d'un état propre, donc sans cache partagé, il reconstruit tout à chaque pull request, même quand un seul fichier d'un seul paquet a changé. Avec un cache partagé, il récupère des résultats produits plus tôt, y compris sur le laptop de quelqu'un.

Le second gain concerne une nouvelle personne qui rejoint l'équipe. Une première exécution après avoir cloné le dépôt peut prendre un quart d'heure, et avec un cache partagé, elle se réduit au téléchargement d'artefacts déjà prêts.

Deux choses méritent réflexion avant d'activer cela, cependant. La première, c'est la confiance : le cache contient des artefacts de build, donc quiconque peut y écrire peut injecter le résultat de quelqu'un d'autre dans ton build. Sur un dépôt public ou avec un accès large, restreins l'écriture au serveur de build et laisse le reste de l'équipe en lecture seule.

La seconde, c'est le coût du transfert. Avec de gros artefacts, télécharger un résultat peut être plus lent que de le construire, en particulier sur une connexion faible. Si les temps ne se sont pas améliorés après l'activation du cache distant, vérifie la taille de ce qui arrive dans le stockage avant de chercher ailleurs.

Le concurrent le plus proche est Nx, qui ajoute des générateurs de code, des plugins de framework et des migrations automatiques de configuration par-dessus le graphe de dépendances et le cache. Le prix à payer est double : la configuration commence à être écrite pour un outil bien précis, et le système de licences de la famille est incohérent. Le cœur est sous licence MIT, mais le paquet client du service cloud déclare proprietary dans le registre tout en embarquant un fichier de licence Creative Commons BY-ND, et les plugins de cache auto-hébergé portent une licence Commercial avec une plage de versions qui ne couvre pas la version actuelle de Nx.

Erreurs courantes

La première, ce sont les variables d'environnement non déclarées, décrites plus haut. C'est la plus dangereuse de la liste, car elle ne se manifeste pas par une panne, mais par le fait de rendre silencieusement un résultat construit pour un autre environnement.

La deuxième, c'est de traiter cet outil comme un remplacement du gestionnaire de paquets. Les workspaces sont définis par le gestionnaire de paquets, et l'outil se contente d'y lire un graphe de dépendances. Une configuration de workspace manquante signifie un graphe vide, et alors il n'y a rien à paralléliser, ce qui est la première chose à vérifier quand la parallélisation ne se produit pas.

La troisième, ce sont les tâches non déterministes. Si un build intègre un horodatage ou un identifiant aléatoire dans sa sortie, deux exécutions sur la même entrée produisent des sorties différentes. Le cache fonctionne toujours, mais il perd tout son sens, car les comparaisons d'artefacts ne veulent plus rien dire.

La quatrième, c'est de découper en paquets de façon trop fine. Un monorepo avec cinquante paquets de trois fichiers chacun passe plus de temps à gérer le graphe qu'à travailler. Découpe le long de frontières qui comptent vraiment pour le déploiement et la propriété du code.

La cinquième, c'est d'oublier qu'un cache ne remplace pas les tests, y compris ceux écrits avec Vitest. Une tâche sautée est une tâche que personne n'a exécutée, donc si un test détecte un problème qui dépend de quelque chose hors des inputs déclarés, il ne s'exécutera tout simplement pas. Savoir exactement ce qui entre dans la clé est la condition pour faire confiance au résultat.

La documentation se trouve sur le site de Turborepo, et le code source dans le dépôt du projet.

À lire aussi