Workspaces sauber strukturieren, ohne Phantom-Dependencies
pnpm Workspaces organisieren mehrere TypeScript-Pakete in einem Repository, ohne die Dependency-Duplizierung von npm oder das großzügige Hoisting von Yarn Classic. Wer ein Monorepo neu aufsetzt oder ein bestehendes migriert, profitiert von einem strikteren, aber vorhersehbareren Dependency-Baum.
Inhaltsverzeichnis
- 1. Warum pnpm Workspaces für TypeScript-Monorepos
- 2. pnpm-workspace.yaml und Paketstruktur
- 3. Cross-Package-Referenzen mit TypeScript Project References
- 4. Eine geteilte tsconfig-Basis über extends
- 5. Dependency Management mit dem workspace-Protokoll
- 6. Build-Orchestrierung: tsc -b, Turborepo oder Nx
- 7. Publishing und Versionierung mit Changesets
- 8. CI-Caching-Strategien für schnelle Pipelines
- 9. Häufige Fallstricke: Hoisting, Peer Dependencies, zirkuläre Referenzen
- 10. Zusammenfassung
- 11. FAQ
1. Warum pnpm Workspaces für TypeScript-Monorepos
pnpm speichert jedes installierte Paket genau einmal in einem globalen, inhaltsadressierten Store und verlinkt es über harte Links in die einzelnen node_modules-Ordner der Pakete. Das spart Plattenplatz und beschleunigt Installationen erheblich, besonders bei vielen Paketen mit überlappenden Abhängigkeiten.
Wichtiger für TypeScript-Monorepos ist aber die strikte node_modules-Struktur: Ein Paket sieht standardmäßig nur die Abhängigkeiten, die es tatsächlich selbst deklariert hat, nicht die transitiv installierten Pakete eines Nachbarpakets.
Das verhindert sogenannte Phantom-Dependencies, bei denen Code kompiliert und läuft, weil ein Paket zufällig über ein anderes Paket im Baum verfügbar ist, aber bricht, sobald diese zufällige Verfügbarkeit sich ändert. Gerade in großen TypeScript-Monorepos mit vielen Teams ist dieser Effekt schwer zu debuggen, weil der Fehler oft erst Monate später bei einem scheinbar unabhängigen Update auftritt.
2. pnpm-workspace.yaml und Paketstruktur
Die Datei pnpm-workspace.yaml im Repository-Root definiert, welche Verzeichnisse als Workspace-Pakete gelten, meist über Glob-Muster wie packages/* und apps/*. Jedes gefundene Verzeichnis mit eigener package.json wird zu einem eigenständigen Paket im Workspace.
Eine typische Struktur trennt wiederverwendbare Bibliotheken unter packages/ von eigenständigen Anwendungen unter apps/, sodass etwa ein UI-Komponenten-Paket, ein Utility-Paket und eine Webanwendung im selben Repository koexistieren, aber unabhängig versioniert und getestet werden können.
Jedes Paket bekommt seine eigene tsconfig.json, package.json und, falls nötig, eigene Build-Skripte, während gemeinsame Tooling-Konfiguration wie ESLint oder Prettier zentral im Root liegt und von allen Paketen referenziert wird.
# pnpm-workspace.yaml
packages:
- "packages/*"
- "apps/*"
- "!**/test/**"
3. Cross-Package-Referenzen mit TypeScript Project References
Damit ein Paket die Typen eines anderen Workspace-Pakets korrekt auflöst und dabei inkrementell kompiliert, statt bei jeder Änderung das gesamte Monorepo neu zu bauen, kommen TypeScript Project References über references in tsconfig.json zum Einsatz.
Jedes referenzierte Paket muss composite: true in seinen compilerOptions setzen, damit TypeScript Deklarationsdateien und einen Build-Info-Cache erzeugt, den abhängige Pakete für die eigene Typprüfung wiederverwenden können.
Der Befehl tsc --build, kurz tsc -b, respektiert die Abhängigkeitsreihenfolge zwischen den referenzierten Projekten automatisch und baut nur die Pakete neu, deren Quellcode oder Abhängigkeiten sich seit dem letzten Lauf geändert haben.
// packages/api-client/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist"
},
"references": [{ "path": "../shared-types" }]
}
4. Eine geteilte tsconfig-Basis über extends
Statt compilerOptions in jedem Paket separat zu pflegen, definiert eine zentrale tsconfig.base.json im Root gemeinsame Einstellungen wie strict, target oder moduleResolution, die jedes Paket über extends übernimmt.
Paketspezifische Abweichungen, etwa unterschiedliche lib-Einstellungen für ein Node.js-Backend-Paket gegenüber einem React-Frontend-Paket, werden lokal in der jeweiligen tsconfig.json überschrieben, ohne die gemeinsame Basis zu verändern.
Diese Struktur reduziert Konfigurationsdrift zwischen Paketen erheblich: Eine Änderung an der Basis, etwa eine strengere Compiler-Option, wirkt sofort auf das gesamte Monorepo, statt in jedem Paket einzeln nachgezogen werden zu müssen.
5. Dependency Management mit dem workspace-Protokoll
Statt eine Versionsnummer aus der npm-Registry anzugeben, referenziert "@acme/shared-types": "workspace:*" in package.json direkt das lokale Paket im selben Repository, unabhängig von dessen veröffentlichter Version.
Beim Publishen ersetzt pnpm das workspace:-Protokoll automatisch durch die tatsächliche Versionsnummer des referenzierten Pakets, sodass veröffentlichte Pakete korrekte, auflösbare Abhängigkeiten enthalten, ohne dass Entwickler das manuell nachpflegen müssen.
Varianten wie workspace:^ oder workspace:~ steuern dabei, welches Semver-Range-Präfix beim Publishen eingesetzt wird, während lokal während der Entwicklung immer die aktuelle Arbeitskopie verwendet wird, ganz ohne erneute Installation nach jeder Änderung.
// apps/web/package.json
{
"dependencies": {
"@acme/shared-types": "workspace:*",
"@acme/ui": "workspace:^"
}
}
6. Build-Orchestrierung: tsc -b, Turborepo oder Nx
Für kleinere Monorepos genügt tsc --build auf das Root-Projekt mit allen referenzierten Paketen, um in korrekter Reihenfolge inkrementell zu bauen. Der eingebaute Build-Cache von TypeScript reicht hier oft völlig aus.
Sobald Build-Zeiten oder die Anzahl paralleler Skripte wachsen, übernehmen Tools wie Turborepo oder Nx die Orchestrierung: Sie erkennen den Abhängigkeitsgraphen zwischen Workspace-Paketen automatisch aus package.json und cachen Build-Ergebnisse remote, teamweit, statt nur lokal.
Beide Ansätze lassen sich kombinieren: tsc -b übernimmt die eigentliche Typprüfung und Kompilierung pro Paket, während Turborepo oder Nx lediglich die Reihenfolge, Parallelisierung und das Caching über alle Skripte im Monorepo steuern.
7. Publishing und Versionierung mit Changesets
Für Monorepos, die einzelne Pakete tatsächlich auf npm veröffentlichen, hat sich Changesets als De-facto-Standard etabliert: Jede Änderung erhält eine kleine Markdown-Datei, die beschreibt, welche Pakete betroffen sind und ob es sich um ein Patch-, Minor- oder Major-Release handelt.
Beim Release-Lauf sammelt Changesets alle offenen Changeset-Dateien, berechnet daraus die neuen Versionsnummern unter Berücksichtigung des workspace:-Protokolls und aktualisiert automatisch alle betroffenen package.json-Dateien samt CHANGELOG.
Das funktioniert speziell mit pnpm zuverlässig, weil die strikte Dependency-Auflösung sicherstellt, dass ein veröffentlichtes Paket wirklich nur die Abhängigkeiten enthält, die es deklariert hat, statt versehentlich von einem Hoisting-Nachbarn zu profitieren.
8. CI-Caching-Strategien für schnelle Pipelines
pnpm bringt über pnpm fetch und den content-addressierten Store bereits eine effiziente Grundlage für CI-Caching: Der globale Store lässt sich zwischen Pipeline-Läufen cachen, wodurch wiederholte Installationen fast ausschließlich aus dem lokalen Cache bedient werden.
Für die eigentliche Build- und Testausführung lohnt sich zusätzliches Caching auf Task-Ebene, etwa über Turborepo Remote Caching, das Build-Ergebnisse anhand eines Hashes aus Quellcode und Abhängigkeiten wiederverwendet, sobald sich an einem Paket nichts geändert hat.
In der Praxis reduziert die Kombination aus pnpm-Store-Caching und Task-Level-Caching die CI-Laufzeit in wachsenden Monorepos oft um mehr als die Hälfte, weil unveränderte Pakete weder neu installiert noch neu gebaut oder getestet werden müssen, was sich besonders bei häufigen, kleinen Pull-Requests auszahlt.
9. Häufige Fallstricke: Hoisting, Peer Dependencies, zirkuläre Referenzen
Die strikte node_modules-Struktur von pnpm deckt Phantom-Dependencies zuverlässig auf, führt aber gelegentlich zu Fehlern bei Paketen, die intern selbst inkorrekt auf transitiv verfügbare Abhängigkeiten vertrauen, statt sie explizit zu deklarieren.
Peer Dependencies verlangen in einem strikten Setup, dass jedes Paket, das eine Peer Dependency erwartet, auch tatsächlich eine kompatible Version im Workspace vorfindet. Fehlt sie, meldet pnpm das explizit als Warnung, statt sie stillschweigend über Hoisting zu ignorieren.
Zirkuläre Abhängigkeiten zwischen Workspace-Paketen, etwa wenn Paket A Paket B importiert und umgekehrt, werden von tsc --build als Fehler erkannt und müssen vor dem ersten erfolgreichen Build aufgelöst werden, meist durch Extraktion der gemeinsam genutzten Typen in ein drittes, gemeinsames Paket, das beide ursprünglichen Pakete unabhängig voneinander importieren können.
| Merkmal | pnpm Workspaces | npm Workspaces | Yarn Workspaces |
|---|---|---|---|
| node_modules-Struktur | Strikt, keine Phantom-Dependencies | Flach gehoistet, anfällig für Phantom-Deps | Flach gehoistet (Classic), strikt (Berry PnP) |
| Plattenverbrauch | Gering, über globalen Store per Hardlink | Hoch, Duplikate je Projekt möglich | Mittel bis gering, je nach Modus |
| workspace-Protokoll | workspace:* nativ unterstützt | Unterstützt seit npm 7 | Unterstützt seit Yarn Berry |
| Installationsgeschwindigkeit | Sehr schnell durch Caching und Hardlinks | Mittel | Schnell mit Plug'n'Play, sonst mittel |
| Monorepo-Tooling-Integration | Sehr gut mit Turborepo, Nx, Changesets | Gut, etwas mehr manuelle Konfiguration | Gut, PnP kann Kompatibilitätsprobleme verursachen |
Mironsoft
TypeScript-Migration, Typsicherheit und Team-Onboarding
JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?
Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.
Migrations-Fahrplan
Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.
Strict-Mode-Einführung
tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.
Team-Onboarding
Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.
10. Zusammenfassung
pnpm Workspaces
Keine Phantom-Deps
Strikte node_modules-Struktur pro Paket
workspace:-Protokoll
Lokale Referenz statt Registry-Version
Project References
tsc -b für inkrementelle Builds
Weniger Plattenplatz
Ein globaler Store statt Duplikate