pnpm ist ein moderner Paketmanager für Node.js-Projekte, der im Moment immer mehr an Beliebtheit gewinnt. Auch ich bin mittlerweile nach Jahren von npm auf pnpm gewechselt. Pnpm verringert den Speicherverbrauch von node_modules Ordnern, bietet exzellente Unterstützung von Monorepositories und kann mehr oder weniger als Drop-in-Replacement angesehen werden. Was pnpm besonders macht, erläutere ich in diesem Beitrag.
Im Gegensatz zu npm oder Yarn speichert pnpm jede Abhängigkeit nur einmal zentral auf dem Computer und verlinkt diese dann in die einzelnen Projekte. Dadurch spart es erheblich Speicherplatz und beschleunigt Installationen. Die Effizienz untermauert pnpm sogar mit offiziellen Benchmarks: https://pnpm.io/benchmarks.
Pnpm bietet einen im Vergleich zu npm stark verbesserten Monorepository-Support. An dieser Stelle möchte ich auf die drei folgenden Punkte genauer eingehen.
Verbessertes Dependency-Hoisting in Monorepositories
Der Workspace-Support von npm installiert Abhängigkeiten im Rootordner des Monorepositories. Dies soll unter anderem Speicherplatz sparen. Man spricht hierbei von sogenanntem Hoisting.
Node.js löst Abhängigkeiten in einem Projekt nach einem klaren, aber oft unterschätzten Prinzip auf: Es durchsucht node_modules-Verzeichnisse von unten nach oben. Das kann zu unerwarteten Effekten führen – insbesondere in Monorepos oder bei verschachtelten Projekten. So kann etwa in einem Projekt das Package express importiert werden und Node.js geht wie folgt vor:
1. Lokales node_modules des aktuellen Pakets
Zuerst sucht Node.js im node_modules-Ordner des aktuellen Arbeitsverzeichnisses: z. B. für das Paket lodash im Ordner ./node_modules/lodash.
Falls das Paket nicht gefunden wurde, sucht Node.js eine Ebene höher:
2. Übergeordnete node_modules-Verzeichnisse
Node.js steigt im Verzeichnisbaum nach oben und prüft jedes node_modules, bis es das Modul findet.
3. Bis zum Wurzelverzeichnis
Falls das Modul nirgends gefunden wird, wirft Node.js einen MODULE_NOT_FOUND-Fehler.
Das heißt es kann zu sogenannten Geister-Abhängigkeiten (Phantom-Dependencies) kommen. Ein Paket kann eine Abhängigkeit nutzen, die im Monorepository-Wurzelverzeichnis liegt, obwohl sie in der package.json des Pakets nicht als Dependency definiert ist. Dies wird spätestens dann zum Problem, wenn das Monorepository ein Paket enthält, (etwa eine Library), die außerhalb des Monorepositories (etwa in der Npm-Registry) veröffentlicht werden soll. Der veröffentlichten Version würde in diesem Fall die Abhängigkeit fehlen.
Pnpm umgeht dieses Problem, indem es die Abhängigkeiten nicht in das Wurzelverzeichnis hoistet. In einem Monorepository mit pnpm müssen alle Abhängigkeiten deklariert werden.
Pakete aus dem Monorepository referenzieren
Ein Anwendungsfall für Monorepositories ist, dass man viele Pakete hat, die voneinander abhängig sind. In npm muss hierfür das Paket als normale Abhängigkeit mit Namen und Versionsnummer angegeben werden. Nehmen wir an, in unserem Paket foo wollen wir das Paket bar referenzieren: "bar": "1.0.0"
Sobald sich in bar die Versionsnummer ändert, etwa weil wir eine Änderung released haben, wird weiterhin bar in Version 1.0.0 referenziert. In der Regel wollen wir aber nicht unser Paket in einer bestimmten Version referenzieren, sondern in der aktuellen Version, wie sie in unserem Monorepository liegt. Hierfür gibt es in pnpm den Workspace-Alias. Um die Version von foo in unserem Repository zu referenzieren können wir die Abhängigkeit mit "bar": "workspace:*" angeben.
Wenn wir unser Paket in der npm-Registry veröffentlichen, wird automatisch die Workspace-Syntax von pnpm durch die korrekte Version ersetzt.
Versionen zentral definieren und im Monorepository referenzieren
Ein zweiter Anwendungsfall im Monorepository ist, dass eine Abhängigkeit, etwa @angular/core in mehreren Paketen verwendet wird. So könnte Angular zum Beispiel als peerDependency, das heißt als Abhängigkeit, die zur korrekten Funktionsweise des Pakets installiert sein muss, definiert sein. In allen Paketen müsste also @angular/core und die passende Versionsnummer angegeben werden. Spätestens, wenn man die Angular-Version anpassen möchte, wird dies unübersichtlich, weil die Versionsnummer an diversen Stellen angepasst werden muss. Doch das muss nicht sein! Pnpm bietet hierfür das Konzept der sogenannten catalogs. Ein Katalog ist eine (benannte) Liste von Abhängigkeiten inklusive Versionsnummern. Bei der Definition einer Abhängigkeit kann nun anstelle der Versionsnummer der Name des Catalogs angegeben werden. Somit muss die Versionsnummer nur einmal im Catalog gepflegt werden. Möchte man nur einen Catalog nutzen, kann er wie folgt definiert werden:
catalog:
'@angular/core': '22.0.0'
Dieser Catalog lässt sich dann wie folgt nutzen: "@angular/core": "catalog:".
Möchte man mehrere Kataloge definieren, so muss man ihnen Namen geben:
catalogs:
angular22:
'@angular/core': '22.0.5'
angular21:
'@angular/core': '22.2.17'
Diesen Namen des Katalogs muss man dann bei der Nutzung angeben: "@angular/core": "catalog:angular22".
Genau wie bei der Workspace-Syntax, wird auch die Catalog-Syntax vor der Veröffentlichung eines Paketes von pnpm aufgelöst.
Gute Dokumentation
Ein weiterer Aspekt, der mir an pnpm gut gefällt, ist die Dokumentation. Sie ist sehr anschaulich und umfangreich. Features und Designentscheidungen sind ausführlich beschrieben. Doch die Dokumentation geht noch weiter: So beschreibt sie Rezepte zur Integration von pnpm in den eigenen Entwicklungsworkflow. So wird etwa die Nutzung von pnpm mit Changesets oder in einer CI-Pipeline beschrieben. Nutzer:innen werden so bei häufigen Fragestellungen an die Hand genommen.
Sinnvolle Sicherheitseinstellungen
Pnpm hat in den letzten Jahren viel unternommen, um die Nutzung der Npm-Registry sicherer zu machen:
Postinstall-Skripte sind standardmäßig deaktiviert
Neue Dependencies werden erst 24 Stunden nach Veröffentlichung heruntergeladen
Subdependencies aus unsicheren Quellen wie Git-Repositories oder Tar-Archiven sind standardmäßig nicht erlaubt
Dazu gibt es einen eigenen Guide auf der Website, der beschreibt, wie Supply-Chain-Angriffe mit pnpm abgewehrt werden können.
Fazit
Ich habe meinen Umstieg auf pnpm bisher nicht bereut. Im Gegenteil: immer wieder stelle ich fest, dass pnpm für meine Arbeit besser geeignet ist als npm. Der einzige Wermutstropfen ist, dass aufgrund der umfangreichen Konfigurationsmöglichkeiten und Features der Umstieg von einzelnen Entwickler:innen nicht sinnvoll ist. Es müssen immer ganze Projektteams migrieren, damit die Vorteile von pnpm vollständig zur Geltung kommen können. Da die meisten die Nutzung von npm gewöhnt sind, kann hier also ein bisschen Überzeugungsarbeit nötig sein. Gleichzeitig ist der Umstieg in der Regel problemlos möglich, da die CLI von pnpm bis auf wenige Ausnahmen dieselben Befehle verwendet wie npm. Ein p am Anfang ist also alles, was dem Umstieg im Wege steht!