Ein Monorepo ist ein einziges Git-Repository, das mehrere Projekte enthält: zum Beispiel den Shop, das Admin-Panel und die Bibliotheken, die sie gemeinsam nutzen. Es ist kein Monolith: Jede Anwendung wird separat gebaut und deployt. Nur der Code liegt zusammen, mit einer Angular-Version und einer Commit-Historie.
Der Gewinn: Eine Änderung, die eine Bibliothek und die beiden Anwendungen betrifft, die sie nutzen, landet in einem einzigen PR und wird gemeinsam getestet. Der Preis: Ohne Tooling baut und testet jeder PR alles neu; ohne Regeln importiert jeder alles. Diese Seite behandelt beide Hälften: den Workspace mit der Angular CLI und ng-packagr, dann das, was Nx obendrauf bringt — den Projektgraphen, affected, den Cache und Boundaries, die beim Linting geprüft werden.
- Ein Monorepo hält mehrere Apps und Bibliotheken in einem Repository, mit einem einzigen Satz an Abhängigkeitsversionen.
- Die Angular CLI unterstützt mehrere Projekte in einem Workspace von Haus aus (
ng generate library). Nx ergänzt den Abhängigkeitsgraphen, Grenzen und Caching. - Eine Bibliothek hat eine öffentliche API: Was nicht aus ihrer Einstiegsdatei exportiert wird, bleibt intern.
- „Affected“ führt Build und Tests nur für das aus, was eine Änderung berührt hat.
- Meist reicht ein Ordner. Eine Bibliothek lohnt sich, wenn sie veröffentlicht oder von mehreren Apps genutzt wird.
„Ordner, Bibliothek oder npm-Paket“ ist eine Architekturentscheidung, keine Stilfrage. Sie bestimmt, wie schnell du eine übergreifende Änderung ausliefern kannst und wie leicht du etwas kaputt machst, ohne es zu merken. Die Angular-Dokumentation sagt es direkt: Eine Bibliothek zwingt dich, Code von der Logik der Anwendung zu entkoppeln, kostet aber Wartungszeit — und das zahlt sich aus, wenn mehrere Anwendungen sie nutzen.
Monorepo, Polyrepo, Monolith
Die drei Modelle unterscheiden sich vor allem in den Kosten einer übergreifenden Änderung. Angenommen, das Modell Product bekommt ein neues Feld, das die UI-Bibliothek und beide Anwendungen verwenden:
| Modell | So sieht es aus | „Product“ ändern | Der Preis |
|---|---|---|---|
| Monolith | Eine Anwendung, ein Deployment, Ordner nach Domäne | 1 PR, trivial | Taugt nicht für zwei Anwendungen mit unterschiedlichem Release-Rhythmus |
| Polyrepo + npm | Jede Bibliothek im eigenen Repo, mit Version veröffentlicht | 3+ PRs: Bibliothek → Publish → Bump in jeder Anwendung | Versionen driften auseinander; ein Fix erreicht Production erst, wenn jedes Team daran „denkt“ |
| Monorepo | Anwendungen + Bibliotheken im selben Repo, aus dem Workspace konsumiert | 1 atomarer PR, getestet mit allen Konsumenten | Die CI muss clever sein (affected, Cache) und die Boundaries müssen durchgesetzt werden |
Ein Angular-CLI-Workspace hat genau eine package.json im Root, also nutzen alle Projekte dieselbe Version von Angular, RxJS oder CDK — das nennt Nx die Single-Version-Policy. Ein ng update aktualisiert das ganze Repo auf einmal. Der Preis ist Koordination: Kein Team bleibt zurück, aber keines kann vorauslaufen. Wenn Teams wirklich unabhängig ausliefern müssen, mit unterschiedlichem Rhythmus und unterschiedlichen Versionen, ist der nächste Schritt Micro Frontends (die ironischerweise oft ebenfalls in einem Monorepo leben).
Multi-Projekt-Workspace mit der Angular CLI
ng new erstellt standardmäßig eine Anwendung im Root. Für ein Monorepo startest du mit einem leeren Workspace (--no-create-application), und jedes ng generate application oder ng generate library fügt ein Projekt unter projects/ und einen Eintrag in angular.json hinzu. Eine Bibliothek mit Scope (@acme/ui) landet in projects/acme/ui, und der Projektname ist der Paketname.
# 1. Leerer Workspace: keine Anwendung im Root (createApplication: false)
ng new acme --no-create-application
cd acme
# 2. Projekte landen in projects/ (newProjectRoot)
ng generate application shop
ng generate application admin
ng generate library @acme/ui # → projects/acme/ui, Standard-Präfix "lib"
# 3. Jeder Befehl bekommt den Projektnamen
ng serve shop
ng build admin
ng test @acme/ui
ng build @acme/ui --watch # inkrementeller Rebuild, während du an der Bibliothek arbeitestDer wichtige Unterschied steckt in der angular.json: Anwendungen nutzen den Builder @angular/build:application (esbuild), Bibliotheken @angular/build:ng-packagr, der ein Paket im Angular Package Format erzeugt und immer AOT kompiliert. Für Bibliotheken ist die Standardkonfiguration production.
{
"newProjectRoot": "projects",
"projects": {
"shop": {
"projectType": "application",
"root": "projects/shop",
"sourceRoot": "projects/shop/src",
"architect": {
"build": { "builder": "@angular/build:application" },
"serve": { "builder": "@angular/build:dev-server" }
}
},
"@acme/ui": {
"projectType": "library",
"root": "projects/acme/ui",
"sourceRoot": "projects/acme/ui/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular/build:ng-packagr",
"defaultConfiguration": "production",
"configurations": {
"production": { "tsConfig": "projects/acme/ui/tsconfig.lib.prod.json" },
"development": { "tsConfig": "projects/acme/ui/tsconfig.lib.json" }
}
}
}
}
}
}Vermeide das Präfix ng- (es ist dem Framework vorbehalten). Für öffentliche Pakete ist ngx- üblich; für interne ist ein Firmen-Scope (@acme/…) am klarsten und passt zu einer privaten Registry.
Aufbau einer Bibliothek
Vier Dateien definieren eine Bibliothek. Jede beantwortet eine bestimmte Frage:
ng-package.json— wohin der Build geht (dest) und welche Datei die öffentliche API enthält (lib.entryFile).package.json— Name, Version und Abhängigkeiten.@angular/*gehören inpeerDependencies: Stünden sie independencies, könnte die Anwendung eine zweite Kopie von Angular erhalten und zur Laufzeit kaputtgehen."sideEffects": falsesagt dem Bundler, dass er alles entfernen darf, was du nicht importierst.public-api.ts— der Vertrag. Was hier nicht exportiert wird, ist privat, du kannst es also refaktorieren, ohne es jemandem zu sagen.tsconfig.lib.prod.json—compilationMode: "partial": Das Partial-Ivy-Format ist über Angular-Versionen hinweg stabil, und die Kompilierung wird im Build der Anwendung mit deren eigener Compiler-Version abgeschlossen. Daher die Regel: Die Anwendung muss dieselbe Angular-Version verwenden, mit der die Bibliothek gebaut wurde, oder eine neuere.
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/acme/ui",
"lib": {
"entryFile": "src/public-api.ts"
}
}Secondary Entry Points
Das Angular Package Format erzeugt vor allem ein flaches ES-Modul pro Entry Point. Ein Bundler kann ein Modul nicht in mehrere Chunks aufteilen, daher ist der Entry Point die kleinste Granularität für Lazy Loading. Deshalb hat Angular Material einen Entry Point pro Komponente (@angular/material/button, …/tabs). Der zweite Grund: Du isolierst spezielle APIs wie @acme/ui/testing, damit sie nicht aus Versehen in Production landen.
projects/acme/ui/
├── ng-package.json → @acme/ui (primärer Entry Point)
├── src/public-api.ts
├── button/
│ ├── ng-package.json → @acme/ui/button (sekundärer Entry Point)
│ └── src/public-api.ts
└── testing/
├── ng-package.json → @acme/ui/testing (Harnesses, Mocks für Tests)
└── src/public-api.ts
ng-packagr findet jede ng-package.json selbst; du registrierst nirgendwo etwas.Eine Bibliothek mit nur einem Zweck (ein HTTP-Client, eine Sammlung von Hilfsfunktionen) kommt gut mit einem einzigen Entry Point aus — und @angular/core hat für die Runtime nur einen.
Konsum, Build und Veröffentlichung
Innerhalb desselben Workspace musst du nichts veröffentlichen, aber du musst die Bibliothek zuerst bauen. ng generate library fügt in der tsconfig.json einen Alias hinzu, der auf dist/ zeigt, nicht auf den Quellcode. Der Grund: Anwendungen und Bibliotheken haben unterschiedliche Builds (esbuild vs. ng-packagr), und dasselbe TypeScript kann als anderes JavaScript herauskommen. Die Dokumentation verlangt ausdrücklich, dass Aliase auf die gebaute Bibliothek zeigen. Während du arbeitest, baut ng build @acme/ui --watch inkrementell neu.
{
"compilerOptions": {
"paths": {
// automatisch von ng generate library hinzugefügt — zeigt auf den BUILD, nicht auf den Quellcode
"@acme/ui": ["./dist/acme/ui"],
// von dir hinzugefügt, für die sekundären Entry Points
"@acme/ui/*": ["./dist/acme/ui/*"]
}
}
}Veröffentlichen heißt: Build in der Konfiguration production (der Standard) und npm publish aus dist — du veröffentlichst das generierte Paket, nicht den Quellordner.
# Distributions-Build: die Konfiguration production (Standard für Bibliotheken) → partial-Ivy
ng build @acme/ui
# Du veröffentlichst den INHALT von dist, nicht den Quellordner
cd dist/acme/ui
npm publish # oder: npm publish --registry https://npm.company.com
# Der Konsument (anderes Repo) installiert es wie jedes andere Paket
npm install @acme/ui@^1.4.0| Konsument | Wie er den Code bekommt | Versionen |
|---|---|---|
| Anwendung im selben Repo | Ein Alias in der tsconfig, immer die neueste Version im Branch | Nicht nötig: Alle sind auf HEAD |
| Anderes Repo / anderer Client | npm install @acme/ui aus der Registry | Striktes Semver, ein Changelog, korrekte peerDependencies |
Der Generator @nx/angular:lib setzt den Alias in die tsconfig.base.json und lässt ihn auf den Quellcode zeigen (libs/…/src/index.ts): Die Bibliothek wird zusammen mit der Anwendung kompiliert, ohne separaten Build-Schritt. Einen eigenen Build (--buildable / --publishable) fügst du nur für Bibliotheken hinzu, die du veröffentlichst, oder für inkrementelle Builds. Mische die beiden Stile nicht im selben Workspace, ohne zu wissen, warum.
Nx: Graph und Boundaries
Nx nutzt dieselben Angular-Builder, legt die Konfiguration jedes Projekts aber in einer eigenen project.json ab und baut aus der Analyse der Imports einen Projektgraphen. Alles Weitere stützt sich auf diesen Graphen: nx graph zeigt ihn dir, affected läuft ihn rückwärts ab, und die Lint-Regel prüft ihn bei jedem Import. In einem bestehenden Angular-CLI-Workspace startest du mit npx nx@latest init.
# Bestehender Angular-CLI-Workspace → Nx (behält die Struktur)
npx nx@latest init
# …oder verschiebe die Projekte nach apps/ und libs/
npx nx@latest init --integrated
# Bibliotheken mit Tags vom ersten Tag an
nx g @nx/angular:lib libs/shop/feature-cart --tags=scope:shop,type:feature
nx g @nx/angular:lib libs/shared/ui --tags=scope:shared,type:ui
nx g @nx/angular:lib libs/shared/ui --publishable --importPath=@acme/ui # nur, wenn du sie veröffentlichst
# Der Projektgraph, interaktiv im Browser
nx graph
nx graph --focus shop-feature-cart
# Nur ausführen, was von den Änderungen gegenüber main betroffen ist
nx affected -t lint test build
nx affected -t build --graph # den Task-Graphen ansehen, bevor du startest
nx run-many -t test # alles, ohne FilterBibliothekstypen
Der Nx-Leitfaden schlägt vier Typen vor — dieselben Rollen wie bei Smart vs. Dumb und in der Ordnerstruktur, nur dass die Grenze jetzt eine Bibliothek mit öffentlicher API ist:
| Typ | Was er enthält | Darf abhängen von |
|---|---|---|
feature | Seiten und Container-Komponenten für einen Use Case; meist lazy | jedem Typ |
ui | Präsentations-Komponenten; die Daten kommen über Inputs, keine injizierten Services | ui, util |
data-access | Kommunikation mit dem Backend und Zustand (Stores) | data-access, util |
util | Pure Funktionen, oft ohne Framework-Code | util |
acme/
├── nx.json ← cache, targetDefaults, defaultBase
├── tsconfig.base.json ← "@acme/shop/feature-cart": ["libs/shop/feature-cart/src/index.ts"]
├── apps/
│ ├── shop/project.json tags: scope:shop, type:app
│ └── admin/project.json tags: scope:admin, type:app
└── libs/
├── shop/
│ ├── feature-catalog/ tags: scope:shop, type:feature (Smart-Seiten, lazy)
│ ├── feature-cart/ tags: scope:shop, type:feature
│ └── data-access/ tags: scope:shop, type:data-access (API + store)
├── admin/
│ ├── feature-users/ tags: scope:admin, type:feature
│ └── data-access/ tags: scope:admin, type:data-access
└── shared/
├── ui/ tags: scope:shared, type:ui (Dumb-Komponenten)
├── data-access/ tags: scope:shared, type:data-access (Auth, HTTP-Konfiguration)
└── util/ tags: scope:shared, type:util (pure Funktionen)Tags in zwei Dimensionen
Jedes Projekt bekommt Tags in der project.json. In der Praxis funktioniert die Kombination aus Scope (die Domäne: scope:shop, scope:admin, scope:shared) × Typ (die Schicht: type:feature, type:ui…). Der Scope beantwortet „wer spricht mit wem“, der Typ „in welche Richtung fließen die Abhängigkeiten“.
{
"name": "shop-feature-cart",
"sourceRoot": "libs/shop/feature-cart/src",
"projectType": "library",
"tags": ["scope:shop", "type:feature"]
}Die Regel @nx/enforce-module-boundaries
Die Lint-Regel nimmt eine Liste von depConstraints entgegen. Für einen Import nimmt Nx alle Constraints, deren sourceTag das Quellprojekt besitzt, und wendet sie mit UND an: shop/feature-cart muss sowohl die Scope-Regel als auch die Typ-Regel erfüllen. onlyDependOnLibsWithTags verlangt, dass das Ziel mindestens eines der Tags hat. Ein Projekt ohne passenden Tag darf von nichts abhängen.
import nx from '@nx/eslint-plugin';
export default [
...nx.configs['flat/base'],
...nx.configs['flat/typescript'],
...nx.configs['flat/javascript'],
{
files: ['**/*.ts'],
rules: {
'@nx/enforce-module-boundaries': [
'error',
{
allow: [],
depConstraints: [
// Dimension 1: die Domäne (wer mit wem sprechen darf)
{ sourceTag: 'scope:shop', onlyDependOnLibsWithTags: ['scope:shop', 'scope:shared'] },
{ sourceTag: 'scope:admin', onlyDependOnLibsWithTags: ['scope:admin', 'scope:shared'] },
{ sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'] },
// Dimension 2: der Typ (die Richtung, in die Abhängigkeiten fließen)
{ 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'] },
],
},
],
},
},
];Neben den Tags hat die Regel feste Prüfungen, die vor ihnen laufen. Drei davon zählen im Alltag, in dieser Reihenfolge: zirkuläre Abhängigkeiten, der Import einer Anwendung (verboten) und ein statischer Import einer lazy geladenen Bibliothek (er würde den Chunk ins Haupt-Bundle ziehen). Alle siehst du in der Demo:
scope:shopscope:sharedscope:adminlazy (loadChildren)
Dieselbe Regel akzeptiert auch feinere Formen:
// Weitere nützliche Formen von Constraints (dieselbe Regel, dieselben depConstraints):
[
// "ui" darf data-access auch nicht TRANSITIV erreichen (prüft die ganze Kette)
{ sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:data-access'] },
// die Regel gilt nur für Projekte, die BEIDE Tags haben
{ allSourceTags: ['scope:shop', 'type:ui'], onlyDependOnLibsWithTags: ['scope:shared'] },
// für einen Projekttyp verbotene npm-Pakete
{ sourceTag: 'type:util', bannedExternalImports: ['@angular/*'] },
// Glob und Regex in Tags
{ sourceTag: 'scope:*', onlyDependOnLibsWithTags: ['/^scope:(shared|core)$/'] },
]Achte auf den Unterschied: notDependOnLibsWithTags prüft die gesamte Kette der Abhängigkeiten des Ziels, nicht nur die direkte. Das ist das richtige Werkzeug für „die UI erreicht nie data-access, auch nicht über Umwege“. Für Boundaries zwischen Ordnern in einer einzelnen Anwendung, ohne Nx, siehe Boundaries.
Affected und Cache
nx affected -t test startet bei den Dateien, die sich zwischen --base (standardmäßig defaultBase, also main) und --head geändert haben, ordnet sie Projekten zu und läuft dann im Graphen entlang der umgekehrten Kanten nach oben: Alles, was direkt oder transitiv von einem geänderten Projekt abhängt, ist betroffen. Im Demo-Workspace betrifft eine Änderung in shop/data-access 4 Projekte (die Bibliothek, die beiden Shop-Features und die Shop-Anwendung); eine in shared/util betrifft alle 10.
Dazu kommt der Cache: Bevor ein Task läuft, berechnet Nx einen Hash seiner Inputs — die Quellen des Projekts, die seiner Abhängigkeiten (gemäß inputs), die Konfiguration, das Kommando. Wurde der Hash schon einmal gesehen, spielt Nx das Ergebnis erneut ab (die Terminal-Ausgabe und die Dateien), statt den Task auszuführen. Mit einem Remote-Cache läuft ein Task, der einmal in der CI gelaufen ist, auf keinem Laptop mehr.
geändert ✎betroffengelaufen ✓aus dem Cache
Klicke auf ein Projekt im Graphen: Du änderst eine seiner Dateien.
- Hier erscheint das CI-Terminal.
{
"defaultBase": "main",
"namedInputs": {
"default": ["{projectRoot}/**/*"],
"production": ["default", "!{projectRoot}/**/*.spec.ts"]
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"test": {
"inputs": ["default", "^production"],
"cache": true
},
"lint": { "cache": true }
}
}Wenn jede Anwendung von einer riesigen shared-Bibliothek abhängt, betrifft jede Änderung darin das ganze Repo, und affected spart nichts mehr. Kleinere Bibliotheken, nach Domäne und Typ aufgeteilt, bedeuten engere Graphen und mehr Cache-Hits.
Der Cache setzt Tasks ohne Seiteneffekte voraus: Dieselben Inputs liefern dasselbe Ergebnis. Ein E2E-Test, der ein echtes Backend anspricht, ist nicht cachebar — sein Ergebnis hängt von etwas ab, das Nx nicht sehen kann.
Ordner oder Bibliothek?
Der Nx-Leitfaden zur Projektgröße fasst den Kompromiss gut zusammen. Du teilst in Bibliotheken auf, wenn Tests dich ausbremsen (affected und Cache beginnen, Teile zu überspringen), wenn der Code einen anderen Besitzer hat (ein Team, eine CODEOWNERS-Datei) oder wenn du verifizierte Boundaries willst. Was sich immer gemeinsam ändert, teilst du nicht auf — die Grenze kostet Navigation und Konfiguration, ohne etwas zu bringen. Und solange das Design noch erkundet wird, ist ein einziges Projekt billiger; die Bibliothek extrahierst du, sobald die API sich gesetzt hat.
Ein Ordner im Feature (oder in shared/) mit einer index.ts als öffentlicher API bringt dir 80 % der Vorteile ohne Konfiguration. Extrahiere die Bibliothek, sobald ein zweiter Konsument auftaucht.
src/app/features/cart/ui/ + Lint-Regel für ImportsFür die Option „Ordner“ bringen dir die Regeln aus Shared vs. Feature und eine index.ts als öffentliche API die meisten Vorteile, ganz ohne zusätzliche Konfiguration.
Wann ja, wann nein
- Zwei oder mehr Anwendungen teilen Domänencode oder ein internes Design-System.
- Häufige übergreifende Änderungen (API-Modelle, Auth, Theme), die du atomar in einem PR ausliefern willst.
- Mehrere Teams im selben Code: Tags und die Lint-Regel machen die Boundaries explizit.
- Eine langsame CI:
affected+ Cache verkürzen die Zeit proportional zu dem, was du angefasst hast. - Ein Design-System, das auch von außerhalb des Repos genutzt wird: eine veröffentlichbare Bibliothek, gebaut mit ng-packagr.
- Eine Anwendung, ein Team: Domänenordner (Ordnerstruktur) und eine
no-restricted-imports-Regel geben dir dieselben Boundaries. - Teams, die völlig unabhängig ausliefern müssen, auch mit unterschiedlichen Angular-Versionen: Polyrepo oder Micro Frontends.
- Eine Bibliothek pro Komponente: Konfiguration ohne Nutzen. Eine Bibliothek pro Domäne und Typ, nicht pro Datei.
- Veröffentlichung auf npm für Code, der nur im Repo verwendet wird: Versionen bringen Zeremonie ohne Wert.
Häufige Stolperfallen
"@acme/ui": ["projects/acme/ui/src/public-api.ts"] wirkt bequem (du musst die Bibliothek nicht mehr bauen), aber die Anwendung kompiliert den Code mit esbuild, während externe Konsumenten das bekommen, was ng-packagr erzeugt. Die Bugs treten nur bei ihnen auf. Fix: ein Alias auf dist und ng build @acme/ui --watch — oder wechsle zu Nx, das dafür gedacht ist, aus dem Quellcode zu kompilieren.
Die Bibliothek bringt ihre eigene Kopie von Angular mit, und die Anwendung endet mit zwei Instanzen: DI und Change Detection gehen auf schwer zu diagnostizierende Weise kaputt. Fix: Alles, was @angular/* ist, gehört in peerDependencies.
- Deep Imports (
@acme/ui/src/lib/button/button), diepublic-api.tsumgehen: Das erste interne Refactoring bricht die Konsumenten. Fix: aus der öffentlichen API exportieren oder einen Secondary Entry Point anlegen; das Muster mitno-restricted-importsverbieten. - Relative Imports zwischen Entry Points derselben Bibliothek: ng-packagr baut jeden Entry Point separat, und der Build schlägt fehl. Fix: über den Paketnamen importieren (
@acme/ui). - Projekte ohne Tags, „repariert“ mit
sourceTag: '*'→'*': Die Regel prüft nichts mehr. Fix:--tagsbeim Generieren und die Tags im Review prüfen. - Eine Shared-Bibliothek als Müllschublade, von der alles abhängt: affected = das ganze Repo bei jedem PR. Fix: nach Domäne und Typ aufteilen.
- Nicht deterministische Tasks cachen (E2E gegen ein echtes Backend, Tests, die die Uhrzeit lesen): Nx spielt ein Ergebnis ab, das nicht mehr stimmt. Fix: kein
cache: truebei diesen Targets, oder vollständige Inputs. - Ein statischer Import über einem
loadChildren: Der Lazy-Chunk verschwindet im initialen Bundle. Die Nx-Regel erkennt das; ohne Nx kontrollierst du das Bundle (siehe Performance).
Spickzettel
ANGULAR CLI (Multi-Projekt)
ng new acme --no-create-application leerer Workspace
ng g application shop | ng g library @acme/ui
ng build @acme/ui [--watch] Bibliotheken werden gebaut, BEVOR sie konsumiert werden
tsconfig paths → ./dist/acme/ui (+ "@acme/ui/*" für sekundäre Entry Points)
cd dist/acme/ui && npm publish nur was das Repo verlässt, wird veröffentlicht
BIBLIOTHEK
ng-package.json dest + lib.entryFile (sekundär: nur "lib")
public-api.ts die öffentliche API — der Rest ist privat
package.json peerDependencies für @angular/*, "sideEffects": false
compilationMode "partial" für npm
NX
nx graph der Projektgraph
nx affected -t lint test build nur die betroffenen Projekte (inkl. transitiver Abhängiger)
"cache": true in targetDefaults Hash über Inputs → Replay beim zweiten Lauf
tags + @nx/enforce-module-boundaries → Boundaries werden beim Linting durchgesetzt
Typen: feature → ui, data-access, util · ui → ui, util · data-access → data-access, util · util → util