Caching und Build-Pipelines für mehrere Apps in einem Repository
Mehrere Vue-Anwendungen und geteilte Pakete in einem Repository zu verwalten, wird ohne Task-Orchestrierung schnell langsam und unübersichtlich. Turborepo bringt inkrementelles Caching, parallele Task-Ausführung und eine deklarative Pipeline-Definition, die Build-Zeiten in Vue-Monorepos von Minuten auf Sekunden reduziert.
Inhaltsverzeichnis
- 1. Warum Vue-Projekte überhaupt ins Monorepo wandern
- 2. Workspace-Struktur mit pnpm und Turborepo
- 3. Task-Pipelines deklarativ definieren
- 4. Inkrementelles Caching verstehen und nutzen
- 5. Remote-Caching für das ganze Team
- 6. Geteilte Vue-Komponenten und Konfiguration
- 7. CI-Pipelines mit betroffenen Paketen (Affected)
- 8. Typische Fallstricke bei Vue-Monorepos
- 9. Turborepo im Werkzeugvergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Vue-Projekte überhaupt ins Monorepo wandern
Ein Monorepo für Vue-Projekte bündelt mehrere Anwendungen und geteilte Pakete, etwa ein Design System oder gemeinsame TypeScript-Typen, in einem einzigen Repository, statt sie über mehrere separate Git-Repositories zu verteilen. Der unmittelbare Vorteil: Eine Änderung an einer geteilten Vue-Komponente wird in derselben Pull Request sichtbar, die auch die konsumierenden Anwendungen anpasst, statt über mehrere Repositories mit separaten Versionsbumps synchronisiert werden zu müssen.
Ohne geeignetes Tooling wird ein Vue-Monorepo jedoch schnell zum Performance-Problem: Ein einfacher npm run build im Root-Verzeichnis baut standardmäßig alle Pakete neu, selbst wenn nur eine einzige Datei in einer einzigen Vue-Anwendung geändert wurde. Bei zehn oder mehr Paketen summiert sich das zu Build-Zeiten, die jeden Entwicklungsvorteil des Monorepos wieder zunichtemachen. Genau hier setzt Turborepo an: Es orchestriert Tasks über Paketgrenzen hinweg und baut nur das, was sich tatsächlich geändert hat.
Der Umstieg auf ein Monorepo mit Turborepo lohnt sich für Vue-Teams besonders dann, wenn mehrere Anwendungen dieselben internen Pakete nutzen, etwa ein Shop-Frontend und ein Admin-Tool, die beide auf demselben Design-System und denselben API-Client-Typen aufbauen. Für ein einzelnes Vue-Projekt ohne geteilte Pakete lohnt sich der zusätzliche Tooling-Aufwand dagegen selten.
2. Workspace-Struktur mit pnpm und Turborepo
Die gängige Struktur für ein Vue-Monorepo mit Turborepo trennt Anwendungen und geteilte Pakete in zwei Top-Level-Ordner, üblicherweise apps/ und packages/. Jede Vue-Anwendung in apps/ hat ihr eigenes package.json mit eigenen Abhängigkeiten, während wiederverwendbare Bausteine wie das Design System, ein API-Client oder geteilte ESLint-Konfiguration in packages/ liegen und von den Anwendungen als interne Workspace-Abhängigkeiten referenziert werden.
pnpm ist der verbreitetste Paketmanager für Turborepo-Monorepos, weil sein Workspace-Protokoll (workspace:*) interne Pakete per Symlink verknüpft, statt sie über die Registry aufzulösen. Das bedeutet: Eine Änderung an einer geteilten Vue-Komponente ist sofort in der konsumierenden Anwendung sichtbar, ohne einen Publish-Schritt zwischen den Paketen.
// pnpm-workspace.yaml — Defines which folders are part of the monorepo
// packages:
// - "apps/*"
// - "packages/*"
// apps/shop/package.json — Consuming an internal package via workspace protocol
{
"name": "@mironsoft/shop",
"dependencies": {
"@mironsoft/design-system": "workspace:*",
"@mironsoft/api-client": "workspace:*",
"vue": "^3.4.0"
}
}
// packages/design-system/package.json — Internal package, never published externally
{
"name": "@mironsoft/design-system",
"main": "./dist/index.js",
"peerDependencies": { "vue": "^3.4.0" }
}
3. Task-Pipelines deklarativ definieren
Das Herzstück von Turborepo ist die Datei turbo.json, in der definiert wird, wie einzelne Tasks, etwa build, test oder lint, voneinander abhängen. Die Syntax "dependsOn": ["^build"] bedeutet, dass der build-Task eines Pakets erst startet, nachdem der build-Task aller Pakete abgeschlossen ist, von denen es abhängt. Für ein Vue-Monorepo bedeutet das konkret: Die Shop-Anwendung wartet automatisch darauf, dass das Design-System-Paket fertig gebaut ist, bevor ihr eigener Build startet.
Turborepo leitet diesen Abhängigkeitsgraphen automatisch aus den package.json-Dependencies ab, ohne dass er manuell gepflegt werden muss. Das ist ein erheblicher Vorteil gegenüber selbst gebauten Skript-Lösungen, bei denen die Build-Reihenfolge häufig hart kodiert und bei jedem neuen Paket manuell nachgepflegt werden muss.
// turbo.json — Declarative pipeline definition for a Vue monorepo
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".output/**"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"lint": {
"dependsOn": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
4. Inkrementelles Caching verstehen und nutzen
Der eigentliche Performance-Gewinn von Turborepo in Vue-Monorepos entsteht durch inkrementelles Caching. Turborepo berechnet einen Hash aus allen Eingaben eines Tasks, dazu zählen der Quellcode des Pakets, seine Abhängigkeiten und die relevanten Umgebungsvariablen. Ändert sich keine dieser Eingaben zwischen zwei Läufen, liest Turborepo das Ergebnis, etwa den dist-Ordner einer Vue-Anwendung, direkt aus dem Cache, statt den Build erneut auszuführen.
Für Vue-Projekte ist besonders wichtig, im outputs-Feld jeder Task-Definition exakt anzugeben, welche Verzeichnisse als Build-Ergebnis gelten, üblicherweise dist/** bei Vite-Builds. Fehlt diese Angabe, cached Turborepo zwar die Ausführung, kann das Ergebnis aber nicht wiederherstellen, was bei einem Cache-Hit zu einem fehlenden dist-Ordner führt. Ein zweiter wichtiger Punkt: Umgebungsvariablen, die das Build-Ergebnis beeinflussen, etwa die API-Basis-URL, müssen explizit in der env-Konfiguration gelistet werden, sonst invalidiert eine geänderte Variable den Cache nicht und ein veralteter Build wird ausgeliefert.
5. Remote-Caching für das ganze Team
Lokales Caching beschleunigt nur wiederholte Builds auf derselben Maschine. Remote-Caching in Turborepo teilt die Cache-Ergebnisse über Vercel Remote Cache oder einen selbst gehosteten Cache-Server mit dem gesamten Team und der CI-Pipeline. Baut ein Entwickler morgens eine Vue-Anwendung, die ein Kollege am Vortag bereits unverändert gebaut hat, lädt Turborepo das Ergebnis aus dem Remote-Cache, statt lokal neu zu bauen, selbst wenn der lokale Cache leer ist.
Der größte Hebel entsteht in der CI-Pipeline: Ein CI-Runner startet typischerweise mit leerem lokalen Cache, kann aber über Remote-Caching auf Build-Ergebnisse zugreifen, die bereits von einem vorherigen CI-Lauf oder einem lokalen Entwickler-Build erzeugt wurden. Für ein Vue-Monorepo mit mehreren Anwendungen kann das die CI-Laufzeit von zehn Minuten auf unter eine Minute reduzieren, wenn nur ein kleiner Teil der Pakete tatsächlich geändert wurde.
#!/usr/bin/env bash
# .github/workflows/ci.yml (excerpt) — Enabling Turborepo remote caching in CI
set -euo pipefail
export TURBO_TOKEN="${TURBO_TOKEN}"
export TURBO_TEAM="${TURBO_TEAM}"
# Turborepo automatically checks the remote cache before running any task
pnpm turbo run build test lint --cache-dir=.turbo
# Only re-runs tasks whose input hash changed since the last cached execution
echo "[OK] CI run complete — cache hits skip redundant Vue app rebuilds"
6. Geteilte Vue-Komponenten und Konfiguration
Ein häufiges Muster in Vue-Monorepos mit Turborepo ist ein eigenes packages/ui-Paket für geteilte Vue-Komponenten, ein packages/config-Paket für geteilte ESLint- und TypeScript-Konfiguration, und ein packages/tsconfig-Paket, das als Basis-Konfiguration von allen Anwendungen erweitert wird. Diese Struktur verhindert, dass jede Vue-Anwendung ihre eigene, leicht abweichende Lint- und TypeScript-Konfiguration pflegt.
Wichtig ist, dass geteilte Pakete selbst auch am Turborepo-Caching teilnehmen. Ein Änderung an der Basis-TypeScript-Konfiguration invalidiert automatisch den Cache aller Anwendungen, die davon abhängen, weil Turborepo den Abhängigkeitsgraphen korrekt aus den Workspace-Referenzen ableitet. Ohne diese automatische Invalidierung würde eine Konfigurationsänderung sonst unbemerkt einen veralteten, gecachten Build ausliefern.
// packages/tsconfig/vue-app.json — Base config extended by every Vue app
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"jsx": "preserve",
"types": ["vite/client"]
}
}
// apps/shop/tsconfig.json — Each app extends the shared base, never duplicates it
{
"extends": "@mironsoft/tsconfig/vue-app.json",
"compilerOptions": { "outDir": "dist" },
"include": ["src"]
}
7. CI-Pipelines mit betroffenen Paketen (Affected)
Neben dem Task-Level-Caching bietet Turborepo mit --filter die Möglichkeit, CI-Läufe auf tatsächlich betroffene Pakete zu beschränken. Ein git diff gegen den letzten deployten Commit zeigt, welche Dateien sich geändert haben, und turbo run build --filter=...[HEAD^1] baut nur die Vue-Anwendungen und Pakete, die von diesen Änderungen tatsächlich betroffen sind, plus alle davon abhängigen Pakete.
Für ein Vue-Monorepo mit fünf Anwendungen bedeutet das: Eine Pull Request, die nur den Checkout betrifft, löst keinen Build der anderen vier Anwendungen aus, selbst ohne Caching. Kombiniert mit Remote-Caching für die tatsächlich betroffenen Pakete entsteht eine CI-Pipeline, deren Laufzeit proportional zum Umfang der Änderung ist, nicht zur Gesamtgröße des Monorepos.
8. Typische Fallstricke bei Vue-Monorepos
Der häufigste Fehler in Vue-Monorepos mit Turborepo ist ein unvollständiges outputs-Feld, das dazu führt, dass Turborepo einen Cache-Hit meldet, aber die tatsächlichen Build-Artefakte fehlen. Ein zweiter verbreiteter Fehler ist, umgebungsabhängige Werte wie API-Endpunkte nicht in der env-Konfiguration zu deklarieren, wodurch ein Staging-Build fälschlich als identisch zu einem Produktions-Build erkannt und aus dem Cache bedient wird.
Ein dritter, subtilerer Fallstrick betrifft zirkuläre Abhängigkeiten zwischen internen Vue-Paketen: Wenn das Design-System-Paket versehentlich von der Shop-Anwendung importiert, während die Shop-Anwendung gleichzeitig vom Design-System abhängt, kann Turborepo keinen gültigen Abhängigkeitsgraphen mehr berechnen. Ein Linting-Tool wie dependency-cruiser, das im CI-Prozess zirkuläre Abhängigkeiten erkennt, verhindert dieses Problem, bevor es zu kryptischen Turborepo-Fehlermeldungen führt.
#!/usr/bin/env bash
# ci/check-circular-deps.sh — Catch circular package dependencies before Turborepo does
set -euo pipefail
npx depcruise packages apps \
--config .dependency-cruiser.js \
--output-type err-html \
--output-to dep-report.html
# dependency-cruiser exits non-zero when a circular dependency rule is violated
echo "[OK] No circular dependencies found between Vue monorepo packages"
9. Turborepo im Werkzeugvergleich
Für Monorepo-Tooling in Vue-Projekten stehen neben Turborepo weitere etablierte Werkzeuge zur Verfügung, die unterschiedliche Kompromisse zwischen Konfigurationsaufwand, Funktionsumfang und Ökosystem-Reife eingehen.
| Werkzeug | Konfigurationsaufwand | Remote-Caching | Besonderheit |
|---|---|---|---|
| Turborepo | Niedrig | Eingebaut | Einfache turbo.json, schneller Einstieg |
| Nx | Hoch | Eingebaut (Nx Cloud) | Umfangreichste Features, Code-Generatoren |
| pnpm Workspaces (allein) | Minimal | Keins | Nur Dependency-Verknüpfung, kein Task-Caching |
| Lerna (klassisch) | Mittel | Über Nx-Integration | Historisch verbreitet, heute meist mit Nx kombiniert |
Für die meisten Vue-Teams, die frisch in ein Monorepo einsteigen, ist Turborepo wegen des geringen Konfigurationsaufwands und der schnellen Einrichtungszeit die pragmatischste Wahl. Nx lohnt sich, sobald zusätzlich Code-Generatoren, Dependency-Graph-Visualisierung oder tiefere IDE-Integration benötigt werden, bringt aber eine steilere Lernkurve mit sich.
Mironsoft
Monorepo-Architektur, Turborepo und schnelle Vue-Build-Pipelines
CI-Laufzeiten für euer Vue-Monorepo drastisch senken?
Wir migrieren eure Vue-Projekte in eine Turborepo-Struktur mit korrekten Task-Pipelines, Remote-Caching und affected-basierten CI-Läufen, die nur bauen, was sich tatsächlich geändert hat.
Workspace-Setup
pnpm-Workspaces mit sauberer apps/packages-Struktur aufbauen
Caching-Strategie
Turbo.json-Pipelines und Remote-Cache für Team und CI konfigurieren
CI-Optimierung
Affected-basierte Pipelines, die nur betroffene Vue-Apps bauen
10. Zusammenfassung
Monorepo Vue-Projekte mit Turborepo lösen das Skalierungsproblem, das entsteht, sobald mehrere Vue-Anwendungen und geteilte Pakete in einem Repository verwaltet werden. Eine klare apps/- und packages/-Struktur mit pnpm-Workspaces bildet das Fundament, während turbo.json deklarativ definiert, wie Tasks voneinander abhängen. Inkrementelles Caching mit korrekt konfiguriertem outputs-Feld reduziert Build-Zeiten drastisch, sobald sich Eingaben eines Pakets nicht geändert haben.
Remote-Caching teilt diese Zeitersparnis über das gesamte Team und die CI-Pipeline, während affected-basierte Filter zusätzlich sicherstellen, dass nur tatsächlich betroffene Vue-Anwendungen überhaupt gebaut werden. Wer die typischen Fallstricke bei outputs-Konfiguration, Umgebungsvariablen und zirkulären Paketabhängigkeiten vermeidet, bekommt mit Turborepo eine der pragmatischsten Lösungen für schnelle, wartbare Vue-Monorepos.
Monorepo Vue-Projekte mit Turborepo — Das Wichtigste auf einen Blick
Workspace-Struktur
apps/ für Anwendungen, packages/ für geteilte Pakete, verknüpft über pnpm-Workspaces.
Task-Pipelines
turbo.json mit dependsOn definiert Build-Reihenfolge automatisch aus dem Abhängigkeitsgraphen.
Caching
Korrektes outputs-Feld und env-Liste verhindern veraltete, fälschlich gecachte Builds.
CI-Optimierung
Remote-Caching plus --filter für affected Pakete reduzieren CI-Laufzeiten drastisch.