Kurs Next.js · Moduł 9: Integracje i zaawansowane funkcje

Monorepo z Turborepo - Skalowanie projektów Next.js

5 min czytania
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-dev

Struktura 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.json

Konfiguracja 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 pakiet
  • outputs - co cachować (foldery wynikowe)
  • cache: false - dev server nie powinien być cachowany
  • persistent: 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.3s

Remote 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 CI

Filtrowanie 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/admin

2. 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:

  1. Współdzielone pakiety - UI, typy, konfiguracje używane w wielu aplikacjach
  2. Inteligentny cache - pomijanie niezmienionych pakietów, przyspieszenie buildów
  3. Pipeline - zależności między zadaniami, równoległe budowanie
  4. Workspace dependencies - workspace:* dla wewnętrznych pakietów
  5. Remote caching - współdzielony cache między developerami i CI
  6. 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. 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)

Przydatne artykuły