Best Practices

Tipps und Helfer für npm run Scripts (1/2)

: Möglichkeiten zur Verbesserung eures Frontend Build-Setups.

Der Zweck dieses zweiteiligen Artikels ist nicht, npm als Build-Tool anzupreisen. Zu diesem Thema gibt bereits viele gute Artikel, in denen sowohl gute Gründe für die Verwendung von npm scripts als auch exemplarische Umsetzungen von bestimmten, gängigen Build-Schritten beschrieben sind. Es geht vielmehr darum, einige vielleicht nicht so offensichtliche Möglichkeiten aufzuzeigen und dadurch potentiell euer Build-Setup zu verbessern.

Wenn ihr jedoch erstmal eine Einführung in die Vorteile der Verwendung von CLI Tools via npm Scripts benötigt und erfahren wollt, welche Vorteile euch das im Gegensatz zu Taskrunnern wie Gulp und deren Plugin Öko-System bietet, empfehle ich den Artikel Why npm Scripts?.

Im heutigen, ersten Teil des Artikels geht es darum, was ihr mit Bordmitteln die euch npm bietet erreichen könnt.

Kurze Einführung

Ihr könnt einfach npm Scripts definieren, in dem ihr sie in die scripts Property in eurer package.json Datei eintragt und sie dann via npm run script-name ausführen. Mit Hilfe des Befehls npm run könnt ihr alle verfügbaren Scripts auflisten.

Binaries von lokal installierten Packages sind automatisch »verlinkt«, so dass sie mit ihrem Namen aufgerufen werden können anstatt über den Pfad node_modules/.bin/package-name.

{
  "name": "my-package",
  "scripts": {
    "lint": "eslint ."
  },
  "devDependencies": {
    "eslint": "^5.16.0"
  }
}
npm run lint

Exit Codes

Wenn ein Command Line Tool, welches man via npm run ausführt mit einem non-zero Exit Code beendet wird, dann bricht auch der npm Prozess mit einen non-zero Exit Code ab. Dies ist zum einen elementar in CI/CD Umgebungen und man kann es sich auch bei der Verwendungen von Lifecycle-Scripts zu nutze machen.

Lifecycle Scripts

npm hat vordefinierte Lifecyle Scripts, die unter bestimmten Voraussetzungen ausgeführt werden sofern sie in eurer package.json definiert sind.

{
  "name": "my-package",
  "scripts": {
    "prepublishOnly": "snyk test",
    "postinstall": "echo 'Installed. Enter `npm run` to see available scripts.'"
  },
  "devDependencies": {
    "snyk": "^1.147.4"
  }
}

prepublishOnly wird automatisch ausgeführt, bevor euer Paket via npm publish in der npm Registry veröffentlicht bzw. aktualisiert wird. In dem konkreten Beispiel werden eure Production Dependencies nach bekannten Schwachstellen durchsucht und im Fall, dass etwas gefunden wird, der Prozeß vor dem Veröffentlichen abgebrochen.

postinstall wird automatisch nach erfolgreicher Installation via npm install ausgeführt und kann bspw. genutzt werden, um Informationen auszugeben, Shell Scripts ausführbar zu machen und Node.js Scripts auszuführen. Eurer Kreativität sind hierbei keine Grenzen gesetzt.

In den npm Docs könnt ihr eine komplette Liste der verfügbaren Lifecycle Scripts einsehen.

»npm start« und »npm test«

Diese gehören ebenfalls in die Kategorie der Lifecycle Scripts, werden jedoch nicht automatisch ausgeführt.

{
  "name": "my-package",
  "scripts": {
    "start": "node server.js",
    "test": "jest"
  },
  "devDependencies": {
    "jest-cli": "^24.7.1"
  }
}

Da es sich hierbei um Lifecycle Scripts handelt können sie ohne run wie folgt aufgerufen werden:

npm test
npm start

»pre« und »post« Scripts

»pre« und »post« Scripts können genutzt werden um Scripts automatisch in einer bestimmten Reihenfolge auszuführen bzw. voneinander abhängig zu machen.

{
  "name": "my-package",
  "scripts": {
    "pretest": "eslint .",
    "test": "jest"
  },
  "devDependencies": {
    "eslint": "^5.16.0",
    "jest-cli": "^24.7.1"
  }
}
npm test

