Führen Sie in einem Projekt npm install aus und danach für dieselbe package.json pnpm install. Sie erhalten zwei unterschiedliche node_modules-Bäume und zwei unterschiedliche Lockfiles. Kommt ein Kollege dazu, der Yarn nutzt, landet das Repository mit drei Lockfiles in seiner Historie, jeder mit einem leicht anderen Abhängigkeitsbaum. Alle drei lesen dieselbe package.json und laden aus derselben npm-Registry. Uneinig sind sie sich darüber, was auf die Festplatte geschrieben wird.
Wie npm, Yarn und pnpm node_modules aufbauen
Die Struktur von npm ist flach. Jedes Paket, das Ihre Abhängigkeiten brauchen, egal wie tief im Baum, wird in die oberste node_modules gehoben, solange kein Versionskonflikt besteht. Das ist seit npm 3 das Standardverhalten. Der Nebeneffekt: Ihr Code kann ein Paket per require() oder import laden, das Sie nie in package.json eingetragen haben, solange etwas anderes im Baum davon abhängt. Das funktioniert, bis dieses andere Paket die Abhängigkeit in einem Minor-Release fallenlässt und Ihr Build bricht, ohne dass Sie Ihren eigenen Code geändert hätten. Das ist eine Phantom-Abhängigkeit, und die Struktur von npm tut nichts, um sie zu verhindern.
Yarn Classic (v1) hebt genauso, hat also dieselbe Angriffsfläche. Yarn Berry (v2 und später) setzt standardmäßig auf Plug’n’Play, das überhaupt kein node_modules hat. Abhängigkeiten liegen gepackt in .yarn/cache, und eine generierte .pnp.cjs-Datei bildet jeden Import direkt auf das richtige Archiv ab. Committen Sie .yarn/cache zusammen mit dem Lockfile, läuft ein frischer Clone ohne Installationsschritt – Yarn nennt das Zero-Install. Der Preis ist Kompatibilität. Tools, die node_modules direkt durchsuchen, und native Add-ons, die echte Dateien auf der Platte erwarten, können unter PnP scheitern. nodeLinker: node-modules in .yarnrc.yml stellt die konventionelle flache Struktur wieder her und gibt dafür Zero-Install auf.
pnpm pflegt einen einzigen, inhaltsadressierten Store pro Maschine. Jede Paketversion liegt genau einmal auf der Platte, indiziert über einen Hash ihres Inhalts, egal wie viele Projekte sie verwenden. Für jedes Projekt verlinkt pnpm die benötigten Dateien per Hardlink in einen virtuellen Store unter node_modules/.pnpm und setzt dann nur für Ihre direkten Abhängigkeiten Symlinks in die oberste node_modules. Sie können node_modules/react auflösen, weil Sie es deklariert haben. Ein Paket, das nur zwei Ebenen tiefer im Baum einer anderen Abhängigkeit existiert, können Sie nicht auflösen, weil nichts es mit der obersten Ebene verknüpft. Das Phantom-Abhängigkeits-Problem wird durch die Struktur selbst gelöst, nicht durch Disziplin.
Die Befehle von npm, pnpm und Yarn im Vergleich
Im Alltag entsprechen sich die Befehle fast eins zu eins. Der Unterschied, der zählt, ist das CI-Flag: genau das installieren, was das Lockfile sagt, nichts auflösen, nichts schreiben.
| Aktion | npm | pnpm | Yarn Berry |
|---|---|---|---|
| Alles installieren | npm install | pnpm install | yarn install |
| Abhängigkeit hinzufügen | npm install axios | pnpm add axios | yarn add axios |
| Dev-Abhängigkeit hinzufügen | npm install -D vitest | pnpm add -D vitest | yarn add -D vitest |
| Skript ausführen | npm run build | pnpm run build | yarn run build |
| CI-sichere Installation | npm ci | pnpm install --frozen-lockfile | yarn install --immutable |
npm ci löscht zunächst node_modules, installiert strikt nach package-lock.json und schlägt fehl, wenn Lockfile und package.json nicht übereinstimmen. --frozen-lockfile und --immutable überspringen das Löschen, erzwingen aber dieselbe Regel: keine neuen Auflösungen, kein Schreiben des Lockfiles, ein Exit-Code ungleich null, wenn das Lockfile so nicht erfüllbar ist. Verwenden Sie die CI-sichere Form in jeder Pipeline und die normale Installation auf Ihrer eigenen Maschine. Ein veraltetes Lockfile soll den Build laut zum Scheitern bringen, nicht still aufgelöst werden.
Wenn Sie Abhängigkeiten innerhalb eines Docker-Images installieren, erklärt Dockerfile Best Practices: Kleinere, schnellere Builds, wie Sie Manifest und Lockfile kopieren und die Installation vor COPY . . ausführen, damit dieser Layer nur neu gebaut wird, wenn sich das Lockfile ändert.
Warum ein Repository nur ein Lockfile committen sollte
npm schreibt package-lock.json, pnpm schreibt pnpm-lock.yaml, und beide Yarn-Generationen schreiben yarn.lock (in Formaten, die sich zwischen v1 und Berry unterscheiden). Jedes erfasst die genaue Version jeder transitiven Abhängigkeit. Keines liest das eines anderen.
So sieht der Bruch aus. Jemand führt yarn add lodash in einem Repository aus, das bis dahin nur package-lock.json gecommittet hatte. Jetzt existiert auch yarn.lock, die beiden Dateien sind sich uneinig, was aufgelöst wurde, und welches Tool die CI zufällig ausführt, gewinnt. Die nächste lokale Installation mit dem anderen Tool löst wieder etwas anderes auf.
Wählen Sie einen Paketmanager pro Repository, committen Sie nur dessen Lockfile, und ignorieren Sie die anderen, bevor jemand sie versehentlich erzeugt:
| |
Den Paketmanager mit corepack festlegen
Eine .gitignore hält niemanden davon ab, das falsche Tool auszuführen, wenn es auf seinem PATH liegt. corepack schon. Es liest das Feld packageManager in package.json und führt genau diese Version von npm, Yarn oder pnpm aus, geladen beim ersten Gebrauch:
| |
Führen Sie corepack enable einmal aus, um die Shims von Yarn und pnpm auf Ihren PATH zu legen (der npm-Shim wird nur installiert, wenn Sie ihn explizit anfordern). Von da an führt pnpm install in diesem Repository auf jeder Maschine die festgelegte Version 10.12.1 aus, und niemand muss daran denken, das zu prüfen.
Corepack ist in Node von 14.19.0 bis ausschließlich 25.0.0 enthalten. Ab Node 25 installieren Sie es mit npm install -g corepack und führen dann wie zuvor corepack enable aus. In CI funktioniert die cache-Option von actions/setup-node ('npm', 'yarn' oder 'pnpm') zusammen mit corepack; Abhängigkeiten in GitHub Actions cachen erklärt die Cache-Keys dahinter.
Wie sich Workspaces in npm, Yarn und pnpm unterscheiden
Alle drei verwalten ein Monorepo aus lokalen, miteinander verlinkten Paketen. Sie unterscheiden sich darin, wo Sie die Mitglieder deklarieren und wie Sie eines davon ansprechen.
npm und Yarn lesen ein workspaces-Array aus der package.json im Root:
| |
Wenn Sie einmal im Root installieren, werden die lokalen Pakete gegenseitig in ihre node_modules verlinkt. Um ein Skript in einem Paket auszuführen, verlangt npm einen Pfad:
| |
Yarn verlangt den Paketnamen:
| |
pnpm ignoriert das Feld workspaces und liest die Mitgliedschaft aus seiner eigenen pnpm-workspace.yaml:
| |
Es adressiert Pakete über den Namen mit --filter:
| |
Ein npm- oder Yarn-Monorepo auf pnpm zu migrieren bedeutet, diese pnpm-workspace.yaml selbst aus den vorhandenen workspaces-Globs zu schreiben. pnpm import kann das alte Lockfile konvertieren, sodass die erste Installation die bereits vorhandenen Versionen behält.
Wenn pnpms strikte Struktur eine Abhängigkeit zerbricht
Die Struktur, die Phantom-Abhängigkeiten in Ihrem Code blockiert, legt auch Pakete offen, die sich auf eine solche Abhängigkeit verlassen haben. Ein Paket, das lodash importiert, ohne es in seinen eigenen dependencies zu listen, funktioniert unter npm, weil lodash auf der obersten Ebene liegt, egal wer es dorthin gebracht hat. Unter pnpm wirft dasselbe require('lodash') Cannot find module.
Die eigentliche Lösung gehört in die package.json dieses Pakets, also eröffnen Sie ein Issue oder schicken Sie einen Patch upstream. In der Zwischenzeit hat pnpm Auswege. Ab pnpm 10.6 liegen sie in pnpm-workspace.yaml; auf einer älteren, festgepinnten Version setzen Sie dieselben Schlüssel mit Bindestrichen in .npmrc (public-hoist-pattern, shamefully-hoist, node-linker). publicHoistPattern hebt nur die Pakete, die auf ein Glob passen, was für ein oder zwei Ausreißer genau richtig ist:
| |
shamefullyHoist: true hebt alles, nahe am Standard-Layout von npm. Nutzen Sie es nur, wenn mehrere unabhängige Pakete allesamt einen flachen Baum voraussetzen, denn Sie geben damit genau den Schutz auf, für den pnpm existiert. nodeLinker: hoisted geht weiter und entfernt die Symlinks ganz, für Tools, die an den Symlinks scheitern und nicht am fehlenden Hoisting. Yarn Berry hat unter PnP dasselbe Problem, und seine Antwort ist nodeLinker: node-modules in .yarnrc.yml.
Welchen Paketmanager Sie für ein neues Projekt wählen sollten
Für ein neues Projekt nehmen Sie pnpm. Eine einzige Kopie jeder Paketversion pro Maschine hält Installationen auf der Platte klein, und die strikte Struktur bringt eine fehlende Abhängigkeit auf Ihrem Laptop zum Vorschein statt Monate später in einem defekten Release. Rechnen Sie damit, in der ersten Woche ein oder zwei Einträge zu publicHoistPattern hinzuzufügen.
Wenn Sie ein npm-Projekt übernehmen, das sauber installiert, und nichts Sie zum Wechsel zwingt, bleiben Sie bei npm. Fügen Sie ein packageManager-Feld hinzu, führen Sie corepack enable aus, und jede Maschine läuft zumindest mit derselben npm-Version.
Wählen Sie Yarn Berry, wenn Sie Zero-Install mit dem in Git gecommitteten Cache wollen. Ohne das bringt der Kompatibilitätspreis von PnP nichts.