Cours Next.js · Module 9: Intégrations et fonctions avancées
Monorepo avec Turborepo - Mettre à l'échelle les projets Next.js
Dans cette leçon8
Dans la vaste infrastructure de la Métropole Quantum 2150, où des dizaines de microservices et d'applications frontend collaborent entre eux, tu as besoin d'un outil pour gérer le code à grande échelle. Turborepo est un système de build pour monorepo qui fonctionne avec Next.js comme un processeur quantique précis - rapidement, efficacement et de manière fiable.
Pourquoi un monorepo pour Next.js ?
Un monorepo est une stratégie consistant à stocker plusieurs projets dans un seul dépôt. Pour l'écosystème Next.js, cela apporte d'énormes avantages :
- Partage de code - composants UI, configurations, types utilisés dans plusieurs applications
- Changements atomiques - une modification dans une bibliothèque partagée est immédiatement visible partout
- Outillage cohérent - une seule configuration ESLint, TypeScript, Prettier
- CI/CD simplifié - un seul pipeline pour tout l'écosystème
- Meilleure collaboration - tout au même endroit, revue de code facilitée
Configuration de Turborepo
Initialisation du monorepo
1# Crée un nouveau monorepo avec Turborepo
2npx create-turbo@latest quantum-monorepo
3
4# Ou ajoute Turborepo à un projet existant
5npm install turbo --save-devStructure des dossiers
1quantum-monorepo/
2 apps/
3 web/ # Application Next.js principale
4 package.json
5 next.config.js
6 admin/ # Panneau d'administration Next.js
7 package.json
8 next.config.js
9 docs/ # Documentation Next.js
10 package.json
11
12 packages/
13 ui/ # Composants React partagés
14 package.json
15 src/
16 Button.tsx
17 Card.tsx
18 index.ts
19 config-eslint/ # Configuration ESLint partagée
20 package.json
21 config-typescript/ # Configuration TypeScript partagée
22 package.json
23 types/ # Interfaces TypeScript partagées
24 package.json
25 src/
26 user.ts
27 product.ts
28
29 turbo.json # Configuration de Turborepo
30 package.json # package.json racineConfiguration de turbo.json
1{
2 "$schema": "https://turbo.build/schema.json",
3 "globalDependencies": ["**/.env.*local"],
4 "pipeline": {
5 "build": {
6 "dependsOn": ["^build"],
7 "outputs": [".next/**", "!.next/cache/**", "dist/**"]
8 },
9 "lint": {
10 "dependsOn": ["^build"]
11 },
12 "dev": {
13 "cache": false,
14 "persistent": true
15 },
16 "test": {
17 "dependsOn": ["^build"]
18 },
19 "type-check": {
20 "dependsOn": ["^build"]
21 }
22 }
23}Éléments clés de la configuration :
dependsOn: ["^build"]- construis les dépendances avant de construire le packageoutputs- ce qu'il faut mettre en cache (dossiers de sortie)cache: false- le serveur de dev ne doit pas être mis en cachepersistent: true- processus de longue durée (serveur de dev)
package.json racine
1{
2 "name": "quantum-monorepo",
3 "private": true,
4 "workspaces": ["apps/*", "packages/*"],
5 "scripts": {
6 "build": "turbo build",
7 "dev": "turbo dev",
8 "lint": "turbo lint",
9 "test": "turbo test",
10 "type-check": "turbo type-check"
11 },
12 "devDependencies": {
13 "turbo": "^2.0.0"
14 }
15}Packages partagés
Package UI - composants React
1// packages/ui/package.json
2{
3 "name": "@quantum/ui",
4 "version": "0.1.0",
5 "main": "./src/index.ts",
6 "types": "./src/index.ts",
7 "dependencies": {
8 "react": "^19.2.0"
9 },
10 "devDependencies": {
11 "@quantum/config-typescript": "workspace:*",
12 "typescript": "^5.5.0"
13 }
14}1// packages/ui/src/Button.tsx
2import React from 'react';
3
4interface ButtonProps {
5 children: React.ReactNode;
6 variant?: 'primary' | 'secondary' | 'danger';
7 onClick?: () => void;
8 disabled?: boolean;
9}
10
11export function Button({ children, variant = 'primary', onClick, disabled }: ButtonProps) {
12 const styles = {
13 primary: { background: '#7c4dff', color: '#fff' },
14 secondary: { background: '#64ffda', color: '#000' },
15 danger: { background: '#f44336', color: '#fff' },
16 };
17
18 return (
19 <button
20 onClick={onClick}
21 disabled={disabled}
22 style={{
23 ...styles[variant],
24 padding: '10px 24px',
25 borderRadius: '8px',
26 border: 'none',
27 cursor: disabled ? 'not-allowed' : 'pointer',
28 opacity: disabled ? 0.5 : 1,
29 }}
30 >
31 {children}
32 </button>
33 );
34}1// packages/ui/src/index.ts
2export { Button } from './Button';
3export { Card } from './Card';Package types - interfaces partagées
1// packages/types/src/user.ts
2export interface User {
3 id: string;
4 email: string;
5 name: string;
6 role: 'admin' | 'user' | 'moderator';
7 createdAt: string;
8}1// packages/types/src/index.ts
2export * from './user';
3export * from './product';Workspace dependencies
Pour utiliser un package partagé dans une application Next.js :
1// apps/web/package.json
2{
3 "name": "@quantum/web",
4 "dependencies": {
5 "@quantum/ui": "workspace:*",
6 "@quantum/types": "workspace:*",
7 "next": "^16.0.0",
8 "react": "^19.2.0",
9 "react-dom": "^19.2.0"
10 },
11 "devDependencies": {
12 "@quantum/config-typescript": "workspace:*",
13 "@quantum/config-eslint": "workspace:*"
14 }
15}Utilisation dans le code de l'application :
1// apps/web/app/page.tsx
2import { Button, Card } from '@quantum/ui';
3import type { Product } from '@quantum/types';
4
5export default async function HomePage() {
6 return (
7 <div>
8 <h1>Quantum Store</h1>
9 <Button variant="primary">Ajouter au panier</Button>
10 </div>
11 );
12}Cache et pipeline de Turborepo
Turborepo utilise un système de cache intelligent qui mémorise les résultats de build et ignore les packages inchangés.
Comment fonctionne le cache
1# Premier build - tout à partir de zéro
2npx turbo build
3# Output: 5 tasks, 0 cached, 5 not cached
4# Temps : 45s
5
6# Deuxième build (sans changement) - cache complet
7npx turbo build
8# Output: 5 tasks, 5 cached, 0 not cached
9# Temps : 0,3sRemote Caching avec Vercel
1# Connecte-toi au Remote Cache de Vercel
2npx turbo login
3npx turbo link
4
5# Maintenant, le cache est partagé entre les développeurs et la CIFiltrer le build
1# Construis uniquement une application spécifique
2npx turbo build --filter=@quantum/web
3
4# Construis l'application et ses dépendances
5npx turbo build --filter=@quantum/web...
6
7# Construis uniquement ce qui a changé depuis le dernier commit
8npx turbo build --filter=[HEAD^1]Déployer un monorepo sur Vercel
Configuration de next.config.js pour un monorepo
1// apps/web/next.config.js
2/** @type {import('next').NextConfig} */
3const nextConfig = {
4 transpilePackages: ['@quantum/ui'],
5 output: 'standalone',
6};
7
8module.exports = nextConfig;L'option transpilePackages indique à Next.js de transpiler le code des packages du monorepo (par défaut, Next.js ne transpile pas node_modules).
Déploiement multi-applications
Vercel ne construit automatiquement que les applications modifiées. Si tu modifies packages/ui, les applications qui dépendent de ce package seront reconstruites, tandis que les autres seront ignorées.
Bonnes pratiques
1. Conventions de nommage des packages
Utilise un scope (@nom-organisation/) pour tous les packages et applications :
1@quantum/ui
2@quantum/types
3@quantum/config-eslint
4@quantum/web
5@quantum/admin2. Versioning
Utilise workspace:* pour les dépendances internes - ne code jamais les versions en dur :
1"@quantum/ui": "workspace:*"3. Gestion des variables d'environnement
Chaque application a son propre fichier .env, et turbo.json déclare les dépendances globales vis-à-vis des fichiers d'environnement.
Résumé
Turborepo est un outil puissant pour mettre à l'échelle les projets Next.js dans une architecture monorepo :
- Packages partagés - UI, types, configurations utilisés dans plusieurs applications
- Cache intelligent - ignore les packages inchangés, accélère les builds
- Pipeline - dépendances entre les tâches, build en parallèle
- Dépendances de workspace -
workspace:*pour les packages internes - Remote caching - cache partagé entre les développeurs et la CI
- Déploiement sur Vercel - support natif, détection automatique des changements
Dans la Métropole Quantum 2150, où chaque projet grandit et doit être mis à l'échelle, Turborepo est le fondement de l'architecture - il permet de gérer des dizaines d'applications et de bibliothèques depuis un seul endroit.
Code de cette leçon : App.tsx
1import React, { useState } from 'react';
2
3interface Package {
4 name: string;
5 type: 'app' | 'package';
6 status: 'cached' | 'building' | 'built' | 'pending';
7 duration?: number;
8 deps: string[];
9}
10
11const TurborepoDemo = () => {
12 const [packages, setPackages] = useState<Package[]>([
13 { name: '@quantum/web', type: 'app', status: 'pending', deps: ['@quantum/ui', '@quantum/types'] },
14 { name: '@quantum/admin', type: 'app', status: 'pending', deps: ['@quantum/ui', '@quantum/types'] },
15 { name: '@quantum/docs', type: 'app', status: 'pending', deps: [] },
16 { name: '@quantum/ui', type: 'package', status: 'pending', deps: ['@quantum/types'] },
17 { name: '@quantum/types', type: 'package', status: 'pending', deps: [] },
18 { name: '@quantum/config-ts', type: 'package', status: 'pending', deps: [] },
19 { name: '@quantum/config-eslint', type: 'package', status: 'pending', deps: [] },
20 ]);
21 const [running, setRunning] = useState(false);
22 const [logs, setLogs] = useState<string[]>([]);
23 const [cacheEnabled, setCacheEnabled] = useState(true);
24 const [buildCount, setBuildCount] = useState(0);
25 const [changedPkg, setChangedPkg] = useState<string | null>(null);
26
27 const addLog = (msg: string) => setLogs(prev => [...prev.slice(-12), msg]);
28 const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
29
30 const statusIcon = (s: string) => {
31 switch (s) {
32 case 'built': case 'cached': return '✅';
33 case 'building': return '⏳';
34 default: return '⚪';
35 }
36 };
37
38 const statusColor = (s: string) => {
39 switch (s) {
40 case 'built': return '#4caf50';
41 case 'cached': return '#64ffda';
42 case 'building': return '#ff9800';
43 default: return '#555';
44 }
45 };
46
47 const runBuild = async () => {
48 setRunning(true);
49 setLogs([]);
50 setPackages(prev => prev.map(p => ({ ...p, status: 'pending', duration: undefined })));
51 setBuildCount(c => c + 1);
52 const isSecondBuild = buildCount > 0;
53
54 addLog('$ npx turbo build' + (changedPkg ? ' --filter=' + changedPkg + '...' : ''));
55
56 // Build d'abord les packages (ceux sans dépendances en premier)
57 const noDeps = packages.filter(p => p.type === 'package' && p.deps.length === 0);
58 for (const pkg of noDeps) {
59 const isCached = isSecondBuild && cacheEnabled && pkg.name !== changedPkg;
60 setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'building' } : p));
61 addLog((isCached ? 'CACHE HIT ' : 'Building ') + pkg.name);
62 await sleep(isCached ? 100 : 300);
63 setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: isCached ? 'cached' : 'built', duration: isCached ? 0 : 2 } : p));
64 }
65
66 // Puis les packages avec dépendances
67 const withDeps = packages.filter(p => p.type === 'package' && p.deps.length > 0);
68 for (const pkg of withDeps) {
69 const isCached = isSecondBuild && cacheEnabled && pkg.name !== changedPkg && !pkg.deps.includes(changedPkg || '');
70 setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'building' } : p));
71 addLog((isCached ? 'CACHE HIT ' : 'Building ') + pkg.name);
72 await sleep(isCached ? 100 : 400);
73 setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: isCached ? 'cached' : 'built', duration: isCached ? 0 : 5 } : p));
74 }
75
76 // Puis les apps
77 const apps = packages.filter(p => p.type === 'app');
78 for (const pkg of apps) {
79 const affectedByChange = changedPkg && (pkg.deps.includes(changedPkg) || pkg.name === changedPkg);
80 const isCached = isSecondBuild && cacheEnabled && !affectedByChange;
81 if (changedPkg && !affectedByChange && pkg.name !== changedPkg) {
82 setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'cached', duration: 0 } : p));
83 addLog('SKIP ' + pkg.name + ' (not affected)');
84 await sleep(80);
85 continue;
86 }
87 setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: 'building' } : p));
88 addLog((isCached ? 'CACHE HIT ' : 'Building ') + pkg.name);
89 await sleep(isCached ? 150 : 600);
90 setPackages(prev => prev.map(p => p.name === pkg.name ? { ...p, status: isCached ? 'cached' : 'built', duration: isCached ? 0 : 15 } : p));
91 }
92
93 const cached = packages.filter(() => true).length; // placeholder
94 addLog('Build terminé !');
95 setRunning(false);
96 setChangedPkg(null);
97 };
98
99 const simulateChange = (pkgName: string) => {
100 setChangedPkg(pkgName);
101 addLog('Modifié : ' + pkgName);
102 };
103
104 const reset = () => {
105 setPackages(prev => prev.map(p => ({ ...p, status: 'pending', duration: undefined })));
106 setLogs([]);
107 setBuildCount(0);
108 setChangedPkg(null);
109 };
110
111 return (
112 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff', fontFamily: 'sans-serif' }}>
113 <h1 style={{ color: '#64ffda', textAlign: 'center' }}>Monorepo z Turborepo</h1>
114 <p style={{ textAlign: 'center', color: '#888', marginBottom: '20px' }}>Packages partagés + Cache intelligent + Pipeline</p>
115
116 <div style={{ maxWidth: '900px', margin: '0 auto' }}>
117 <div style={{ display: 'flex', gap: '8px', justifyContent: 'center', marginBottom: '16px', flexWrap: 'wrap' }}>
118 <button onClick={runBuild} disabled={running}
119 style={{ padding: '8px 20px', background: running ? '#555' : '#4caf50', border: 'none', borderRadius: '6px', color: '#fff', cursor: running ? 'default' : 'pointer', fontWeight: 'bold' }}>
120 {running ? 'Build en cours...' : 'turbo build'}
121 </button>
122 <button onClick={reset} disabled={running}
123 style={{ padding: '8px 20px', background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer' }}>
124 Réinitialiser
125 </button>
126 <label style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
127 <input type="checkbox" checked={cacheEnabled} onChange={() => setCacheEnabled(!cacheEnabled)} />
128 <span style={{ color: cacheEnabled ? '#64ffda' : '#666', fontSize: '13px' }}>Cache distant</span>
129 </label>
130 </div>
131
132 {buildCount > 0 && !running && (
133 <div style={{ display: 'flex', gap: '6px', justifyContent: 'center', marginBottom: '12px', flexWrap: 'wrap' }}>
134 <span style={{ color: '#888', fontSize: '12px', padding: '4px 0' }}>Simuler un changement dans :</span>
135 {packages.filter(p => p.type === 'package').map(p => (
136 <button key={p.name} onClick={() => simulateChange(p.name)}
137 style={{ padding: '4px 10px', background: changedPkg === p.name ? '#ff9800' : 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '4px', color: '#fff', cursor: 'pointer', fontSize: '11px' }}>
138 {p.name.replace('@quantum/', '')}
139 </button>
140 ))}
141 </div>
142 )}
143
144 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
145 <div>
146 <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>Packages du workspace</h3>
147 <div style={{ background: 'rgba(255,255,255,0.05)', borderRadius: '10px', overflow: 'hidden' }}>
148 {packages.map(pkg => (
149 <div key={pkg.name} style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '10px 12px', borderBottom: '1px solid #1a1a2e' }}>
150 <span>{statusIcon(pkg.status)}</span>
151 <span style={{ background: pkg.type === 'app' ? '#7c4dff33' : '#64ffda33', color: pkg.type === 'app' ? '#7c4dff' : '#64ffda', padding: '2px 6px', borderRadius: '4px', fontSize: '10px' }}>
152 {pkg.type}
153 </span>
154 <span style={{ flex: 1, color: statusColor(pkg.status), fontSize: '13px' }}>{pkg.name}</span>
155 {pkg.status === 'cached' && <span style={{ color: '#64ffda', fontSize: '10px' }}>CACHED</span>}
156 {pkg.duration !== undefined && pkg.duration > 0 && <span style={{ color: '#555', fontSize: '11px' }}>{pkg.duration}s</span>}
157 </div>
158 ))}
159 </div>
160
161 <div style={{ marginTop: '12px', background: 'rgba(255,255,255,0.05)', padding: '12px', borderRadius: '8px' }}>
162 <h4 style={{ color: '#64ffda', fontSize: '12px', marginBottom: '8px' }}>Graphe de dépendances</h4>
163 <div style={{ fontSize: '11px', fontFamily: 'monospace', color: '#888' }}>
164 <div>@quantum/web ── @quantum/ui ── @quantum/types</div>
165 <div>@quantum/admin ── @quantum/ui ── @quantum/types</div>
166 <div>@quantum/docs (aucune dépendance interne)</div>
167 </div>
168 </div>
169 </div>
170
171 <div>
172 <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>Journal de build</h3>
173 <div style={{ background: '#000', padding: '12px', borderRadius: '8px', minHeight: '300px', maxHeight: '400px', overflowY: 'auto', fontFamily: 'monospace', fontSize: '11px' }}>
174 {logs.length === 0 ? (
175 <span style={{ color: '#555' }}>Clique sur « turbo build » pour démarrer...</span>
176 ) : logs.map((log, i) => (
177 <div key={i} style={{ color: log.includes('CACHE') ? '#64ffda' : log.includes('SKIP') ? '#888' : log.includes('Building') ? '#ff9800' : '#0f0', padding: '2px 0' }}>
178 {log}
179 </div>
180 ))}
181 </div>
182 </div>
183 </div>
184 </div>
185 </div>
186 );
187};
188
189export default TurborepoDemo;Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle affirmation décrit le mieux le principal avantage de Turborepo dans les projets Next.js ?
Tâches pratiques dans le jeu
- Tri vertical
Remets dans l'ordre la construction des packages dans un monorepo Turborepo (des packages sans dépendances jusqu'aux applications)