Ein Workspace mit sechs Projekten und je drei Aufgaben hat 13 Aufgaben (bei uns). Bei jeder Änderung alle zu laufen, wäre in der CI bald zu langsam. Nx löst das mit zwei Mitteln: dem Cache (was schon berechnet wurde, wird nicht neu berechnet) und affected (was von der Änderung nicht berührt ist, wird gar nicht erst angefasst). Die Lektion zeigt beides an dem Workspace aus 0101 und 0102 und skizziert, wie eine Pipeline es nutzt.

Stand der Prüfung. Ausgeführt (Node 24.22.3, Nx 24.0.6, Angular 22.2, Vitest 4.1.12, Wegwerf-Workspace außerhalb des Repositorys): nx run-many -t lint test build ohne und mit Cache, nx affected -t lint test build --base=HEAD~1 --head=HEAD mit --explain und ohne; die Ausgaben stehen so da, wie sie gedruckt wurden (Pfade und Dauer gekürzt). Gelesen: die Nx-Seiten „Cache task results“, „Run only tasks affected by a PR“, „Remote cache“ und das Beispiel für Azure Pipelines (nur als Zusammenfassung des Seitentextes; den YAML-Wortlaut haben wir nicht gesehen). Nicht ausgeführt: jede Pipeline (GitHub Actions, Azure DevOps), Nx Cloud, ein selbst betriebener Cache, nx migrate. Offen: Preise und Bedingungen von Nx Cloud (die Seite nennt keine), die Bedienung eines selbst betriebenen Caches (nur die Seite „Self-hosted remote cache“ ist genannt, nicht gelesen).

Die Aufgabenpipeline

Eine Aufgabe (build, test, lint) hat bei Nx drei Eigenschaften, die für Cache und Affected zählen:

  • inputs: die Dateien und Werte, von denen das Ergebnis abhängt. Sie bilden den Hash. Benannte Eingaben (namedInputs in nx.json) fassen sie zusammen: default meint alles im Projekt, production lässt Testdateien und die Lint-Konfiguration weg.
  • outputs: was die Aufgabe erzeugt und der Cache wiederherstellt (bei build der Ausgabeordner).
  • dependsOn: Aufgaben, die vorher laufen müssen. "^build" heißt: erst build der Projekte, von denen dieses abhängt.

Wie Nx sie für unser Projekt kontakte-ui sieht, zeigt nx show project kontakte-ui --json. Bei uns waren lint und test zwischenspeicherbar (cache: true); die Eingabe von test enthält default, ^production (die Produktionsdateien der Abhängigkeiten), die Datei tsconfig.spec.json, die externe Abhängigkeit vitest und den Wert der Umgebungsvariable CI. Das letzte ist lehrreich: Ein Test, den Sie lokal laufen lassen, hat einen anderen Hash als derselbe Test mit gesetztem CI, und die Ergebnisse teilen sich nicht.

Nach der Seite zum Cache gehören zum Hash, was ein Projekt an Quelldateien und Abhängigkeiten hat, relevante Workspace-Konfiguration, Versionen externer Pakete, Laufzeitwerte wie Betriebssystem und CPU-Architektur und die Argumente des Befehls. Quelltexte selbst speichert Nx nicht, nur ihre Hashes; ein Eintrag enthält die Terminalausgabe, die Dateien aus outputs und den Hash. Ein Cache setzt eine Aufgabe ohne Nebenwirkungen voraus: Ein E2E-Test gegen einen laufenden Server gehört laut Seite nicht hinein.

Den Cache beweisen

Lassen Sie alles einmal laufen und dann ein zweites Mal, ohne etwas zu ändern:

PowerShell
npx nx run-many -t lint test build
npx nx run-many -t lint test build

Der erste Lauf endete bei uns mit Exit 0 und der Zeile „Cache: 0/13 hit (0%)“ (nach einer Änderung von tsconfig.base.json; ein Lauf mit teilweise warmem Cache zeigte vorher „4/13 hit (31%)“). Der zweite endete mit Exit 0 in 61 Millisekunden:

√  nx run kontakte-ui:test  [local cache]
√  nx run kladde-buero:build:production  [local cache]
√  nx run kontakte-ui:lint  [existing outputs match the cache, left as is]

 NX   Successfully ran targets lint, test, build for 6 projects

