npm-, pnpm- und Yarn-Workspaces: Paketverlinkung im TypeScript-Monorepo
AI generated
<T>
type
TypeScript · Monorepo · Workspaces
npm-, pnpm- und Yarn-Workspaces
Paketverlinkung im TypeScript-Monorepo richtig nutzen

Workspaces sind die technische Grundlage, ohne die kein TypeScript-Monorepo funktioniert, doch die drei großen Package Manager lösen Symlinks, Hoisting und das workspace-Protokoll unterschiedlich, und diese Unterschiede sind die Ursache für viele schwer nachvollziehbare Import-Fehler.

16 Min. Lesezeit Workspaces · workspace-Protokoll · Hoisting npm 10 · pnpm 9 · Yarn Berry

1. Was Workspaces im TypeScript-Monorepo tatsächlich lösen

Ohne Workspaces müsste jedes Paket in einem TypeScript-Monorepo entweder über eine veröffentlichte npm-Version des internen Schwesterpakets installiert werden, oder Entwickler müssten Pakete manuell mit npm link verknüpfen und diese Verknüpfung bei jedem neuen Checkout wiederherstellen. Beide Varianten sind in der Praxis unpraktikabel, weil jede Änderung an einem gemeinsam genutzten Paket erst veröffentlicht oder manuell nachgezogen werden müsste, bevor sie in einem anderen Paket sichtbar wird.

Workspaces lösen dieses Problem, indem der Package Manager alle Pakete eines TypeScript-Monorepos in einem einzigen node_modules-Baum installiert und interne Abhängigkeiten automatisch als Symlinks auf die lokalen Paketverzeichnisse auflöst, statt sie aus der Registry herunterzuladen. Eine Änderung in einem Paket ist damit sofort in jedem anderen Paket sichtbar, das es importiert, ohne Publish-Schritt und ohne manuelle Verlinkung. Genau diese sofortige Sichtbarkeit ist der Kern dessen, was ein TypeScript-Monorepo praktisch nutzbar macht.

Technisch legt jeder Package Manager beim Installieren für jedes interne Paket einen symbolischen Link im node_modules-Verzeichnis an, der auf den tatsächlichen Quellordner des Pakets zeigt, etwa node_modules/@myorg/shared-utils -> ../../packages/shared-utils. Importiert ein anderes Paket @myorg/shared-utils, folgt Node.js dem Symlink und lädt die Dateien direkt aus dem Quellordner, nicht aus einer separaten Kopie.

Diese Mechanik hat eine wichtige Konsequenz für ein TypeScript-Monorepo: Ohne einen laufenden Build-Prozess, der die TypeScript-Quelldateien in JavaScript kompiliert, zeigt der Symlink zwar auf den Quellordner, aber die dist-Ausgabe des importierten Pakets existiert möglicherweise noch nicht oder ist veraltet. Ein häufiges Problem in frisch geklonten Repositories ist deshalb, dass Imports fehlschlagen, weil zwar alle Symlinks korrekt gesetzt sind, aber noch kein einziges Paket gebaut wurde.


# Symlink-Struktur nach der Installation prüfen
ls -la node_modules/@myorg/

# Beispielausgabe:
# shared-utils -> ../../packages/shared-utils
# contracts -> ../../packages/contracts

# Nach frischem Checkout: erst installieren, dann bauen
npm install
npm run build --workspaces

3. Das workspace-Protokoll in package.json

Um ein internes Paket als Abhängigkeit zu deklarieren, reicht in einem TypeScript-Monorepo nicht die normale Versionsnummer wie "1.2.0", weil der Package Manager sonst versuchen würde, diese Version aus der npm-Registry herunterzuladen, statt das lokale Paket zu verlinken. Das workspace:-Protokoll, ursprünglich von pnpm eingeführt und inzwischen auch von Yarn unterstützt, macht diese Absicht explizit: "workspace:*" bedeutet, immer die aktuell im Repository vorhandene Version des lokalen Pakets zu verwenden.

