Ein Blick auf npm und package.json

2 Kommentare

Node, npm und das JavaScript-Ökosystem werden, gerade in der Enterprise-Entwicklung, gerne belächelt. So wurde das Thema z.B. bei @autoweird.fm intensiv diskutiert. Trotzdem führt momentan kein Weg an modernen Web-Anwendungen vorbei. Das Problem: npm und package.json sind für viele eine Blackbox, die ich in diesem Artikel ein wenig demystifizieren möchte.

Projekt-Setup

Fangen wir ganz vorne an: Wenn ein neues Projekt angelegt wird, kann man natürlich eine alte package.json kopieren, selbige von Hand schreiben oder aber es etwas einfacher angehen: Mit npm init (docs.npmjs.com/cli/init) wird ein Dialog gestartet, in dem man folgende Dinge über das eigene Projekt angeben kann:

  • Name des Packages
  • Version
  • Beschreibung
  • Einstiegspunkt
  • Test-Befehl
  • Git Repository
  • Keywords (für die npm-Suche)
  • Autor
  • Lizenz

Hier schlägt npm immer einen sinnvollen Standard vor, und man kann viele dieser Standards einfach akzeptieren. Noch einfacher ist es npm init -y (oder --yes) zu nutzen, damit npm einfach eine package.json mit diesen Standards anlegt. Um zu vermeiden, dass ein Modul versehentlich veröffentlicht wird, kann man dieses in der package.json mit “private”: true entsprechend auszeichnen.

Dependencies

Libraries und Frameworks lassen sich mit npm install <dependency> (docs.npmjs.com/cli/install) installieren. Auch hier gibt es einen kürzeren Befehl: npm i <dependency>.

Neben den normalen Abhängigkeiten erlaubt es npm auch, Dev-Dependencies zu installieren (npm install --save-dev <dependency> oder kurz: npm i -D <dependency>) . Dev-Dependencies sind alle Abhängigkeiten, die nur während der Entwicklung, nicht aber zur Laufzeit benötigt werden.

Beispiele für solche Libraries sind der Testrunner, Buildtools und Linter. Was hier oft vergessen wird: In vielen Frontend-Projekten werden die im Einsatz befindlichen Libraries und Frameworks oft im Rahmen des builds in eine oder mehrere JavaScript-Dateien gepackt. In diesem Fall sind dann auch, streng genommen, React, Angular und Co. Dev-Dependencies. Die richtige Verwendung von Dependencies kann die Größe von npm-Modulen erheblich beeinflussen.

In Produktion

Wenn ihr npm mit dem Flag --production nutzt oder die NODE_ENV-Umgebungsvariable den Wert “production” hat, wird npm keine Dev-Depenencies installieren.

Versionen

Auch wenn einige Libraries sich nicht daran halten: npm setzt stark auf Semantic Versioning und bietet euch zahlreiche Optionen, dies zu nutzen. Trotzdem muss hier klar darauf hingewiesen werden, dass man je nach verwendeter Library vorsichtig mit den Möglichkeiten von npm umgehen muss.

Wer ganz sicher gehen will, trägt immer eine feste Version in die package.json ein. Der Standard bei npm ist ein Zirkumflex vor der Version (^).

Versionen in npm definieren

  • fixe Version, wie z.B. “1.0.1”: In diesem Fall installiert npm auch nur die dort angegebene Version.
  • Versionsbereich, wie z.B. “>1.0.0”: Es ist möglich, hier einen größeren Bereich abzudecken. Die möglichen Vergleichsoperatoren sind “<”, “<=”, “>”, “>=” und “=”.
  • Tags: Es ist möglich, einer Version einen Tag wie “-beta” oder “-alpha.3” zu geben, um Versionen, die nicht stabil sind, zu veröffentlichen.
  • Patchversion, mit z.B “~1.0.0”: In diesem Fall wird die aktuellste minor-Version genutzt. Dies ist in vielen Fällen ein sinnvoller Standard, wenn die im Einsatz befindliche Library semantic versioning korrekt umsetzt.
  • Die erste Zahl (ungleich 0) bleibt stabil mit Zirkumflex: “^1.0.0”: Hier sind also i.d.R. auch Minor Upgrades Teil der unterstützten Version. Bei “^0.0.1” würde sich die Version allerdings nicht verändern, da die erste Zahl, die nicht 0 ist, in diesem Fall die Minor-Version ist.

Dependencies aktualisieren

Gerade während der Entwicklung macht es häufig Sinn, die verwendeten Dependencies auf dem aktuellen Stand zu halten. Mit Hilfe von npm outdated kann man sich eine Liste der Dependencies anzeigen lassen und diese dann mit npm update auch auf die aktuelle Version aktualisieren.

package-lock.json und npm-shrinkwrap.json

Immer wenn über npm der Inhalt des node_modules-Ordner oder die package.json geändert wird, wird die package-lock.json verändert (bzw. erzeugt). In dieser wird der komplette Dependency-Tree aufgeführt, um
die Installation unabhängig von inzwischen aktuelleren Versionen zu reproduzieren. Diese Datei sollte unter Versionskontrolle gehalten werden, damit im Team die gleichen Dependencies verwendet werden. Wichtig hier: Es geht dabei nicht darum, Versionen festzuzurren (dies könnt ihr mit shrinkwrap), sondern vielmehr darum die Arbeit im Team zu vereinfachen.