Nx read the output from the cache instead of running the command for 13 out of 13 tasks.

  Run duration:      61ms
  Cache:             13/13 hit (100%)

Zwei Marken beweisen den Treffer: der Zusatz [local cache] oder [existing outputs match the cache, left as is] an der Aufgabe und die Zusammenfassung „13/13 hit“. Die Nx-Seite beschreibt keine feste Marke; wir lesen sie aus der Ausgabe dieser Fassung. Ein Cache-Treffer ist keine Garantie, dass der Test grün ist, sondern dass er mit denselben Eingaben grün war. Das ist der Sinn, aber auch die Grenze: Was Ihr Test außerhalb der Eingaben liest (Uhrzeit, Netz, Datei außerhalb des Projekts), ändert den Hash nicht.

Wann der Cache fehlt. Wir änderten strict in tsconfig.base.json: danach 0 von 13 Treffern, denn die Wurzelkonfiguration ist Eingabe aller Aufgaben. Wir änderten einen Kommentar in shared-util-datum: nur dessen lint und test liefen neu (Abschnitt unten). nx reset leert Cache und Daten des Daemons; den Ort des lokalen Caches nennt die gelesene Seite nicht, das haben wir nicht ermittelt.

Affected

nx affected -t <Aufgaben> führt nur aus, was eine Änderung berühren kann. Nach der Nx-Seite: Nx fragt Git nach den geänderten Dateien, wählt jede Aufgabe, deren Eingaben eine geänderte Datei enthalten, wählt Aufgaben, die die Ausgaben einer gewählten Aufgabe lesen, und führt sie samt Abhängigkeiten aus (unveränderte Abhängigkeiten kommen aus dem Cache). Die Auswahl hängt von Eingaben ab, nicht vom Aufgabengraphen: Ein dependsOn allein macht eine Aufgabe nicht betroffen. Deshalb sind richtige inputs und outputs auch bei Aufgaben nötig, die nicht gecacht werden.

Ab Nx 24 aufgabenbasiert. Bis Nx 23 löste eine Änderung an einem Projekt alle Aufgaben dieses Projekts und aller seiner Abhängigen aus (projektbasiert). Nx 24 wählt einzelne Aufgaben. Ein Workspace, der mit nx migrate auf 24 gehoben wird, behält laut Seite das alte Verhalten über "affected": { "legacy": true } in nx.json; wer die Einstellung entfernt oder auf false setzt, bekommt das neue. Unser Workspace entstand mit Nx 24 und hatte keine solche Einstellung.

Die Probe. Wir committeten eine kleine Änderung in libs/kontakte/ui (der Name der Karte bekam eine Klasse) und fragten mit --explain, was betroffen wäre; --explain führt nichts aus:

PowerShell
npx nx affected -t lint test build --base=HEAD~1 --head=HEAD --explain
 NX   7 out of 13 lint, test and build tasks are affected

Changing 1 file touches 3 lint tasks, 3 test tasks and 1 build task. Pass --verbose to list each with its reasons.

  libs/kontakte/ui/src/lib/kontakt-karte.ts:
    - kladde-buero:build:production
    - kladde-buero:lint
    - kladde-buero:test
    - kontakte-feature-liste:lint
    - kontakte-feature-liste:test
    - and 1 more lint task and 1 more test task

Ohne --explain liefen genau diese sieben Aufgaben (Exit 0, „Cache: 0/7 hit“, 18,6 Sekunden): kontakte-ui selbst, die Feature-Bibliothek und die Anwendung. Die drei anderen Bibliotheken liefen nicht, termine-data-access, kontakte-data-access und shared-util-datum haben kontakte-ui nicht importiert. Das ist der Gewinn: Der Graph aus Lektion 0102 entscheidet, wer neu läuft.

Zwei weitere Commits: Ein Kommentar in shared-util-datum betraf mit --explain „2 out of 13“ (dessen lint und test; keine Anwendung importiert es bisher). Eine Zeile in der README.md im Wurzelordner ergab „Nothing affected“. Das liegt an den Eingaben: Die README ist in keiner inputs-Liste. Gegenstück: Eine Änderung der Sperrdatei betrifft nach der Seite standardmäßig alle Aufgaben; mit projectsAffectedByDependencyUpdates auf "auto" nur die, deren aufgelöste Abhängigkeiten sich ändern (nicht ausgeführt).