npm selbst kennt das workspace:-Protokoll nicht in derselben Form, löst aber Abhängigkeiten innerhalb des definierten Workspace-Bereichs automatisch lokal auf, solange die Versionsbereiche kompatibel sind, ohne dass ein spezielles Protokoll-Präfix nötig ist. Für ein plattformübergreifendes TypeScript-Monorepo-Setup, das mit mehreren Package Managern kompatibel bleiben soll, empfiehlt sich daher, die genaue Syntax des jeweils verwendeten Tools zu kennen, statt Konfigurationen blind zwischen npm, pnpm und Yarn zu kopieren.


{
  "name": "@myorg/api",
  "dependencies": {
    "@myorg/contracts": "workspace:*",
    "@myorg/shared-utils": "workspace:^1.0.0"
  }
}

4. npm-Workspaces: einfach, aber mit Hoisting-Nebenwirkungen

npm-Workspaces werden über das workspaces-Feld im Root-package.json aktiviert und benötigen keine zusätzliche Konfigurationsdatei. Die Installation aller Pakete erfolgt mit einem einzigen npm install im Root, das npm intern in einen gemeinsamen, flachen node_modules-Baum auflöst. Dieses sogenannte Hoisting hebt gemeinsame externe Abhängigkeiten, etwa dieselbe Version von React in zwei Paketen, in das Root-node_modules-Verzeichnis, statt sie in jedem Paket separat zu duplizieren.

Der Nachteil dieses Hoistings zeigt sich, wenn ein Paket in einem TypeScript-Monorepo versehentlich von einer Abhängigkeit profitiert, die es gar nicht selbst in seiner eigenen package.json deklariert hat, sondern nur über das Hoisting eines anderen Pakets verfügbar ist. Dieses sogenannte Phantom-Dependency-Problem funktioniert lokal, bricht aber, sobald das Paket isoliert installiert oder veröffentlicht wird, weil die eigentlich benötigte Abhängigkeit dann fehlt.


{
  "name": "my-typescript-monorepo",
  "private": true,
  "workspaces": [
    "packages/*",
    "apps/*"
  ]
}

5. pnpm-Workspaces: strikte Isolation über den Content-Store

pnpm verfolgt einen fundamental anderen Ansatz. Statt Pakete zu hoisten, speichert pnpm jede Version jedes Pakets genau einmal in einem globalen Content-Addressable-Store und verlinkt sie über harte Links und Symlinks in eine verschachtelte node_modules-Struktur pro Paket. Jedes Paket in einem TypeScript-Monorepo sieht dadurch ausschließlich die Abhängigkeiten, die es selbst in seiner package.json deklariert hat, und keine gehoisteten Abhängigkeiten anderer Pakete.

Diese strikte Isolation deckt Phantom-Dependencies zuverlässig auf, weil ein fehlender, aber tatsächlich benötigter Import sofort mit einem Fehler fehlschlägt, statt zufällig über einen gehoisteten Pfad zu funktionieren. Der Preis dafür ist eine gewisse Inkompatibilität mit älteren Node.js-Tools, die eine flache node_modules-Struktur voraussetzen, weshalb pnpm zusätzlich eine shamefully-hoist-Option anbietet, um im Einzelfall auf klassisches Hoisting zurückzufallen.


# pnpm-workspace.yaml
packages:
  - "packages/*"
  - "apps/*"
  - "!**/test-fixtures/**"

6. Yarn-Workspaces: Plug'n'Play als Sonderweg

Yarn unterstützt sowohl den klassischen node_modules-Modus, der Hoisting ähnlich zu npm durchführt, als auch den moderneren Plug'n'Play-Modus, bei dem gar kein node_modules-Verzeichnis mehr angelegt wird. Stattdessen erzeugt Yarn eine einzige .pnp.cjs-Datei, die eine Abbildung von Paketnamen auf Zip-Archive im .yarn/cache-Verzeichnis enthält, und Node.js löst Importe über einen speziellen Resolver auf, statt Dateisystem-Symlinks zu folgen.

