von pnpm Workspaces bis zum Nx Project Graph
Monorepo JS Tooling loest ein Problem, das mit wachsender Anzahl an Paketen in einem Projekt unausweichlich wird: geteilte Abhaengigkeiten, wiederholte Build-Schritte und Versionierung ueber mehrere Packages hinweg. Workspaces in npm, pnpm und yarn legen das Fundament, waehrend Turborepo und Nx als Task-Runner darauf aufbauen und mit Caching die Build-Zeit drastisch senken.
Inhaltsverzeichnis
- 1. Was ein Monorepo loest und wann es sich lohnt
- 2. Workspaces: npm, pnpm und yarn im Vergleich
- 3. pnpm Workspace-Protokoll und Symlink-Strategie
- 4. Task-Runner: Turborepo Grundlagen und Caching
- 5. Nx: Project Graph und affected Commands
- 6. Versionierung: Changesets fuer unabhaengige Releases
- 7. Shared Configs: eslint, tsconfig, tailwind teilen
- 8. CI-Caching-Strategien fuer Monorepos
- 9. npm Workspaces vs. pnpm vs. Turborepo vs. Nx
- 10. Zusammenfassung
- 11. FAQ
1. Was ein Monorepo loest und wann es sich lohnt
Ein Monorepo buendelt mehrere logisch getrennte Pakete, etwa eine Frontend-App, eine Backend-API und geteilte UI-Komponenten, in einem einzigen Git-Repository. Monorepo JS Tooling beschreibt den gesamten Werkzeugkasten, der noetig ist, um diese Struktur produktiv zu betreiben: Paketverwaltung, Task-Ausfuehrung, Caching und Versionierung ueber Paketgrenzen hinweg. Ohne dieses Tooling wird ein Monorepo schnell zur Belastung, weil jede Aenderung in einem Kernpaket manuell in allen abhaengigen Paketen nachgezogen werden muesste.
Der Hauptvorteil eines Monorepos liegt in atomaren Aenderungen: eine Aenderung an einer geteilten Utility-Bibliothek und ihre Konsumenten lassen sich in einem einzigen Commit und einem einzigen Pull Request abbilden, statt ueber mehrere Repositories mit versetzten Versions-Updates synchronisiert zu werden. Fuer Teams, die viele eng verwandte Pakete pflegen, etwa ein Design-System mit mehreren Frontend-Apps, reduziert Monorepo JS Tooling den Koordinationsaufwand erheblich.
Ein Monorepo lohnt sich allerdings nicht automatisch fuer jedes Projekt. Bei wenigen, komplett unabhaengigen Paketen ohne geteilten Code ueberwiegt oft der zusaetzliche Tooling-Aufwand den Nutzen. Monorepo JS Tooling entfaltet seinen Wert vor allem dort, wo mehrere Pakete haeufig gemeinsam geaendert werden und eine konsistente Versionierung ueber Paketgrenzen hinweg wichtig ist, etwa bei Plattform-Teams mit mehreren Micro-Frontends oder Bibliotheks-Monorepos mit vielen kleinen, verwandten npm-Paketen.
2. Workspaces: npm, pnpm und yarn im Vergleich
Workspaces sind das Fundament jedes Monorepo JS Tooling Setups: eine Deklaration im Root-package.json, die dem Package Manager mitteilt, welche Unterverzeichnisse als eigenstaendige Pakete behandelt werden sollen. Alle drei grossen Package Manager, npm, pnpm und yarn, unterstuetzen Workspaces mittlerweile nativ, mit teils erheblichen Unterschieden in Installationsgeschwindigkeit, Festplattenverbrauch und Striktheit bei impliziten Abhaengigkeiten.
npm Workspaces, seit Version 7 verfuegbar, ist die am weitesten verbreitete Wahl, weil kein zusaetzliches Tool installiert werden muss. yarn Workspaces, mit Yarn Berry (Version 2+) und dem Plug'n'Play-Modus, geht noch einen Schritt weiter und ersetzt node_modules teilweise durch eine ZIP-basierte Struktur, was Installationen beschleunigt, aber Kompatibilitaetsprobleme mit Tools verursachen kann, die direkten Dateisystemzugriff auf node_modules erwarten.
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint"
}
}
Diese Grunddeklaration ist bei npm und yarn nahezu identisch, pnpm nutzt stattdessen eine separate pnpm-workspace.yaml-Datei, dazu mehr im naechsten Abschnitt. Wichtig fuer alle drei Varianten: Workspaces heben Abhaengigkeiten, die in mehreren Paketen mit derselben Version benoetigt werden, automatisch in ein gemeinsames Wurzel-node_modules-Verzeichnis, was Installationszeit und Festplattenplatz gegenueber getrennten Repositories deutlich reduziert.
3. pnpm Workspace-Protokoll und Symlink-Strategie
pnpm loest die Speicherplatz-Problematik von Monorepo JS Tooling radikaler als npm oder yarn: statt Pakete pro Projekt zu kopieren, haelt pnpm einen globalen Content-addressierten Store und verlinkt Pakete per Hardlink in ein striktes, verschachteltes node_modules-Layout. Das Ergebnis ist nicht nur deutlich weniger Festplattenverbrauch bei vielen Paketen mit gemeinsamen Abhaengigkeiten, sondern auch eine striktere Trennung: ein Paket kann nur auf Abhaengigkeiten zugreifen, die es explizit in seiner eigenen package.json deklariert hat, sogenannte Phantom-Dependencies werden dadurch systematisch verhindert.
Fuer Referenzen zwischen Workspace-Paketen nutzt pnpm das workspace:-Protokoll, das explizit macht, dass eine Abhaengigkeit aus dem lokalen Monorepo kommt und nicht aus der npm-Registry aufgeloest werden soll. Das verhindert ein subtiles Problem: ohne dieses Protokoll koennte pnpm theoretisch eine aeltere, oeffentlich veroeffentlichte Version desselben Paketnamens aus der Registry laden, statt die lokale Workspace-Version zu verwenden.
# pnpm-workspace.yaml — root of the monorepo
packages:
- "apps/*"
- "packages/*"
- "!**/test/**"
{
"name": "@myorg/web-app",
"dependencies": {
"@myorg/ui-components": "workspace:*",
"@myorg/api-client": "workspace:^1.2.0"
}
}
Beim Publizieren nach npm ersetzt pnpm das workspace:*-Protokoll automatisch durch die tatsaechliche, zum Publish-Zeitpunkt aktuelle Versionsnummer, sodass veroeffentlichte Pakete keine ungueltigen Protokoll-Strings enthalten. Diese automatische Transformation ist einer der Gruende, warum pnpm inzwischen die bevorzugte Wahl fuer anspruchsvolles Monorepo JS Tooling in vielen groesseren Open-Source-Projekten geworden ist.
4. Task-Runner: Turborepo Grundlagen und Caching
Workspaces alleine loesen nur die Paketverwaltung, nicht die Ausfuehrung von Build-, Test- und Lint-Kommandos ueber viele Pakete hinweg effizient. Genau hier setzt Turborepo als Task-Runner an: es analysiert Abhaengigkeiten zwischen Paketen ueber eine turbo.json-Konfiguration und fuehrt Tasks nur dann erneut aus, wenn sich Input-Dateien seit dem letzten Lauf tatsaechlich geaendert haben, sonst wird das zwischengespeicherte Ergebnis direkt aus dem Cache zurueckgegeben.
Dieses Caching ist der zentrale Mehrwert von Turborepo im Monorepo JS Tooling Oekosystem: ein build-Kommando, das in fuenf Sekunden lokal oder in der CI durchlaeuft, weil sich nur ein einziges von zwanzig Paketen geaendert hat, spart in Summe erhebliche Zeit gegenueber einem kompletten Neu-Build aller Pakete bei jedem Commit. Turborepo unterstuetzt zusaetzlich Remote Caching, bei dem Cache-Ergebnisse zwischen verschiedenen CI-Laeufen und sogar zwischen Entwicklerrechnern geteilt werden.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
Das Praefix ^ in dependsOn bedeutet, dass Turborepo zuerst den build-Task aller Abhaengigkeiten eines Pakets ausfuehrt, bevor es das Paket selbst baut, genau die Reihenfolge, die man auch manuell einhalten wuerde. Diese deklarative Abhaengigkeitsauflösung ist der Kern dessen, was Monorepo JS Tooling von einer simplen Sammlung von npm-Skripten unterscheidet.
5. Nx: Project Graph und affected Commands
Nx verfolgt einen aehnlichen Ansatz wie Turborepo, geht aber ueber reines Task-Caching hinaus: Nx baut einen vollstaendigen Project Graph, der nicht nur explizite package.json-Abhaengigkeiten erfasst, sondern auch tatsaechliche Code-Importe zwischen Paketen analysiert. Dieser Graph macht Nx besonders in grossen Monorepo JS Tooling Setups mit vielen internen Bibliotheken wertvoll, weil er auch versteckte Abhaengigkeiten erkennt, die in package.json gar nicht deklariert sind.
Der praktische Nutzen zeigt sich vor allem im affected-Kommando: statt Tests oder Builds fuer das gesamte Repository auszufuehren, ermittelt Nx anhand des Git-Diffs und des Project Graphs, welche Pakete von einer Aenderung tatsaechlich betroffen sind, und fuehrt Tasks nur fuer diese aus. In einer CI-Pipeline mit hundert Paketen, von denen sich in einem typischen Pull Request nur drei aendern, reduziert das die Ausfuehrungszeit drastisch, ohne die Testabdeckung zu verringern.
# Run tests only for packages affected by the current branch diff
npx nx affected --target=test --base=main
# Visualize the actual project dependency graph in the browser
npx nx graph
# Run a target for every project that depends on @myorg/ui-components
npx nx run-many --target=build --projects=tag:depends-on-ui
Nx bietet zusaetzlich Generatoren, mit denen neue Pakete nach einem konsistenten Template erzeugt werden, inklusive Tests, Lint-Konfiguration und Build-Setup. Fuer Teams, die haeufig neue interne Pakete anlegen, reduziert das den Boilerplate-Aufwand deutlich staerker als bei Turborepo, das sich bewusst auf Task-Ausfuehrung und Caching konzentriert, ohne eigene Generatoren mitzubringen.
6. Versionierung: Changesets fuer unabhaengige Releases
Versionierung ist eines der am haeufigsten unterschaetzten Probleme im Monorepo JS Tooling: wenn zwanzig Pakete im selben Repository liegen, aber unabhaengig voneinander auf npm veroeffentlicht werden sollen, braucht es ein System, das nachvollziehbar entscheidet, welches Paket welche Versionsnummer bekommt und welche Changelog-Eintraege dazugehoeren. Changesets loest genau dieses Problem mit einem einfachen Workflow: Entwickler fuegen bei jeder relevanten Aenderung eine kleine Markdown-Datei hinzu, die beschreibt, welche Pakete betroffen sind und ob es sich um ein Patch-, Minor- oder Major-Update handelt.
Beim Release-Vorgang sammelt Changesets alle offenen Changeset-Dateien, berechnet daraus automatisch die neuen Versionsnummern nach Semantic Versioning, aktualisiert Changelogs und passt sogar interne Workspace-Referenzen zwischen abhaengigen Paketen an, falls ein internes Paket auf eine neue Major-Version eines anderen internen Pakets angewiesen ist.
# Add a changeset describing the current change
npx changeset
# Interactive prompt: which packages changed, patch/minor/major, summary text
# Consume all pending changesets: bump versions, update changelogs
npx changeset version
# Publish all packages whose version actually changed
npx changeset publish
Der Vorteil gegenueber manueller Versionierung ist Nachvollziehbarkeit: jede Versionserhoehung laesst sich bis zu den urspruenglichen Changeset-Dateien und damit bis zu den zugehoerigen Pull Requests zurueckverfolgen. Fuer Monorepo JS Tooling mit oeffentlich veroeffentlichten Paketen ist Changesets inzwischen der De-facto-Standard, weil es sich nahtlos in CI-Pipelines integrieren laesst und Release-Prozesse ohne manuellen Eingriff automatisiert.
7. Shared Configs: eslint, tsconfig, tailwind teilen
Ein oft uebersehener Baustein von Monorepo JS Tooling ist die Verwaltung geteilter Konfigurationen. Ohne zentrale Strategie landet dieselbe ESLint-Konfiguration, dieselbe tsconfig.json Basis und dieselbe Tailwind-Konfiguration in zwanzig leicht unterschiedlichen Kopien, die bei jeder Aenderung einzeln gepflegt werden muessen. Die uebliche Loesung: ein eigenes internes Paket, etwa @myorg/tsconfig oder @myorg/eslint-config, das als Workspace-Abhaengigkeit in alle anderen Pakete eingebunden wird.
TypeScript unterstuetzt das explizit ueber extends in tsconfig.json, sodass jedes Paket nur die tatsaechlich abweichenden Optionen ueberschreiben muss, waehrend die gemeinsame Basis zentral gepflegt wird. Fuer ESLint funktioniert das analog ueber die extends-Option in der Flat-Config, und Tailwind erlaubt das Teilen einer Basis-Konfiguration ueber ein exportiertes Preset, das jedes Frontend-Paket in seiner eigenen tailwind.config importiert und um projektspezifische Anpassungen erweitert.
8. CI-Caching-Strategien fuer Monorepos
In der Continuous Integration entfaltet Monorepo JS Tooling seinen groessten Zeitgewinn, sofern das Caching richtig konfiguriert ist. Die Grundvoraussetzung ist ein persistenter Cache-Speicher zwischen CI-Laeufen, sowohl fuer installierte Abhaengigkeiten als auch fuer Task-Ergebnisse von Turborepo oder Nx. Ohne diesen Cache muss jeder CI-Lauf alle Abhaengigkeiten neu installieren und alle Pakete neu bauen, selbst wenn sich nur eine einzige Zeile Code geaendert hat.
Remote Caching, wie es sowohl Turborepo als auch Nx anbieten, geht noch einen Schritt weiter: Task-Ergebnisse werden nicht nur zwischen aufeinanderfolgenden Laeufen desselben CI-Jobs geteilt, sondern zwischen allen Jobs und sogar zwischen lokalen Entwicklerrechnern und der CI. Hat ein Entwickler ein Paket bereits lokal gebaut und getestet, kann die CI-Pipeline dieses Ergebnis direkt aus dem geteilten Cache uebernehmen, ohne den Build-Schritt zu wiederholen, solange sich die relevanten Input-Dateien nachweislich nicht geaendert haben.
9. npm Workspaces vs. pnpm vs. Turborepo vs. Nx
Die folgende Uebersicht ordnet die wichtigsten Werkzeuge im Monorepo JS Tooling Oekosystem nach ihrer primaeren Rolle ein, da viele dieser Tools sich nicht gegenseitig ausschliessen, sondern gemeinsam eingesetzt werden.
| Tool | Primaere Rolle | Staerke | Kombinierbar mit |
|---|---|---|---|
| npm Workspaces | Paketverwaltung | Keine zusaetzliche Installation noetig | Turborepo, Nx, Changesets |
| pnpm | Paketverwaltung | Speicherplatz, striktes node_modules | Turborepo, Nx, Changesets |
| Turborepo | Task-Runner, Caching | Einfache Konfiguration, schnelles Setup | npm/pnpm/yarn Workspaces |
| Nx | Task-Runner, Code-Generierung | Project Graph, affected Commands | npm/pnpm/yarn Workspaces |
| Changesets | Versionierung, Release | Nachvollziehbare Changelogs | Alle obigen Tools |
In der Praxis ist die Kombination selten binaer: viele Teams nutzen pnpm fuer die Paketverwaltung, Turborepo oder Nx fuer Task-Ausfuehrung und Caching, und Changesets fuer die Versionierung, alle drei Ebenen von Monorepo JS Tooling ergaenzen sich, statt sich gegenseitig zu ersetzen.
Mironsoft
Monorepo-Architektur und Build-Pipeline-Optimierung
CI-Laufzeiten, die mit jedem neuen Paket weiter wachsen?
Wir richten pnpm Workspaces, Turborepo oder Nx Caching und einen automatisierten Changesets-Release-Workflow fuer euer JavaScript-Monorepo ein und beschleunigen eure CI-Pipeline messbar.
Monorepo-Setup
Workspaces, Task-Runner und geteilte Configs von Grund auf einrichten
CI-Optimierung
Remote Caching und affected-basierte Pipelines fuer kuerzere Laufzeiten
Release-Automatisierung
Changesets-Workflow fuer nachvollziehbare, unabhaengige Package-Releases
10. Zusammenfassung
Monorepo JS Tooling besteht aus mehreren zusammenspielenden Ebenen: Workspaces in npm, pnpm oder yarn legen das Fundament fuer geteilte Abhaengigkeiten, Task-Runner wie Turborepo und Nx sorgen mit Caching und Abhaengigkeitsanalyse dafuer, dass nur tatsaechlich betroffene Pakete neu gebaut werden, und Changesets loest die Versionierungsfrage fuer unabhaengig veroeffentlichte Pakete nachvollziehbar. pnpm setzt sich durch striktes Dependency-Management und effiziente Speichernutzung zunehmend als bevorzugter Package Manager fuer anspruchsvolles Monorepo JS Tooling durch.
Nx eignet sich besonders fuer grosse Setups mit vielen internen Bibliotheken dank Project Graph und Code-Generatoren, waehrend Turborepo mit einfacherer Konfiguration und schnellerem Einstieg punktet. Shared Configs fuer ESLint, TypeScript und Tailwind sowie konsequentes CI-Caching runden ein produktives Monorepo JS Tooling Setup ab und verhindern, dass Build-Zeiten linear mit der Paketanzahl wachsen.
Monorepo JS Tooling — Das Wichtigste auf einen Blick
Workspaces
npm, pnpm oder yarn deklarieren Unterverzeichnisse als Pakete und heben geteilte Abhaengigkeiten in ein gemeinsames node_modules.
Task-Runner
Turborepo und Nx cachen Task-Ergebnisse und fuehren nur Pakete erneut aus, deren Input sich tatsaechlich geaendert hat.
Versionierung
Changesets sammelt kleine Markdown-Beschreibungen und berechnet daraus automatisch Versionsnummern und Changelogs.
CI-Caching
Remote Caching teilt Task-Ergebnisse zwischen CI-Laeufen und Entwicklerrechnern fuer drastisch kuerzere Pipeline-Laufzeiten.