Der Lauf in der CI

In der CI gibt es keinen „aktuellen Zustand der Platte gegen den letzten Commit“, sondern einen Vergleich zweier Commits. Die Seite sagt: Standard für --base ist der Hauptzweig, für --head der Stand des Dateisystems; in der CI geben Sie beides ausdrücklich an (--base=origin/main --head=<Zweig>) oder setzen NX_BASE und NX_HEAD. Empfohlen ist als Basis der letzte erfolgreiche Commit auf dem Hauptzweig, damit alles seit dem letzten grünen Lauf eingeschlossen ist. Voraussetzung ist die ganze Historie: Ohne fetch-depth: 0 (GitHub) beziehungsweise fetchDepth: '0' (Azure DevOps) gibt es keinen Basis-Commit zum Vergleichen.

GitHub Actions (Skizze, nicht ausgeführt). Für GitHub nennt die Seite die Aktion nrwl/nx-set-shas, die NX_BASE und NX_HEAD aus dem letzten erfolgreichen Lauf des Hauptzweigs setzt. Fassungen der Aktionen siehe Seite; welche Eingabe sie für Ihren Hauptzweig braucht, haben wir nicht gelesen: offen.

YAML
steps:
  - uses: actions/checkout@<Fassung>
    with:
      fetch-depth: 0
  - uses: nrwl/nx-set-shas@<Fassung>
  - run: npm ci
  - run: node tools/boundary-probe.mts
  - run: npx nx affected -t lint test build

Azure DevOps (Skizze, nicht ausgeführt). Nach unserer Zusammenfassung des Nx-Beispiels checkt die Pipeline mit fetchDepth: '0' aus, rechnet BASE_SHA und HEAD_SHA aus (bei Pull Requests der Merge-Base mit dem Zielzweig, sonst der letzte erfolgreiche Build, Rückfall HEAD~1) und ruft npx nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) -t lint test build auf. Eigene Skizze dazu, ohne die Berechnung der Basis:

YAML
steps:
  - checkout: self
    fetchDepth: '0'
  - script: npm ci
  - script: node tools/boundary-probe.mts
  - script: npx nx affected --base=$(BASE_SHA) --head=$(HEAD_SHA) -t lint test build

Zwei Dinge gehören in jede Pipeline: der Exit-Code zählt (ein roter Lint-Lauf muss die Pipeline rot machen, sonst sind die Grenzen aus 0102 Wünsche), und die Probe boundary-probe.mts läuft als eigener Schritt. npm ci statt npm install installiert genau das, was die Sperrdatei sagt.

Entfernter Cache

Der lokale Cache liegt auf Ihrer Maschine. In der CI hat jeder Lauf eine frische Maschine, also ist er leer, es sei denn, jemand teilt ihn. Die Seite „Remote cache“ nennt zwei Wege: Nx Cloud (Verbindung mit npx nx@latest connect; teilt Ergebnisse zwischen Entwicklungsrechnern und CI) und einen selbst betriebenen Cache-Server gegen eine OpenAPI-Beschreibung (Seite „Self-hosted remote cache“, nicht gelesen). Welche Speicher dafür möglich sind, nennt die Seite nicht. Preise und Lizenzbedingungen von Nx Cloud stehen dort nicht: offen, vor dem Einsatz bei Nx nachlesen. Beachten Sie, was ein geteilter Cache bedeutet: Ergebnisse, die jemand anderes erzeugt hat, laufen auf Ihrer Maschine ungeprüft; Zugriffsrechte und Vertrauen gehören zur Entscheidung (ein ADR, K4 4.1.3).

Konventionen für Commits

Ein Monorepo mit vielen Projekten profitiert von lesbaren Commits, denn aus ihnen entstehen später Versionen und Änderungsprotokolle (Lektion 0302). Conventional Commits schreiben vor, dass jede Nachricht mit einem Typ beginnt, optional mit Geltungsbereich: feat(kontakte-ui): add card class, fix(shared-util-datum): ..., chore(deps): ..., docs: ..., test: ..., ci: .... Als Bereich bietet sich der Projektname an, den Nx kennt. In den Lektionen K1 bis K3 steht das Thema nicht (der Plan verwies auf eine K2-Lektion, die es nicht gibt); wir erklären es hier nur in diesem Umfang. Die Spezifikation haben wir nicht gelesen: offen.