Für ein TypeScript-Monorepo bringt Plug'n'Play spürbar schnellere Installationen und verhindert Phantom-Dependencies noch strikter als pnpm, weil selbst versehentlich vorhandene, aber nicht deklarierte Pakete im Cache gar nicht auflösbar sind. Der Nachteil ist eine geringere Kompatibilität mit Tools, die direkten Dateisystemzugriff auf node_modules erwarten, weshalb viele Teams für neue TypeScript-Projekte weiterhin den klassischen nodeLinker: node-modules-Modus von Yarn wählen, um Kompatibilitätsprobleme zu vermeiden.

7. tsconfig-Referenzen und Workspace-Paket-Auflösung

Workspaces lösen die Frage, wo ein Paket zur Laufzeit gefunden wird, beantworten aber nicht automatisch, wie der TypeScript-Compiler beim Bearbeiten eines Pakets die Typen eines anderen Workspace-Pakets findet. Ohne zusätzliche Konfiguration im tsconfig.json verlässt sich TypeScript darauf, dass bereits kompilierte Deklarationsdateien im node_modules-Symlink-Ziel vorhanden sind, was bei einem frisch geklonten TypeScript-Monorepo ohne vorherigen Build fehlschlägt.

Die robuste Lösung kombiniert Workspaces für die Laufzeit-Modulauflösung mit TypeScript Project References für die Compile-Zeit-Typprüfung. Jedes Paket referenziert seine internen Abhängigkeiten explizit über das references-Array in seiner tsconfig.json, wodurch der Compiler abhängige Pakete bei Bedarf automatisch mitkompiliert, statt auf veraltete oder fehlende dist-Ausgaben angewiesen zu sein.


{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../shared-utils" },
    { "path": "../contracts" }
  ],
  "include": ["src/**/*.ts"]
}

8. Häufige Fehler bei Workspace-Setups

Der mit Abstand häufigste Fehler ist, ein internes Paket direkt über einen relativen Pfad zu importieren, etwa import { foo } from "../../shared-utils/src/index", statt den Paketnamen über den Workspace-Symlink zu nutzen. Das funktioniert zunächst lokal, bricht aber sobald sich die Verzeichnisstruktur ändert oder das Paket unabhängig getestet werden soll, weil der relative Pfad die eigentliche Paketgrenze in einem TypeScript-Monorepo umgeht.

Ein zweiter häufiger Fehler betrifft veraltete Lockfiles: Wird ein neues internes Paket zum Root-package.json hinzugefügt, aber kein neuer install-Lauf durchgeführt, findet der Package Manager den neuen Symlink nicht, und Imports schlagen mit einer Meldung fehl, die fälschlicherweise wie ein Tippfehler im Paketnamen aussieht. Der Fix ist meist simpel, aber leicht zu übersehen: Nach jeder Änderung am Workspace-Feld oder an internen Abhängigkeiten erneut install ausführen.

9. npm, pnpm und Yarn im direkten Vergleich

Alle drei Package Manager lösen das Grundproblem der lokalen Paketverlinkung, unterscheiden sich aber deutlich in Isolation, Geschwindigkeit und Kompatibilität.

Kriterium npm pnpm Yarn (PnP)
Phantom Dependencies Möglich durch Hoisting Verhindert durch Isolation Verhindert durch Cache-Resolver
Festplattenverbrauch Hoch, viele Duplikate Niedrig, Content-Store Niedrig, Zip-Cache
Tool-Kompatibilität Sehr hoch Hoch, selten Anpassung nötig Niedriger bei PnP-Modus
workspace:-Protokoll Implizit, kein Präfix nötig Ursprung des Protokolls Vollständig unterstützt

Für Teams, die maximale Kompatibilität mit bestehendem Tooling priorisieren, bleibt npm die risikoärmste Wahl. Wer strikte Isolation gegen Phantom-Dependencies und geringeren Festplattenverbrauch in einem wachsenden TypeScript-Monorepo priorisiert, profitiert stärker von pnpm oder Yarn im Plug'n'Play-Modus, muss dafür aber gelegentliche Kompatibilitätsprobleme mit älteren Tools einplanen.

Mironsoft

