Was steht so in der package.json

Vor einiger Zeit habe ich eine Einführung in ein Standard-Frontend-Projekt gegeben und dabei mit Blick auf die package.json einige Dinge etwas vereinfacht. In diesem Beitrag möchte ich daher einige dieser Details genauer zeigen. Übrigens: zu diesem Thema habe ich vor ein paar Jahren auch etwas im Blog der codecentric geschrieben. Der Artikel ist größtenteils noch aktuell und wird durch diesen Beitrag ergänzt.

Fangen wir mit der package.json an: Ich finde ziemlich gut, dass man hier in der JavaScript-Welt einen Quasi-Standard hat und somit einen zentralen Einstiegspunkt. Es ist an vielen Stellen egal, mit welchem Tool man hier arbeitet, da alle die package.json unterstützen.

Hintergrund hier ist, dass npm das wahrscheinlich größte Repository im JavaScript Ökosystem anbietet und am Ende alle Tools damit interagieren müssen.

In diesem Artikel nutze ich npm, die Dinge, die ich hier beschreibe, sind aber auch mit den anderen Tools möglich.

Erstellung einer package.json

Wer eine package.json anlegen möchte, kann dies über das Terminal tun:

mkdir my-app
cd my-app
npm init -y

Ich erspare mir hier in der Regel den Wizard, den man über npm init starten kann und erzeuge die ganze Datei direkt (Option -y). Diese beinhaltet dann einige Meta-Informationen, die ich im Anschluss im Editor anpasse (Lizenz und Skripte):

{
"name": "my-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

version

Die Version des Moduls, die gerade dann wichtig ist, wenn andere das Modul als Dependency verwenden sollen. Bei so mancher Anwendung bleibt sie aus diesem Grund stabil bei 1.0.0, ich selber finde es aber auch ganz gut, diese aktuell zu halten. Über npm version (Dokumentation) kann man diese übrigens bequem aktualisieren (und gleich einen Tag mit erzeugen).

Syntax:

npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]

main

Besonders bei Bibliotheken ist der Einstiegspunkt des Moduls wichtig, der beim Import durch ein anderes Modul verwendet wird. Dieser wird im Feld main festgelegt und hat als Wert den Pfad zu einer JavaScript Datei, die die Funktionalität des Moduls bereitstellt.

"main": "./src/index.js",

The "main" field is a module ID that is the primary entry point to your program. That is, if your package is named foo, and a user installs it, and then does require("foo"), then your "main" module's exports object will be returned.
This should be a module relative to the root of your package folder.
For most modules, it makes the most sense to have a "main" script and often not much else.
If "main" is not set, it defaults to index.js in the packages root folder.

-- npm Dokumentation

Skripte

Ein weiterer wichtiger Block ist der scripts Block, da sich dort in der Regel alle Skripte finden lassen, mit denen die Anwendung gestartet, getestet und gebaut wird. Der scripts Block ist ein Objekt wobei jeweils der key den Namen des Scripts festlegt und der jeweilige Wert wird entsprechend ausgeführt.

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

In diesem Beispiel kann dann mit npm run build webpack gestartet werden.

Hier unterschiden wir zwischen eigenen Skripten und Lifecycle Events, die npm von Hause aus unterstützt (und wo wir uns das run im Aufruf sparen können):

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

So kann sich in einer package.json also unter anderem ein Skript test befinden, welches dann das Testframework (hier jest) ausführt.

"scripts": {
"test": "jest"
},

Soweit also kein wesentlicher Unterschied zu den eigenen Skripten, bei den Lifecycle Events können aber auch noch “pre” und “post” vorangestellt werden. Ebenso ist es möglich, npm-Skripte in anderen Skripten wiederzuverwenden.

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

Die Bibliotheken in meinen Skripten kann ich verwenden, wenn sie in den Dependencies vorhanden sind, da npm diese automatisch zum PATH hinzufügt, wenn es aufgerufen wird.

Daher sollten auch alle Tools, die zum Bauen des Projekts benötigt werden, in den Dependencies verwaltet werden.

Dependencies

Und natürlich gibt es mehrere Möglichkeiten Dependencies in der package.json zu definieren:

  • dependencies
  • devDependencies
  • peerDependencies
  • peerDependenciesMeta
  • bundledDependencies
  • optionalDependencies

In den meisten Projekten, die ich kenne, werden dependencies, devDependencies und vereinzelt peerDependencies eingesetzt.