Wenn zwei Entwickler neue Abhängigkeiten einbringen, kann dies Konflikte in der package-lock.json verursachen. Daher muss hier besonders darauf geachtet werden, dass die package-lock.json nicht einfach blind gemerged oder überschrieben wird, sondern die Updates kontrolliert sequenziell eingepflegt werden. Hier empfiehlt es sich, diese Situationen dazu zu nutzen, generell mit Hilfe von npm outdated die Anwendung auf veraltete Abhängigkeiten zu überprüfen und Patches bzw. Minor Updates einzupflegen. Im Zuge dieser Aktualisierung kann dann auch eine neue, frische, package-lock.json erstellt werden.

Mit npm shrinkwrap wird neben der package-lock.json eine npm-shrinkwrap.json angelegt. Diese entspricht inhaltlich der package-lock.json, wird aber immer der package-lock.json vorgezogen. Wenn diese Datei mit dem Modul veröffentlicht wird, stellt dies sicher, dass die dort aufgeführten Dependencies unverändert bleiben. Nützlich ist dies vor allem bei globalen Libraries wie Kommandozeilen-Tools. Bei Libraries und Frameworks, bei denen Nutzer die transitiven Abhängigkeiten noch selber pflegen können sollen, sollte auf die Datei verzichtet werden.

Skripte

Das npm CLI kann auch als Taskrunner verwendet werden. Dazu können in der package.json Skripte (docs.npmjs.com/misc/scripts) definiert werden, die über npm ausführbar sind. Über diese ist es auch möglich, Hooks für Lifecycle Events zu definieren, die npm von sich aus mitbringt (z.B. mit Skripten für preinstall oder postinstall).

Lifecycle events

  • publish
  • install
  • uninstall
  • version
  • test
  • stop
  • start
  • restart

Bei all diesen Lifecycle Events können noch “pre” oder “post” vorangestellt werden. Ebenso ist es möglich, npm-Skripte in anderen Skripten wiederzuverwenden. Ein Beispiel zu npm version sieht dann so aus:

"scripts": {
  "preversion": "npm test",
  "version": "npm run build && git add -A dist",
  "postversion": "git push && git push --tags && rm -rf build/temp"
}

Ausgeführt werden die Skripte dann mit npm <lifescycleEvent> oder auch mit npm run <myOwnScript>. Es ist nämlich durchaus möglich, hier eigene Skripte zu definieren:

"scripts": {
  "build": "webpack"
}

Dieses Skript könnte man dann mit npm run build starten.

Zusätzlich erweitert npm die PATH-Umgebungsvariable für die in der package.json definierten Skripte um node_modules/.bin, hier werden alle ausführbaren Skripte gelinkt. Dadurch können in der package.json auch Skripte wie “lint”: “eslint” definiert werden.

Durch dieses Feature muss so gut wie nie eine Library global installiert werden, was früher durchaus bei Lintern oder anderen Tools notwendig war.

Semantic Versioning

Sehr oft beobachtet man, dass Versionen in der package.json manuell geändert werden. Wenn das eigene Modul allerdings Semantic Versioning richtig folgen möchte, lohnt sich ein Blick auf npm version (docs.npmjs.com/cli/version). Dieser Befehl überlässt npm die Aktualisierung der Version. Dabei ist es möglich, sowohl eine feste Version anzugeben, z.B. npm version 1.0.0 aber auch, npm die Erhöhung der Version zu überlassen (z.B. npm version patch). Valide Parameter hier sind: patch, minor, major, prepatch, preminor, premajor, prerelease oder from-git. Hierbei nutzt from-git den aktuellen Tag, während die anderen Parameter die Version entsprechend erhöhen.

Die Option -m erlaubt es, eine Commit-Message hinzuzufügen (npm version patch -m “Updated module to version %s” – %s wird durch die neue Version ersetzt).

Die folgenden Schritte werden von npm version ausgeführt:

  • Überprüfung, ob alle Dateien im Verzeichnis hinzugefügt/committet sind
  • Führe das preversion-Skript, wenn vorhanden, durch
  • Erhöhe die Version in der package.json
  • Führe das version-Skript, wenn vorhanden, durch
  • commit und tag
  • Führe das postversion-Skript, wenn vorhanden, durch

Und viel mehr

Und natürlich bietet npm hier wesentlich mehr. Die Dokumentation von npm ist sehr umfangreich und bietet auch zahlreiche Beispiele. Es lohnt sich hier definitiv, einen genaueren Blick auf diese zu werfen, um npm im eigenen Projekt optimal zu nutzen.

Daniel Mies

Daniel ist als Entwickler und IT Consultant sowohl im Backend als auch im Frontend unterwegs. Seine Leidenschaft gilt modernen Web-Anwendungen.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • Roman Klauser

    15. September 2017 von Roman Klauser

    „npm und package.json sind für viele eine Blackbox“ 😉

    Schöner Überblick.

  • Jonas Verhoelen

    15. September 2017 von Jonas Verhoelen

    Sehr schön, da sind einige Kleinigkeiten dabei, die ich noch nicht kannte. Wird bestimmt mal hilfreich sein 🙂

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.