Kurs Next.js · Moduł 9: Integracje i zaawansowane funkcje
Monorepo z Turborepo - Skalowanie projektów Next.js
W tej lekcji8
W rozległej infrastrukturze Metropolii Quantum 2150, gdzie dziesiątki mikroserwisów i aplikacji frontendowych współpracują ze sobą, potrzebujesz narzędzia do zarządzania kodem na wielką skalę. Turborepo to system budowania dla monorepo, który działa z Next.js jak precyzyjny kwantowy procesor - szybko, wydajnie i niezawodnie.
Dlaczego monorepo dla Next.js?
Monorepo to strategia przechowywania wielu projektów w jednym repozytorium. Dla ekosystemu Next.js daje to ogromne korzyści:
- Współdzielenie kodu - komponenty UI, konfiguracje, typy używane w wielu aplikacjach
- Atomowe zmiany - zmiana w współdzielonej bibliotece natychmiast widoczna wszędzie
- Spójny tooling - jedna konfiguracja ESLint, TypeScript, Prettier
- Uproszczone CI/CD - jeden pipeline dla całego ekosystemu
- Lepsza współpraca - wszystko w jednym miejscu, łatwy code review
Konfiguracja Turborepo
Inicjalizacja monorepo
1# Utwórz nowe monorepo z Turborepo
2npx create-turbo@latest quantum-monorepo
3
4# Lub dodaj Turborepo do istniejącego projektu
5npm install turbo --save-devStruktura katalogów
1quantum-monorepo/
2 apps/
3 web/ # Główna aplikacja Next.js
4 package.json
5 next.config.js
6 admin/ # Panel admina Next.js
7 package.json
8 next.config.js
9 docs/ # Dokumentacja Next.js
10 package.json
11
12 packages/
13 ui/ # Współdzielone komponenty React
14 package.json
15 src/
16 Button.tsx
17 Card.tsx
18 index.ts
19 config-eslint/ # Współdzielona konfiguracja ESLint
20 package.json
21 config-typescript/ # Współdzielona konfiguracja TypeScript
22 package.json
23 types/ # Współdzielone interfejsy TypeScript
24 package.json
25 src/
26 user.ts
27 product.ts
28
29 turbo.json # Konfiguracja Turborepo
30 package.json # Root package.jsonKonfiguracja 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}Kluczowe elementy konfiguracji:
dependsOn: ["^build"]- buduj zależności zanim zbudujesz pakietoutputs- co cachować (foldery wynikowe)cache: false- dev server nie powinien być cachowanypersistent: true- procesy długotrwałe (dev server)
Root package.json
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}Współdzielone pakiety
Pakiet UI - komponenty 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';Pakiet types - współdzielone interfejsy
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
Aby użyć współdzielonego pakietu w aplikacji 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}Użycie w kodzie aplikacji:
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">Dodaj do koszyka</Button>
10 </div>
11 );
12}Cache i pipeline Turborepo
Turborepo używa inteligentnego systemu cache, który zapamiętuje wyniki budowania i pomija niezmienione pakiety.
Jak działa cache
1# Pierwsze budowanie - wszystko od zera
2npx turbo build
3# Output: 5 tasks, 0 cached, 5 not cached
4# Czas: 45s
5
6# Drugie budowanie (bez zmian) - pełny cache
7npx turbo build
8# Output: 5 tasks, 5 cached, 0 not cached
9# Czas: 0.3sRemote Caching z Vercel
1# Połącz z Vercel Remote Cache
2npx turbo login
3npx turbo link
4
5# Teraz cache jest współdzielony między developerami i CIFiltrowanie budowania
1# Zbuduj tylko konkretną aplikację
2npx turbo build --filter=@quantum/web
3
4# Zbuduj aplikację i jej zależności
5npx turbo build --filter=@quantum/web...
6
7# Zbuduj tylko to co się zmieniło od ostatniego commita
8npx turbo build --filter=[HEAD^1]Deploying z monorepo na Vercel
Konfiguracja next.config.js dla 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;Opcja transpilePackages mówi Next.js, żeby transpilować kod z pakietów monorepo (domyślnie Next.js nie transpiluje node_modules).
Wieloaplikacyjny deploy
Vercel automatycznie buduje tylko zmienione aplikacje. Jeśli zmienisz packages/ui, aplikacje zależne od tego pakietu zostaną przebudowane, a te niezależne - pominięte.
Najlepsze praktyki
1. Konwencje nazewnictwa pakietów
Używaj scope (@nazwa-organizacji/) dla wszystkich pakietów i aplikacji:
1@quantum/ui
2@quantum/types
3@quantum/config-eslint
4@quantum/web
5@quantum/admin2. Wersjonowanie
Używaj workspace:* dla wewnętrznych zależności - nigdy nie hardcoduj wersji:
1"@quantum/ui": "workspace:*"3. Zarządzanie zmiennymi środowiskowymi
Każda aplikacja ma własny plik .env, a turbo.json deklaruje globalne zależności od plików środowiskowych.
Podsumowanie
Turborepo to potężne narzędzie do skalowania projektów Next.js w architekturze monorepo:
- Współdzielone pakiety - UI, typy, konfiguracje używane w wielu aplikacjach
- Inteligentny cache - pomijanie niezmienionych pakietów, przyspieszenie buildów
- Pipeline - zależności między zadaniami, równoległe budowanie
- Workspace dependencies -
workspace:*dla wewnętrznych pakietów - Remote caching - współdzielony cache między developerami i CI
- Deploy na Vercel - natywne wsparcie, automatyczne wykrywanie zmian
W Metropolii Quantum 2150, gdzie każdy projekt rośnie i wymaga skalowania, Turborepo jest fundamentem architektury - pozwala zarządzać dziesiątkami aplikacji i bibliotek z jednego miejsca.
Kod do tej lekcji: 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 packages first (no deps first)
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 // Then packages with deps
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 // Then 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 complete!');
95 setRunning(false);
96 setChangedPkg(null);
97 };
98
99 const simulateChange = (pkgName: string) => {
100 setChangedPkg(pkgName);
101 addLog('Changed: ' + 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' }}>Shared packages + Intelligent caching + 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 ? 'Building...' : '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 Reset
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' }}>Remote Cache</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' }}>Simulate change in:</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' }}>Workspace Packages</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' }}>Dependency Graph</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 (no internal deps)</div>
167 </div>
168 </div>
169 </div>
170
171 <div>
172 <h3 style={{ color: '#7c4dff', marginBottom: '10px' }}>Build Log</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' }}>Click "turbo build" to start...</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;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Które stwierdzenie najlepiej opisuje główną zaletę Turborepo w projektach Next.js?
Zadania praktyczne w grze
- Układanie w pionie
Ułóż kolejność budowania pakietów w monorepo Turborepo (od pakietów bez zależności do aplikacji)