Dependencies sind die Abhängigkeiten, die das Projekt zur Laufzeit benötigt ([https://docs.npmjs.com/cli/v7/configuring-npm/package-json#dependencies]).

Bei devDependencies handelt es sich um alle Abhängigkeiten, die während der Entwicklung, aber nicht während der Laufzeit, benötigt werden (z. B. Tools für das Testing oder den Build).

Bei peerDependencies handelt es sich um Abhängigkeiten, die das Modul benötigt, um genutzt zu werden, aber nicht selbst zur Verfügung stellt. Das sieht man oft bei Plug-ins für andere Bibliotheken, die die eigentliche Bibliothek voraussetzen, sie aber nicht mitliefern.

Definition von dependencies

In den meisten Fällen installiert man Dependencies mit npm install <dependency>. Dies installiert die Dependency im aktuellen Projekt und ergänzt sie in der package.json. Wenn man eine devDependency installieren möchte, geht dies über npm install <dependency> --save-dev oder, kürzer, npm install <dependency> -D.

Sollte man eine Abhängigkeit global benötigen, geht dies analog mit --global oder -g. Diese liegen dann im PATH und können dann überall verwendet werden.

In den meisten Fällen wird eine Dependency mit ihrer Version (dazu gleich mehr) in die package.json geschrieben.

devDepedencies:{
"@11ty/eleventy": "1.0.2"
}

Es gibt allerdings auch ein paar Alternativen, die möglich sind:

Git URLs

Die offizielle Doku verweist auf folgendes Pattern, wenn man eine Dependency über eine git URL installieren möchte: <protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]

Im Hintergrund versucht npm dann dieses Projekt auszuchecken und das build-Skript zu starten. Auch wenn das cool aussieht, habe ich das bisher noch nie in einer produktiven Anwendung gesehen.

Bei GitHub URLs ist das sogar etwas einfacher gelöst da man einfach user/project angeben kann:

"devDependencies": {
"@11ty/eleventy": "11ty/eleventy",
}

Auch dies ist mir in freier Wildbahn noch nie begegnet und es sollte sehr bewusst eingesetzt werden, da man in diesem Beispiel dann die aktuellste, womöglich noch nicht veröffentlichte, Version der Dependency bekommt. Außerdem dauert, dass ganze natürlich auch etwas länger, da im Hintergrund das Projekt gebaut werden muss.

Pfade

Statt einer Version kann man auch einen Pfad zu einem Projekt angeben.

dependencies:{
"foo": "../foo"
}

Das ist für Testzwecke manchmal ganz nützlich, würde ich in größeren Projekten allerdings nicht machen. Hier bietet sich dann eher eine Lösung à la Workspaces an.

Wenn es nur darum geht, eine lokal entwickelte Bibliothek in einem Projekt zu testen, ist npm link (Dokumentation) meiner Meinung nach die bessere Wahl.

Versionen

Glücklicherweise verstehen die meisten Projekte im npm Kosmos inzwischen Semantic Versioning und daher kann man sich oft darauf verlassen. Dies liegt sicher auch daran, dass npm dies von Hause aus aktiv durch Skripte (npm version) unterstützt.

Wie immer gilt: sicher ist man nur, wenn man die Version fix, also auch ohne Zirkumflex (^) vor der Version, angibt.

Versionen kann man in seiner package.json auf folgende Weisen definieren:

  1. Fixe Versionen, wie 1.0.0. In diesem Fall installiert npm auch nur exakt diese Version
  2. Versionsbereiche wie >1.0.0. Womit sich größere Versionsbereiche abdecken lassen. Die möglichen Vergleichsoperatoren sind “<”, “<=”, “>”, “>=” und “=”.
  3. Tags wie -beta oder -alpha-3 erlauben es Versionen zu definieren, die noch nicht stabil sind.
  4. Patchversionen wie ~1.0.0. In diesem Fall nimmt npm immer die aktuellste minor Version.
  5. Bei der Definition mit Zirkumflex (^) bleibt die erste Zahl, die keine 0 in der Version ist, stabil. Mit ^1.0.0 ist also jede Version >= 1.0.0 und < 2.0.0 abgedeckt. Bei ^0.0.1 bleibt die Version stabil, da es hier keine andere passende Version gibt.

Tools in der package.json

Viele Tools erlauben es ihre Konfiguration in der package.json abzulegen oder in einer separaten Datei im Projekt. Ich bevorzuge hier den zweiten Weg, damit meine package.json nicht so überfüllt ist und man bei einem Blick auf den root Ordner des Projekts schon sehen kann, welches Tooling verwendet wird.

Und natürlich gibt es hier noch mehr zu entdecken und einige Dinge hängen auch sehr vom jeweiligen Projekt ab. Ich hoffe, der Einblick macht die Orientierung in der package.json etwas einfacher. Fehlt Euch hier noch etwas oder habt ihr weitere Fragen? Schreibt mir gerne auf Mastodon oder Twitter.