npm-Befehle für den Fall, dass etwas schiefläuft
Nutzen Sie npm ls, npm explain, overrides und npm ci, um unerwartete Abhängigkeiten zu finden, Versionen zu korrigieren und Lockfile-Drift zu vermeiden.
Wenn ein Paket in node_modules auftaucht, das nichts in package.json angefordert hat, oder in einer Version, die Sie nicht festgelegt haben, führen Sie npm ls <package> aus, um zu sehen, wo es sitzt, und npm explain <package>, um zu sehen, welche Abhängigkeit es hereingezogen hat – bevor Sie irgendetwas anfassen.
Jede Entwicklerin und jeder Entwickler kennt den Moment, in dem man auf eine Versionsnummer in node_modules starrt und denkt: Wo kommst du eigentlich her? Der Reflex ist vertraut: Irgendetwas sieht im Baum falsch aus, also rm -rf node_modules, neu installieren und hoffen. Manchmal verschwindet das Problem. Häufiger kommt es sofort zurück, weil der Installer denselben Baum aus denselben Eingaben neu aufgebaut hat – und jetzt haben Sie keine Ahnung mehr, was sich verändert hat.
Dieser Artikel führt durch eine einzelne Untersuchung: ein unerwartetes Paket oder eine unerwartete Version, zurückverfolgt bis zu der Abhängigkeit, die es angefordert hat, und anschließend auf der richtigen Ebene behoben. Fehler zur Installationszeit wie ERESOLVE, EACCES und Fehler beim Bauen nativer Module werden an anderer Stelle in diesem Blog behandelt, in den Leitfäden zum Beheben von ERESOLVE-Konflikten, zu EACCES-Berechtigungsfehlern und zu node-gyp-Build-Fehlern. Dieser Beitrag ist für die Fälle, in denen noch gar kein Fehler geworfen wurde.
Die wichtigsten Erkenntnisse
npm ls <package>zeigt jede Stelle, an der ein Paket im installierten Baum auftaucht, samt der Version an jeder Position;npm explain <package>zeigt die Kette von Abhängigkeiten, die es angefordert haben.- Ohne
--alllistetnpm lsnur Ihre direkten Abhängigkeiten auf; mit--allgibt es den vollständigen Baum aus, und--depth=<n>setzt eine explizite Grenze zwischen diesen beiden Extremen. npm whyist ein Alias fürnpm explain, sodass dasselbe Wort in npm, pnpm und yarn funktioniert.- Das Feld
overridesinpackage.jsonerzwingt eine bestimmte Version einer verschachtelten Abhängigkeit, unabhängig von dem Bereich, den das übergeordnete Paket angefordert hat – genau deshalb sollte man zuerst versuchen, das übergeordnete Paket zu aktualisieren. npm cisetzt eine vorhandenepackage-lock.jsonvoraus, löschtnode_modules, installiert exakt das, was die Lockfile vorgibt, und beendet sich mit einem Fehler, wenn Lockfile undpackage.jsonnicht zusammenpassen.
Warum vernichtet das Löschen von node_modules die Beweise?
Wenn Sie node_modules löschen und neu installieren, entfernen Sie die einzige Aufzeichnung darüber, wie ein unerwartetes Paket in Ihr Projekt gelangt ist. Der installierte Baum und package-lock.json kodieren zusammen jede Auflösungsentscheidung, die npm getroffen hat: welches übergeordnete Paket welchen Bereich angefordert hat, welche Version diesen erfüllt hat und wo das Ergebnis auf der Festplatte gelandet ist.
Eine Neuinstallation spielt diese Entscheidungen aus package.json und der Lockfile erneut ab. Wenn sich die Eingaben nicht geändert haben, erhalten Sie denselben Baum und dieselbe Überraschung. Wenn sie sich geändert haben (ein Konfigurationsflag, eine Registry, eine Anpassung eines Bereichs), überschreibt die Neuinstallation genau den Zustand, mit dem Sie hätten vergleichen müssen. In beiden Fällen gilt: Lesen Sie den Baum, bevor Sie ihn neu aufbauen. Die beiden Befehle, die ihn lesen, sind npm ls und npm explain.
npm ls: Wo liegt das Paket und in welcher Version?
npm ls <package> filtert den installierten Baum auf die Pfade, die beim genannten Paket enden, und gibt jede Position als name@version aus, mit den übergeordneten Paketen darüber eingerückt. Sie können zusätzlich nach einem Versionsbereich filtern, etwa mit npm ls semver@^6, wenn Sie nur an den Kopien innerhalb einer bestimmten Major-Version interessiert sind.
# Every copy of semver, with the path down to each
npm ls semver
# The complete tree, not just direct dependencies
npm ls --all
# Cap the walk at two levels
npm ls --all --depth=2
# Only what ships to production
npm ls --all --omit=dev
Die Einstellung depth steht standardmäßig auf 0, sofern --all nicht übergeben wird – in diesem Fall wird sie zu Infinity. Dieser Standardwert gilt für ein blankes npm ls ohne Paketargument. Sobald Sie ein Paket benennen, folgt npm dem Pfad zu jeder Kopie, unabhängig von der Tiefe. Genau deshalb zeigt das Beispiel npm ls promzard in der Dokumentation einen verschachtelten Treffer ohne --all; übergeben Sie --depth=<n> explizit, wenn Sie diesen Durchlauf begrenzen möchten.
Was npm ausgibt, ist eine Karte davon, welches Paket von welchem abhängt – sie entspricht also nicht der tatsächlichen Ordnerstruktur auf der Festplatte: Ein dedupliziertes Paket erscheint unter jedem übergeordneten Paket, das es benötigt, und nicht nur an der einen Stelle, an der seine Dateien tatsächlich liegen. Die Ausgabe kennzeichnet außerdem Pakete, die überzählig (installiert, aber nicht deklariert) oder fehlend sind oder in einer Version vorliegen, die den deklarierten Bereich nicht erfüllt; fehlende Pakete erscheinen mit der Kennzeichnung UNMET DEPENDENCY. Mit --package-lock-only meldet npm den Baum, den die Lockfile erzeugen würde, und ignoriert dabei, was node_modules aktuell enthält.
Zwei Hinweise zur Schreibweise. Die aktuellen Filter lauten --omit=dev und --include=dev; --production ist ein veralteter Alias für --omit=dev und --dev ein veralteter Alias für --include=dev, während --development überhaupt keine dokumentierte Option ist. Außerdem beendet sich npm ls mit einem Exit-Code ungleich null, wenn ein Paket fehlt, in einer ungültigen Version vorliegt oder wenn ein benanntes Paket auf nichts zutrifft – das macht den Befehl als CI-Prüfung nutzbar; überzählige Pakete allein führen dagegen nicht zum Fehlschlag.
npm explain: Wer hat das Paket angefordert?
npm explain <package> gibt für jede installierte Kopie die Kette von Abhängigkeitsdeklarationen aus, die dazu geführt haben, dass sie vorhanden ist, und arbeitet sich dabei nach oben bis zum Wurzelprojekt vor. Während npm ls die Frage „wo” beantwortet, beantwortet npm explain die Frage „wer”.
npm explain semver
npm why semver # identical
npm explain semver --json # for jq
Jeder Block der Ausgabe beginnt mit der aufgelösten name@version und dem zugehörigen node_modules-Pfad, danach folgt pro Sprung eine eingerückte Zeile: der Bereich, den ein übergeordnetes Paket deklariert hat, dessen eigene Version und dessen Pfad – abgeschlossen mit einer Zeile, die das Wurzelprojekt benennt. Lesen Sie sie von unten nach oben, um von Ihrer package.json bis zu der Kopie zu gelangen, die Sie nicht erwartet haben. Für duplizierte Pakete gibt es je Kopie einen Block, sodass widersprüchliche Bereiche direkt nebeneinander sichtbar werden. Sie können auch einen Ordner übergeben, etwa npm explain node_modules/foo/node_modules/semver, um genau eine verschachtelte Kopie zu erklären.
Die Synopsis von npm explain führt why als Alias auf, und die anderen großen Paketmanager verwenden dasselbe Verb.
| Paketmanager | Befehl | Form der Ausgabe |
|---|---|---|
| npm | npm explain <pkg> oder npm why <pkg> | Ein Block pro installierter Kopie, Kette bis zur Wurzel |
| pnpm | pnpm why <pkg> | Ein auf den Kopf gestellter Baum, mit dem abgefragten Paket an der Spitze |
| Yarn | yarn why <pkg> | Begründungen pro Workspace, akzeptiert pkg@range |
Sollten Sie das übergeordnete Paket aktualisieren oder ein Override hinzufügen?
Sobald npm explain das übergeordnete Paket benennt, das den problematischen Bereich angefordert hat, besteht die erste Korrektur darin, dieses Paket auf ein Release zu heben, das einen besseren Bereich anfordert. Führen Sie npm outdated <parent> aus, um zu prüfen, ob eine neuere Version existiert, oder lesen Sie die package.json des übergeordneten Pakets in der Registry mit npm view <parent>@latest dependencies. Wenn ein neueres übergeordnetes Paket einen akzeptablen Bereich deklariert, aktualisieren Sie es und lassen npm das Kindpaket neu auflösen.
Erst wenn kein Release des übergeordneten Pakets den Bereich korrigiert, sollten Sie zu overrides greifen:
{
"overrides": {
"semver": "^7.5.4"
}
}
Ein Override ersetzt die Version der verschachtelten Abhängigkeit unabhängig von dem Bereich, den das übergeordnete Paket deklariert hat – das übergeordnete Paket läuft danach möglicherweise gegen eine Version, mit der es nie getestet wurde. Das ist der Kompromiss, und genau deshalb ist overrides der zweite und nicht der erste Zug. Ein paar Regeln aus der Dokumentation: Overrides werden nur in der package.json des Wurzelprojekts berücksichtigt; ein Paket, von dem Sie direkt abhängen, kann nur mit einer Spezifikation überschrieben werden, die mit seiner eigenen identisch ist, sonst wirft npm EOVERRIDE – für diesen Fall existiert die Referenzform $name; und Werte können eine exakte Version, ein Bereich, ein Dist-Tag oder ein npm:-, file:- oder Git-Spezifikator sein. Ordnen Sie das Override unter dem Namen des übergeordneten Pakets ein, wenn es nur für einen Zweig des Baums gelten soll statt überall.
npm config list: Einstellungen, die Sie längst vergessen haben
npm config list gibt die Einstellungen aus, die Sie, Ihre Umgebung oder eine .npmrc-Datei gesetzt haben; npm config list -l gibt zusätzlich die Standardwerte von npm aus, und --json liefert dieselben Daten als JSON. Wenn sich ein Baum auf eine Weise auflöst, die package.json allein nicht erklären kann, ist die Ursache oft ein Konfigurationswert, an dessen Eintragung sich niemand erinnert.
npm config list
npm config list -l
Die Ausgabe ist nach Quelle gruppiert (Kommandozeile, Umgebung, Projekt-.npmrc, Benutzer-.npmrc, global), was Ihnen zeigt, welche Datei Sie bearbeiten müssen. Zwei Schlüssel verdienen dabei zuerst Beachtung. Eine vom Standard abweichende registry bedeutet, dass Versionen gegen einen Mirror oder eine private Registry aufgelöst wurden, deren Inhalt der öffentlichen hinterherhinken kann. Eine gespeicherte Einstellung legacy-peer-deps weist npm an, den Baum völlig ohne Berücksichtigung von peerDependencies aufzubauen – so wie es sich bis Version 6 verhalten hat –, sodass Kombinationen entstehen können, die der aktuelle Resolver abgelehnt hätte. Das hat einen Folgeeffekt: Sobald eine Lockfile mit diesem Flag erstellt wurde, benötigt jedes spätere npm ci es ebenfalls, sonst bricht die Installation ab. Eine einzige vergessene Zeile in einer Projekt-.npmrc kann sowohl einen merkwürdigen lokalen Baum als auch einen roten CI-Lauf erklären.
npm ci vs. npm install: Was passiert, wenn die Lockfile nicht passt?
Wenn die Lockfile die package.json erfüllt, verwendet npm install die exakten Versionen aus der Lockfile; wenn nicht, löst npm install neu auf und aktualisiert package-lock.json. npm ci bricht stattdessen mit einem Fehler ab.
| Verhalten | npm install | npm ci |
|---|---|---|
Setzt package-lock.json voraus | Nein | Ja |
Lockfile und package.json passen nicht zusammen | Löst neu auf, schreibt Lockfile neu | Bricht mit Fehler ab |
Vorhandene node_modules | Werden weiterverwendet | Werden zuerst entfernt |
Schreibt package.json oder Lockfile | Ja | Niemals |
| Einzelnes Paket hinzufügen | Ja | Nein |
Die npm-install-Dokumentation ist bei der Rangfolge eindeutig: Die Bereiche in package.json sind die maßgebliche Quelle, und die Lockfile behält ihre festgeschriebenen Versionen nur so lange bei, wie sie noch in diese Bereiche passen. Genau dieses Verhalten will man in der CI nicht, wo eine still umgeschriebene Lockfile genau die Abweichung verbirgt, die man aufspüren möchte. npm ci weigert sich, die beiden Dateien in Einklang zu bringen, und schlägt lautstark fehl – verwenden Sie es daher in Pipelines und heben Sie sich npm install für den Rechner auf, auf dem Sie Abhängigkeiten bewusst ändern wollen.
Fazit
Ein unerwartetes Paket im Baum ist eine Auflösungsentscheidung mit Papierspur, und npm ls zusammen mit npm explain liest diese Spur, ohne sie zu stören. Verfolgen Sie die Kette bis zu dem übergeordneten Paket, das den Bereich deklariert hat, korrigieren Sie das übergeordnete Paket, wenn es ein besseres Release gibt, greifen Sie nur andernfalls zu einem Override und prüfen Sie anschließend mit npm config list, ob Einstellungen die Auflösung von vornherein verzerrt haben. Führen Sie in der CI npm ci aus, damit die nächste Abweichung den Build zum Scheitern bringt, statt still die Lockfile umzuschreiben.
FAQs
Was bedeutet 'deduped' neben einem Paket in der Ausgabe von npm ls?
Die Kennzeichnung 'deduped' bedeutet, dass npm ls das Paket an dieser Stelle im logischen Abhängigkeitsgraphen anzeigt, dort aber keine separate Kopie existiert: Eine einzelne installierte Kopie weiter oben in node_modules erfüllt den Bereich dieses übergeordneten Pakets. Das ist kein Fehler. Da npm ls den logischen Baum ausgibt, erscheint dasselbe Paket unter jedem übergeordneten Paket, das es benötigt, und nur die Zeile ohne Kennzeichnung entspricht einem physischen Ordner.
Wie entferne ich Pakete, die npm ls als überzählig meldet?
Führen Sie npm prune aus. Der Befehl löscht alles in node_modules, von dem nichts anderes abhängt; benennen Sie ein oder mehrere Pakete, um ihn darauf zu beschränken. Mit --omit=dev oder mit NODE_ENV auf production werden auch Ihre devDependencies entfernt. Verwenden Sie --dry-run, um den Plan vorab zu sehen, und --json, um die Änderungen als JSON zurückzuerhalten. Installationen räumen überzählige Pakete ohnehin selbst auf, Sie benötigen das also meist nur nach einem Absturz oder einer halb abgeschlossenen Installation.
Behebt npm dedupe doppelte Versionen, die npm ls anzeigt, oder brauche ich overrides?
npm dedupe fasst nur Kopien zusammen, die die deklarierten Bereiche ohnehin zulassen. Der Befehl durchläuft den Baum und hebt jede Abhängigkeit so weit nach oben wie möglich, sodass übergeordnete Pakete mit überlappenden Bereichen am Ende eine einzige Kopie teilen; dabei wird nie etwas Neues aus der Registry geholt. Wenn zwei übergeordnete Pakete Bereiche anfordern, die keine gemeinsame Version haben, bleiben beide Kopien bestehen, und die Lösung besteht darin, ein übergeordnetes Paket zu aktualisieren oder einen overrides-Eintrag hinzuzufügen. npm find-dupes führt denselben Durchlauf als Trockenlauf aus, sodass Sie das Ergebnis vorab sehen können.
Wie liste ich global installierte npm-Pakete auf?
Führen Sie npm ls -g aus. Das Flag --global richtet npm ls auf das globale Prefix und listet die dort installierten Pakete statt derjenigen im aktuellen Projekt auf. Es gelten dieselben Tiefenregeln: Ohne --all werden nur die globalen Top-Level-Pakete ausgegeben, und npm ls -g --all expandiert jedes davon zu seinem vollständigen Abhängigkeitsbaum. Fügen Sie einen expliziten --depth-Wert hinzu, um den Durchlauf zu begrenzen, oder --json für maschinenlesbare Ausgabe.