Übung 1

Einen Cache-Treffer erzeugen und beweisen.

  1. Führen Sie in Ihrem Workspace npx nx run-many -t lint test build aus; Exit-Code lesen.
  2. Führen Sie denselben Befehl noch einmal aus, ohne etwas zu ändern.
  3. Ändern Sie einen Wert in tsconfig.base.json (und lassen Sie ihn geändert; ein Zurücksetzen ergäbe wieder den alten Hash) und führen Sie den Befehl erneut aus.
  4. Notieren Sie je Lauf die Zeile mit „Cache:“ und die Dauer in docs/cache.md.

So prüfen Sie: Lauf 1 hat 0 Treffer oder nur teilweise, Lauf 2 „13/13 hit“ (bei Ihnen die Zahl Ihrer Aufgaben) mit „Nx read the output from the cache“, Lauf 3 wieder 0 Treffer. Alle drei enden mit exit=0.

Lösung 1

  • Lauf 2 hat keine Treffer: Etwas ändert sich zwischen den Läufen (eine Datei wird erzeugt, die inputs einer Aufgabe enthalten sie). nx show project <name> --json zeigt die inputs.
  • Lauf 2 hat Treffer, aber der Test lief nicht „wirklich“: erwartet; er war mit denselben Eingaben schon grün.
  • test meldet beim ersten Lauf Exit 1 „No test files found“: Siehe Lektion 0102 (passWithNoTests).
  • Lauf 3 hat weiter Treffer: Die geänderte Datei ist in keiner Eingabe. tsconfig.base.json ist es bei uns gewesen (0 von 13).

Übung 2

Affected: ein Commit in einer Bibliothek.

  1. Committen Sie den Stand. Ändern Sie dann eine Datei in libs/kontakte/ui/src (zum Beispiel eine Klasse im Template) und committen Sie mit feat(kontakte-ui): add name class.
  2. Führen Sie npx nx affected -t lint test build --base=HEAD~1 --head=HEAD --explain aus und notieren Sie, wie viele von wie vielen Aufgaben betroffen sind und welche Projekte darunter sind.
  3. Führen Sie ohne --explain aus und lesen Sie den Exit-Code.
  4. Ändern Sie danach nur die README.md, committen Sie mit docs: update readme und fragen Sie erneut mit --explain.

So prüfen Sie: Bei Schritt 2 steht kontakte-ui selbst, die Feature-Bibliothek und die Anwendung darunter, aber nicht termine-data-access; Schritt 3 endet mit exit=0; Schritt 4 meldet „Nothing affected“.

Lösung 2

  • Alles ist betroffen: Sie haben eine Datei der Wurzel geändert (zum Beispiel tsconfig.base.json oder die Sperrdatei); die gelten für alle Aufgaben.
  • Nichts ist betroffen, obwohl Sie libs/ änderten: Die Änderung ist nicht committet (--head=HEAD liest den Verlauf), oder --base zeigt auf denselben Commit.
  • Nx meldet Git-Fehler: kein Repository oder kein Commit; in der CI eine zu flache Kopie (fetch-depth: 0).
  • Die Zahlen weichen ab: Sie haben andere Projekte; maßgeblich ist, dass Projekte ohne Weg zur Änderung fehlen.

Was Sie jetzt können

  • Sie lesen inputs, outputs und dependsOn einer Aufgabe und wissen, was den Hash bestimmt (darunter CI bei Tests).
  • Sie beweisen einen Cache-Treffer an der Ausgabe und erklären, wann er ausbleibt.
  • Sie lassen nx affected -t mit --explain zeigen, welche Aufgaben ein Commit auslöst, und kennen den Unterschied zwischen projekt- und aufgabenbasiert (Nx 24).
  • Sie skizzieren eine Pipeline mit vollem Verlauf, expliziter Basis, npm ci und Grenzprobe und wissen, was dort nicht geprüft wurde.
  • Sie ordnen den entfernten Cache ein, benennen die offenen Punkte und schreiben Commits nach Conventional Commits.

Weiterlesen

Die Kladde: nx.json mit ihren namedInputs und eine Pipelineskizze mit vollem Verlauf, affected und der Probe. Damit ist das Modul abgeschlossen. Als Nächstes folgt Lektion 0201 im Modul W10.2: Strategisches DDD und die Kontexte der Kladde.