TypeScript-Monorepo-Setup, Package-Management und Build-Infrastruktur

Verwirrende Import-Fehler in eurem Workspace-Setup?

Wir wählen den passenden Package Manager für euer TypeScript-Monorepo, beheben Phantom-Dependency-Probleme und richten eine saubere Kombination aus Workspaces und Project References ein.

Package-Manager-Wahl

npm, pnpm oder Yarn passend zu Teamgröße und Tooling auswählen

Migration

Bestehende Repositories ohne Downtime auf Workspaces umstellen

Fehlerdiagnose

Phantom-Dependencies und Hoisting-Probleme systematisch aufspüren

10. Zusammenfassung

Workspaces sind das technische Fundament, das ein TypeScript-Monorepo überhaupt erst praktikabel macht, indem interne Pakete über Symlinks statt über veröffentlichte npm-Versionen verlinkt werden. npm setzt auf einfaches, aber Phantom-Dependency-anfälliges Hoisting, pnpm auf strikte Isolation über einen Content-Store, und Yarn im Plug'n'Play-Modus geht noch einen Schritt weiter und verzichtet ganz auf node_modules.

Unabhängig vom gewählten Package Manager gilt: Das workspace:-Protokoll macht interne Abhängigkeiten explizit, und die Kombination aus Workspaces für die Laufzeit-Auflösung mit TypeScript Project References für die Compile-Zeit-Typprüfung verhindert die meisten typischen Import-Fehler in einem wachsenden TypeScript-Monorepo.

Die Wahl des Package Managers ist selten dauerhaft in Stein gemeißelt, ein späterer Wechsel bleibt möglich, sollte aber bewusst und mit ausreichend Testzeit geplant werden, statt spontan mitten in einem Sprint vollzogen zu werden.

Workspaces im TypeScript-Monorepo — Das Wichtigste auf einen Blick

Symlink-Mechanik

Interne Pakete werden über Symlinks in node_modules verlinkt, Änderungen sind sofort sichtbar, ohne Publish-Schritt.

workspace:-Protokoll

Macht explizit, dass eine Abhängigkeit lokal statt aus der Registry aufgelöst werden soll.

Isolation vs. Hoisting

pnpm und Yarn PnP verhindern Phantom-Dependencies strikter als das klassische Hoisting von npm.

tsconfig-Referenzen

Project References ergänzen Workspaces um verlässliche Compile-Zeit-Typprüfung über Paketgrenzen hinweg.

11. FAQ: npm-, pnpm- und Yarn-Workspaces

1Unterschied npm, pnpm, Yarn Workspaces?
npm hoistet, pnpm isoliert strikt über Content-Store, Yarn bietet zusätzlich Plug'n'Play ohne node_modules.
2Was ist das workspace:-Protokoll?
Versionsangabe, die sagt, ein internes Paket lokal zu verlinken statt aus der Registry zu laden.
3Import nach Checkout kaputt?
Meist fehlt der Build-Schritt für interne Pakete nach der Installation.
4Was ist eine Phantom Dependency?
Eine ungenutzt deklarierte, aber durch Hoisting verfügbare Abhängigkeit. Bricht bei isolierter Installation.
5Relative Pfade statt Paketnamen?
Nein, immer über Paketnamen importieren, relative Pfade brechen bei Strukturänderungen.
6Reichen Workspaces für Typprüfung?
Nicht zuverlässig, Project References im tsconfig.json sind zusätzlich nötig.
7Was ist Yarn Plug'n'Play?
Modus ohne node_modules, Auflösung über .pnp.cjs und Zip-Cache. Schneller, aber weniger tool-kompatibel.
8Warum ist pnpm strikter als npm?
Verschachtelte node_modules-Struktur, jedes Paket sieht nur eigene deklarierte Abhängigkeiten.
9Package Manager mischen?
Nicht empfohlen, unterschiedliche Lockfiles führen zu Widersprüchen. Einen Manager pro Repository festlegen.
10Immer neu installieren nötig?
Ja, nach jeder Abhängigkeitsänderung oder Änderung am workspaces-Feld erneut install ausführen.