Dies Script linted eure Dateien via ESLint, bevor eure Tests ausgeführt werden. Die Tests werden nicht ausgeführt, wenn Linting-Fehler bestehen – oder allgemeiner ausgedrückt: das nachfolgende Script wird nicht ausgeführt, wenn eines der Scripts in einer Sequenz mit »post« oder »pre« Scripts mit einem non-zero Exit Code beendet wird.

Wichtig: »pre« und »post« Scripts können auch für eigene Custom Scripts genutzt werden. So wird z.B. mit Eingabe von npm run build auch prebuild und postbuild ausgeführt, wenn sie denn definiert sind.

Optionen für npm Scripts

Optionen an verwendete Tools übergeben

Ihr könnt beim Aufruf von npm Scripts dem im jeweiligen Script verwendeten Tool via -- --flag Optionen mitgeben.

{
  "name": "my-package",
  "scripts": {
    "lint": "eslint",
    "lint:fix": "npm run lint -- --fix",
  }
}

Der Aufruf von npm run lint:fix verhält sich also wie wenn man eslint --fix aufrufen würde.

Unnötige Ausgaben im Terminal verringern

npm run verfügt über eine --silent Option, die die Ausgabe im Terminal auf den Output der in npm Scripts verwendeten Tools beschränkt. Dies ist insbesondere sinnvoll wenn npm Scripts mit npm Scripts kombiniert werden.

Z.B. bei folgendem Setup in Sachen JavaScript Linting:

{
  "name": "my-package",
  "scripts": {
    "lint": "xo",
    "lint:fix": "npm run lint --silent -- --fix"
  }
}

Aufruf ohne --silent:

$ npm run lint:fix

> my-package@1.0.0 lint:fix /Users/mkuehnel/temp/my-package
> npm run lint -- --fix

> my-package@1.0.0 lint /Users/mkuehnel/temp/my-package
> xo "--fix"

Aufruf mit --silent:

$ npm run lint:fix

> my-package@1.0.0 lint:fix /Users/mkuehnel/temp/my-package
> npm run lint --silent -- --fix

Auf Twitter findet ihr ein weiteres Beispiel dafür, was die Verwendung von --silent bei komplexeren Build-Setups für einen Unterschied machen kann:

package.json Vars

Alle Properties der package.json Datei werden in Variablen mit dem Präfix npm_package_ gespeichert. Die name Property ist somit als Variable npm_package_name verfügbar.

Verwendung innerhalb der package.json Datei

Diese Variablen lassen sich via $Variablenname referenzieren.
Somit kann man sich das z.B das wiederholte Schreiben der gleichen Pfade sparen. So ließe sich hieraus:

{
  "name": "my-package",
  "scripts": {
    "lint": "eslint ./src/*",
    "test": "jest ./src/*"
  }
}

Folgendes machen:

{
  "name": "my-package",
  "config": {
    "src": "./src/*"
  },
  "scripts": {
    "lint": "eslint $npm_package_config_src",
    "test": "jest $npm_package_config_src"
  }
}

Verwendung innerhalb eigenen Codes

Wenn man Node.js Dateien via npm Scripts ausführt, lassen sich diese Variablen sogar hier nutzen, da sie als Umgebungsvariablen in dem Node.js Prozess verfügbar gemacht werden. Siehe folgendes kleines Beispiel:

package.json:

{
  "name": "my-package",
  "config": {
    "port": "8080"
  },
  "scripts": {
    "start": "node server.js"
  }
}

server.js:

console.log(`Running on port ${process.env.npm_package_config_port}`);

Terminal:

$ npm start
Running on port 8080

Eine weitere Besonderheit bei Verwendung des config Key in der package.json ist, dass sich die Werte der Properties hierunter via npm config bedarfsweise überschreiben lassen:

$ npm config set my-package:port 80
$ npm start
Running on port 80

Das war der erste Teil des Blog Posts »Tipps und Helfer für npm run Scripts«. Im zweiten Teil wird es darum gehen, wie euch weitere Node.js Packages unterstützen können, um noch effektiver mit npm Scripts zu arbeiten.

Michael Kühnel

Frontendentwickler. Ist seit Netscape 4.7.8 im Geschäft und freut sich, dass dank Webstandards seitdem alles besser wird. Folge Michael bei Twitter Twitter oder GitHub.