Ein Workspace ohne Regeln ist ein Ordner, in dem jede Datei jede andere importieren darf. Diese Lektion legt die ersten Bibliotheken der Kladde an, gibt ihnen Etiketten und lässt den Lint-Lauf scheitern, wenn jemand eine Grenze überschreitet. Dazu kommen ein Blick auf Sheriff und ein Test, der die Regeln selbst prüft. Was ein Schnitt nach Fachlichkeit ist und wie man ihn findet, lehrt W10.2 (und K4 Lektion 4.2.4); hier geht es um das Handwerk, Grenzen durchzusetzen. Der Workspace aus Lektion 0101 wird vorausgesetzt.

Stand der Prüfung. Ausgeführt (Node 24.22.3, Nx 24.0.6, Angular 22.2, Vitest 4.1.12): fünf Bibliotheken mit @nx/angular:library, Etiketten und Regeln, drei absichtliche Verstöße mit dem echten Lint-Ergebnis (unten, Pfade gekürzt), der Lauf ohne Verstöße (Exit 0), die Probe boundary-probe.mts samt Gegenprobe. Sheriff: npm view und eine Installation in einem Wegwerf-Projekt mit init, list, verify; gelesen wurden sheriff.softarc.io, die Projektseite bei GitHub und ein Beitrag von angulararchitects.io. Nicht ausgeführt: Sheriffs ESLint-Plugin, Sheriff im Nx-Workspace. Seine Dokumentationsseite lieferte unter der versuchten Adresse 404: offen. Die Formen ContactSummary und AppointmentResponse folgen dem Vertrag des Plans; ob K2 Felder anders benennt, prüfen Sie dort (K2 2.3, 2.12).

Vier Arten und zwei Achsen

Nx nennt auf seiner Seite zur Ordnerstruktur (gelesen) vier Arten von Bibliotheken:

Art Etikett Inhalt Darf abhängen von
Feature type:feature Komponenten, die einen Anwendungsfall tragen (Seite, Routing) jeder Art
UI type:ui darstellende Komponenten ohne Anwendungszustand UI, Hilfsfunktionen
Datenzugriff type:data-access API-Zugriff, Zustand, Fachlogik des Clients Datenzugriff, Hilfsfunktionen
Hilfsfunktionen type:util reine Funktionen, Typen, Helfer nur Hilfsfunktionen

Nx nennt die Tabelle einen Ausgangspunkt. Die zweite Achse ist das Fachgebiet (scope:). Für die Kladde: scope:kontakte, scope:termine, scope:ausschreibungen und scope:shared für das wirklich Gemeinsame. Anwendungen bekommen type:app und scope:buero beziehungsweise scope:buchung. Jedes Projekt trägt genau zwei Etiketten, Art und Fachgebiet; die Namen folgen <fachgebiet>-<art>-<name>: kontakte-feature-liste, kontakte-data-access, kontakte-ui, termine-data-access, shared-util-datum.

Bibliotheken anlegen

Ein Aufruf je Bibliothek. Die Etiketten stehen mit Komma in einem Argument; in PowerShell in Anführungszeichen, sonst meldete Nx bei uns „Schema does not support positional arguments“:

PowerShell
npx nx generate @nx/angular:library libs/kontakte/feature-liste `
    --name=kontakte-feature-liste --prefix=kl `
    --importPath=@kladde/kontakte/feature-liste `
    "--tags=scope:kontakte,type:feature" --style=scss --no-interactive

Dasselbe für libs/kontakte/data-access, libs/kontakte/ui, libs/termine/data-access und libs/shared/util-datum (Etiketten wie in der Tabelle, Fachgebiet termine und shared). Der Generator legt project.json mit "tags": ["scope:kontakte", "type:feature"] an, einen Eintrag in tsconfig.base.json unter paths (@kladde/kontakte/feature-liste zeigt auf libs/kontakte/feature-liste/src/index.ts), eine Beispielkomponente und beim ersten Mal das Vitest-Plugin in nx.json. Die Bibliotheken sind nicht buildbar (siehe 0302).

Der Eintrag unter paths ist die öffentliche Schnittstelle: Von außen ist nur erreichbar, was src/index.ts ausführt. Unsere Datenzugriff-Bibliothek exportiert export type { ContactSummary } from './lib/contact-summary';, die UI-Bibliothek eine Karte, die eine eigene kleine Form nimmt:

TypeScript
export interface KarteInhalt {
  readonly name: string;
  readonly firma: string;
}
// ... Komponente KontaktKarte mit readonly inhalt = input.required<KarteInhalt>();

Die UI-Art darf nicht vom Datenzugriff abhängen; die Feature-Bibliothek kennt beide und übersetzt. Die Anwendung lädt sie träge: loadComponent: () => import('@kladde/kontakte/feature-liste').then((m) => m.KontaktListe) in app.routes.ts.

Ein Stolperstein: nx test einer Bibliothek ohne Testdatei endete bei uns mit Exit 1 („No test files found“). Eine Bibliothek nur mit Typen braucht in ihrer vite.config.mts unter test die Zeile passWithNoTests: true (so lösten wir es für beide Datenzugriff-Bibliotheken); eine mit Code bekommt einen echten Test.

Die Regeln aufschreiben

Die Regel @nx/enforce-module-boundaries (Paket @nx/eslint-plugin) steht in eslint.config.mjs an der Wurzel. Sie liest die Etiketten aus den Projekten und vergleicht sie mit depConstraints: Jeder Eintrag nennt ein sourceTag und die Etiketten, von denen es abhängen darf (onlyDependOnLibsWithTags). Trägt ein Projekt mehrere Etiketten, gelten alle passenden Einträge, so tragen zwei Achsen:

JavaScript
depConstraints: [
  { sourceTag: 'type:app', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] },
  { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] },
  { sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:ui', 'type:util'] },
  { sourceTag: 'type:data-access', onlyDependOnLibsWithTags: ['type:data-access', 'type:util'] },
  { sourceTag: 'type:util', onlyDependOnLibsWithTags: ['type:util'] },
  { sourceTag: 'scope:buero', onlyDependOnLibsWithTags: ['scope:kontakte', 'scope:termine', 'scope:shared'] },
  { sourceTag: 'scope:kontakte', onlyDependOnLibsWithTags: ['scope:kontakte', 'scope:shared'] },
  { sourceTag: 'scope:termine', onlyDependOnLibsWithTags: ['scope:termine', 'scope:shared'] },
  { sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'] },
],

Das ersetzt den Eintrag der Vorlage { sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }, der alles erlaubt. Etiketten dürfen laut Nx' Seite auch Muster sein (scope:*, reguläre Ausdrücke), und ein Projekt ohne Etiketten darf von keinem anderen abhängen, es sei denn, ein Eintrag erlaubt ausdrücklich *. Das sahen wir: Mit den neuen Regeln und einer Anwendung ohne Etiketten meldete der Lint-Lauf in app.routes.ts (Zeile 6, der dynamische Import):

6:26  error  A project without tags matching at least one constraint cannot depend on any libraries  @nx/enforce-module-boundaries

Die Regel prüft also auch dynamische import(). Nach "tags": ["type:app", "scope:buero"] in project.json der Anwendung lief nx run-many -t lint über alle sechs Projekte mit Exit 0.

Einen Verstoß provozieren

Legen Sie drei Dateien an, jede mit einem verbotenen Import:

TypeScript
// libs/kontakte/ui/src/lib/verboten.ts
import type { ContactSummary } from '@kladde/kontakte/data-access';
// libs/kontakte/feature-liste/src/lib/verboten.ts
import type { AppointmentResponse } from '@kladde/termine/data-access';
// libs/kontakte/feature-liste/src/lib/tief.ts
import type { ContactSummary } from '../../../data-access/src/lib/contact-summary';

(Jede Datei exportiert zusätzlich den importierten Typ unter einem Namen.) npx nx run-many -t lint endete mit Exit 1 und diesen Meldungen (Pfade gekürzt, Zeilen umbrochen):

libs/kontakte/ui/src/lib/verboten.ts
  1:1  error  A project tagged with "type:ui" can only depend on libs tagged with "type:ui", "type:util"
              @nx/enforce-module-boundaries
libs/kontakte/feature-liste/src/lib/tief.ts
  1:1  error  Projects cannot be imported by a relative or absolute path, and must begin with a npm scope
              @nx/enforce-module-boundaries
libs/kontakte/feature-liste/src/lib/verboten.ts
  1:1  error  A project tagged with "scope:kontakte" can only depend on libs tagged with "scope:kontakte", "scope:shared"
              @nx/enforce-module-boundaries

Der erste Fall verletzt die Art, der dritte das Fachgebiet, der zweite die öffentliche Schnittstelle: Wer an index.ts vorbei in den Ordner greift, wird abgelehnt. Als fehlgeschlagen nannte der Lauf kontakte-ui und kontakte-feature-liste. Löschen Sie die Dateien wieder. Der Graph (nx graph --file=graph.json, graph.dependencies) zeigt: kladde-buero hängt von kontakte-feature-liste ab, diese von kontakte-data-access und kontakte-ui.

Sheriff als Alternative

Sheriff ist ein Werkzeug für Modulgrenzen in TypeScript. Gelesen: Die Startseite nennt „zero dependencies“ und in den Hinweisen Version 0.19; die Projektseite bei GitHub nennt MIT, die Installation npm i -D @softarc/sheriff-core @softarc/eslint-plugin-sheriff und index.ts-Dateien als öffentliche Schnittstelle; ein Beitrag von angulararchitects.io sagt, Sheriff erzwinge Grenzen auf Ordnerebene für jedes TypeScript-Projekt.

Ermittelt (die Lücke des Plans): npm view @softarc/sheriff antwortet mit 404; ein Paket dieses Namens gibt es nicht. Es gibt @softarc/sheriff-core (0.20.0, MIT, Peer typescript >=4.8, Programm sheriff) und @softarc/eslint-plugin-sheriff (0.20.0, MIT, Peer eslint 8, 9 oder 10). Unklar ist die Fassung: npm sagt 0.20.0, die Hinweise der Startseite nennen 0.19; welche Seite aktuell ist, ist offen.

Ausprobiert in einem Wegwerf-Projekt mit src/kontakte und src/termine (je ein index.ts): sheriff init src/main.ts erzeugte eine sheriff.config.ts mit enableBarrelLess: true und allen Modulen als noTag. Ein Import vorbei am Barrel (../kontakte/lib/api) ließ sheriff verify mit Exit 1 enden („Total Encapsulation Violations: 1“). Mit der Konfiguration unten und einem Import über das Barrel meldete verify (Exit 1) „from tag domain:termine to tags domain:kontakte“:

TypeScript
import { sameTag, SheriffConfig } from '@softarc/sheriff-core';

export const config: SheriffConfig = {
  modules: { 'src/<domain>': ['domain:<domain>'] },
  depRules: { root: 'domain:*', 'domain:*': sameTag },
};

Wann was? Nx zieht Grenzen zwischen Projekten, Sheriff zwischen Ordnern. Wer Nx hat und Bibliotheken schneidet, hat die Regel im selben Werkzeug; Sheriff passt, wo es keinen Nx-Workspace gibt. Beides zusammen haben wir nicht ausprobiert.

Was Grenzen nicht leisten

Die Regel prüft Importe und sonst nichts. Sie fängt nicht:

  • falsche Etiketten und falsche Schnitte. Ein falsch eingeordnetes Projekt besteht jede Probe; ein falsch geschnittenes Fachgebiet wird durch Regeln nur teurer (W10.2).
  • Kopplung ohne Import. Ein Zeichenketten-Schlüssel, eine globale Variable, ein Ereignis auf window oder dieselbe API-URL aus zwei Fachgebieten taucht im Graphen nicht auf.
  • Regeln, die niemand prüft. Erst wenn der Lint-Lauf in der CI zählt (Lektion 0103), ist eine Grenze mehr als ein Wunsch.

Ein Test für das Regelwerk

Ein Regelwerk, das sich nicht prüfen lässt, wird unbemerkt gelockert. Eine Probe lintet kleine Quelltexte, als lägen sie in einer Bibliothek, und prüft, ob die Regel anschlägt. Node 24 führt .mts-Dateien direkt aus. tools/boundary-probe.mts (Gerüst; probes hat drei Einträge: ui nach data-access und kontakte nach termine mit mustFail: true, feature nach data-access desselben Fachgebiets mit false):

TypeScript
import { ESLint } from 'eslint';

const eslint = new ESLint({ cwd: process.cwd() });
let wrong = 0;
for (const probe of probes) {
  const [result] = await eslint.lintText(probe.code, { filePath: probe.file });
  const failed = (result?.messages ?? []).some((m) => m.ruleId === '@nx/enforce-module-boundaries');
  const ok = failed === probe.mustFail;
  console.log(`${ok ? 'ok   ' : 'WRONG'} ${probe.name}`);
  if (!ok) {
    wrong++;
  }
}
process.exit(wrong === 0 ? 0 : 1);

node tools/boundary-probe.mts: bei uns drei Zeilen ok, Exit 0. Gegenprobe: Wir erlaubten für type:ui zusätzlich type:data-access; die Probe druckte WRONG für den ersten Fall und endete mit Exit 1.

Übung 1

Bibliotheken anlegen, Etiketten setzen, Verstöße sehen, die Probe schreiben.

  1. Legen Sie die fünf Bibliotheken mit Namen, Importpfaden und Etiketten aus dem Text an und versehen Sie kladde-buero mit type:app und scope:buero.
  2. Ersetzen Sie in eslint.config.mjs die erlaubende Regel durch die depConstraints aus dem Text; exportieren Sie ContactSummary, KontaktKarte und KontaktListe über die index.ts und lassen Sie die Anwendung die Liste träge laden.
  3. Provozieren Sie die drei Verstöße, lesen Sie die Meldungen, löschen Sie die Dateien. Führen Sie dann nx run-many -t lint test build aus.
  4. Schreiben Sie tools/boundary-probe.mts mit den drei Proben, führen Sie sie aus, lockern Sie die Regel für type:ui (Gegenprobe) und stellen Sie sie wieder her.

So prüfen Sie: Mit den Verstößen endet lint mit Exit 1 und nennt kontakte-ui und kontakte-feature-liste; ohne sie exit=0. Die Probe: exit=0 mit drei ok-Zeilen, mit gelockerter Regel exit=1 und eine Zeile WRONG.

Lösung 1

  • „Schema does not support positional arguments“: Etiketten in Anführungszeichen ("--tags=scope:kontakte,type:feature").
  • „A project without tags matching at least one constraint cannot depend on any libraries“: Das importierende Projekt hat kein Etikett, das zu einem sourceTag passt; kladde-buero braucht type:app und scope:buero.
  • test einer Bibliothek endet mit Exit 1 („No test files found“): Test hinzufügen oder bei reinen Typen passWithNoTests: true.
  • Die Probe meldet überall WRONG: filePath muss in einem Projektordner liegen, und sie läuft von der Wurzel des Workspaces.

Was Sie jetzt können

  • Sie ordnen Bibliotheken nach Art (type:) und Fachgebiet (scope:) und legen sie samt Etiketten und öffentlicher Schnittstelle an.
  • Sie schreiben depConstraints für beide Achsen, lassen den Lint-Lauf einen verbotenen Import ablehnen und lesen die Meldung; Sie wissen, dass auch dynamische Importe geprüft werden und ein Projekt ohne Etiketten von nichts abhängen darf.
  • Sie ordnen Sheriff ein (@softarc/sheriff-core, @softarc/eslint-plugin-sheriff).
  • Sie benennen, was Grenzregeln nicht leisten, und prüfen das Regelwerk mit Probe und Gegenprobe.

Weiterlesen

Die Kladde: fünf Bibliotheken, zwei Etiketten je Projekt, eine Probe. Als Nächstes folgt Lektion 0103: Aufgabenpipeline, Cache und nx affected in der CI.