From ba140e08af5f1363111f248d41c707e1fc25534e Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Thu, 11 Jun 2026 12:11:02 +0200 Subject: [PATCH 01/24] WIP --- material/ngrx-global-store/README.md | 258 ++++++++++++++++ .../developer-tool-action.png | Bin 0 -> 345496 bytes .../developer-tool-skip-action.png | Bin 0 -> 163470 bytes .../ngrx-global-store/webstore-devtools.png | Bin 0 -> 197821 bytes material/ngrx-intro/README.md | 279 ++++++++++++++++++ 5 files changed, 537 insertions(+) create mode 100644 material/ngrx-global-store/README.md create mode 100644 material/ngrx-global-store/developer-tool-action.png create mode 100644 material/ngrx-global-store/developer-tool-skip-action.png create mode 100644 material/ngrx-global-store/webstore-devtools.png create mode 100644 material/ngrx-intro/README.md diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md new file mode 100644 index 00000000..8ff43340 --- /dev/null +++ b/material/ngrx-global-store/README.md @@ -0,0 +1,258 @@ +--- +title: "State Management mit NgRx – Teil 2: Global Store mit NgRx" +published: "2026-06-11" +lastModified: "2026-06-11" +hidden: true +--- + +**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (4. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** + +Dieser Artikel ist **Teil 2** einer zweiteiligen Serie zum Thema State Management mit NgRx. + +- Teil 1: Wie kommen wir zu zentralem State Management? → [zum Artikel](/material/ngrx-intro) +- Teil 2: Global Store mit NgRx (dieser Artikel) + +[[toc]] + +Im ersten Teil haben wir ein eigenes Modell für zentrales State Management entwickelt und die Grundprinzipien von Redux kennengelernt. Jetzt setzen wir das Ganze mit **NgRx** in die Praxis um. + +## NgRx: Reactive Extensions for Angular + +NgRx ist ein Set von Bibliotheken, das Redux-Architektur mit den reaktiven Möglichkeiten von RxJS in Angular integriert. + +### Projekt vorbereiten + +Wir arbeiten mit der BookMonkey-Variante `16e-ngrx`. + +Installiere die notwendigen Pakete: + +```bash +npm install @ngrx/store @ngrx/effects @ngrx/store-devtools +``` + +### Store einrichten + +Im `AppModule` binden wir den Store ein: + +```ts +// app.module.ts +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { isDevMode } from '@angular/core'; + +@NgModule({ + imports: [ + // ... + StoreModule.forRoot({}, {}), + EffectsModule.forRoot([]), + StoreDevtoolsModule.instrument({ + maxAge: 25, + logOnly: !isDevMode() + }) + ], + // ... +}) +export class AppModule { } +``` + +### Feature anlegen + +Wir legen für das Books-Feature einen eigenen Store-Bereich an. + +```ts +// books/store/book.reducer.ts +import { createReducer, on } from '@ngrx/store'; +import { Book } from '../../shared/book'; +import * as BookActions from './book.actions'; + +export const bookFeatureKey = 'book'; + +export interface State { + books: Book[]; + loading: boolean; +} + +export const initialState: State = { + books: [], + loading: false +}; + +export const reducer = createReducer( + initialState, + + on(BookActions.loadBooks, (state): State => { + return { ...state, loading: true }; + }), + + on(BookActions.loadBooksSuccess, (state, action): State => { + return { + ...state, + books: action.data, + loading: false + }; + }), + + on(BookActions.loadBooksFailure, (state): State => { + return { ...state, loading: false }; + }) +); +``` + +### Actions definieren + +```ts +// books/store/book.actions.ts +import { createAction, props } from '@ngrx/store'; +import { Book } from '../../shared/book'; + +export const loadBooks = createAction('[Book] Load Books'); + +export const loadBooksSuccess = createAction( + '[Book] Load Books Success', + props<{ data: Book[] }>() +); + +export const loadBooksFailure = createAction( + '[Book] Load Books Failure', + props<{ error: string }>() +); +``` + +### Reducer im Feature-Module registrieren + +```ts +// books/books.module.ts +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import * as fromBook from './store/book.reducer'; +import { BookEffects } from './store/book.effects'; + +@NgModule({ + imports: [ + // ... + StoreModule.forFeature(fromBook.bookFeatureKey, fromBook.reducer), + EffectsModule.forFeature([BookEffects]) + ] +}) +export class BooksModule { } +``` + +### Selectors schreiben + +```ts +// books/store/book.selectors.ts +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import * as fromBook from './book.reducer'; + +export const selectBookState = createFeatureSelector( + fromBook.bookFeatureKey +); + +export const selectBooksLoading = createSelector( + selectBookState, + state => state.loading +); + +export const selectAllBooks = createSelector( + selectBookState, + state => state.books +); +``` + +### Komponente mit Store verbinden + +```ts +// books/book-list/book-list.component.ts +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import { Book } from '../../shared/book'; +import { loadBooks } from '../store/book.actions'; +import { selectAllBooks, selectBooksLoading } from '../store/book.selectors'; + +@Component({ + selector: 'bm-book-list', + templateUrl: './book-list.component.html' +}) +export class BookListComponent { + books$: Observable; + loading$: Observable; + + constructor(private store: Store) { + this.books$ = this.store.select(selectAllBooks); + this.loading$ = this.store.select(selectBooksLoading); + + this.store.dispatch(loadBooks()); + } +} +``` + +Template: + +```html +

Books

+
    +
  • + +
  • +
+ +
Loading ...
+``` + +### Effects für HTTP-Seiteneffekte + +```ts +// books/store/book.effects.ts +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; + +import * as BookActions from './book.actions'; +import { BookStoreService } from '../../shared/book-store.service'; + +@Injectable() +export class BookEffects { + + loadBooks$ = createEffect(() => { + return this.actions$.pipe( + ofType(BookActions.loadBooks), + switchMap(() => + this.service.getAll().pipe( + map(data => BookActions.loadBooksSuccess({ data })), + catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) + ) + ) + ); + }); + + constructor( + private actions$: Actions, + private service: BookStoreService + ) {} +} +``` + +### Debugging mit Redux DevTools + +Nach der Installation von `StoreDevtoolsModule` kannst du die Redux DevTools im Browser nutzen. Du siehst alle dispatched Actions, den State vor und nach der Action und kannst Time-Travel-Debugging betreiben. + +### Weiterführende Themen + +Im Buch werden außerdem behandelt: + +- `createActionGroup` für gruppierte Actions +- Entity-Management mit `@ngrx/entity` +- Testing von Reducers, Selectors und Effects (auch mit Marble-Tests) +- `@ngrx/component` und `@ngrx/component-store` +- Facades zur Abstraktion des Stores + +--- + +**Zurück zu Teil 1:** [Wie kommen wir zu zentralem State Management?](/material/ngrx-intro) + +*Dieser Artikel ist ein möglichst literaler Port des Kapitels „State Management mit Redux und NgRx“ aus dem Buch „Angular: Das große Praxisbuch (4. Auflage)“.* +``` \ No newline at end of file diff --git a/material/ngrx-global-store/developer-tool-action.png b/material/ngrx-global-store/developer-tool-action.png new file mode 100644 index 0000000000000000000000000000000000000000..7e4cdc732a806ebf3c68ce7e3047da85e8ef6d11 GIT binary patch literal 345496 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 P0siErmElE_2G9QwW$5)< literal 0 HcmV?d00001 diff --git a/material/ngrx-global-store/developer-tool-skip-action.png b/material/ngrx-global-store/developer-tool-skip-action.png new file mode 100644 index 0000000000000000000000000000000000000000..81e81a32548ce69d185059eeca00344aef2f7ba7 GIT binary patch literal 163470 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%0A~=3U#jC9?_R((TexAll7-#cv#iKn9O~8(FTeR| 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

&YslJ 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

Counter: {{ counter }}

+ +``` + +Wir haben in einem ersten Schritt unseren Zustand zentralisiert. Der Mehrwert zu einer isolierten Lösung besteht darin, dass alle Komponenten denselben Datensatz verwenden und anzeigen. Der Ort der Datenhaltung ist klar definiert, und es gibt keine Datensilos bei den einzelnen Komponenten. + +### Subject in einem Service + +Wir haben den Anwendungszustand an einer zentralen Stelle untergebracht, allerdings hat die Lösung einen Nachteil. Mit der aktuellen Architektur können wir nur über Umwege programmatisch auf Änderungen an den Daten reagieren. Eine Änderung am State wird zwar jederzeit korrekt angezeigt, aber dies basiert allein auf den Mechanismen der Change Detection. Wollen wir hingegen zusätzlich eine Routine anstoßen, sobald sich Daten ändern, haben wir aktuell keine direkte Möglichkeit dazu. + +Um das zu verbessern, ergänzen wir den Service mit einem Subject. Das Subject ist ein Baustein, mit dem wir ein Event an mehrere Subscriber verteilen können. Ein Subject implementiert hierfür sowohl alle Methoden eines Observers (Daten senden) als auch die eines Observables (Daten empfangen). Wenn der Zustand geändert wird, soll das Subject diese Neuigkeit mit einem Event bekannt machen, sodass die Komponenten darauf reagieren können. + +Für unser Beispiel eignet sich ein `BehaviorSubject`. Seine wichtigste Eigenschaft besteht darin, dass es den jeweils letzten Zustand speichert. Jeder neue Subscriber erhält die aktuellen Daten, ohne dass ein neues Event ausgelöst werden muss. Interessierte Komponenten können den Datenstrom also jederzeit abonnieren und auf die Ereignisse reagieren. Das `BehaviorSubject` muss mit einem Startwert initialisiert werden, der über den Konstruktor übergeben wird. + +Wir setzen im Service zunächst die Eigenschaft `state` auf privat, sodass man nun gezwungen ist, das Observable `state$` zu verwenden, anstatt direkt auf das Objekt zuzugreifen. Wird `incrementCounter()` aufgerufen und der State aktualisiert, so lösen wir das `BehaviorSubject` mit dem aktuellen State-Objekt aus. So werden alle Subscriber über den neuen Zustand informiert. + +```ts +// Zentralen Zustand mit Subject verwenden +import { BehaviorSubject } from 'rxjs'; + +export interface State { /* ... */ } + +@Injectable({ providedIn: 'root' }) +export class StateService { + private state = new BehaviorSubject({ + counter: 0, + anotherProperty: 'foo' + }); + readonly state$ = this.state.asObservable(); + + incrementCounter() { + const current = this.state.value; + this.state.next({ + ...current, + counter: current.counter + 1 + }); + } +} +``` + +Unsere Komponenten können nun die Informationen aus dem Subject beziehen. Der Operator `map()` hilft uns, schon in der Komponentenklasse die richtigen Daten aus dem State-Objekt zu selektieren. So erhalten wir z. B. ein Observable, das nur den fortlaufenden Counter-Wert ausgibt. + +```ts +// Zustand vor der Verwendung transformieren +@Component({ ... }) +export class MyComponent { + counter$ = this.stateService.state$.pipe( + map(state => state.counter) + ); + + constructor(private stateService: StateService) {} +} +``` + +Im Template nutzen wir schließlich die AsyncPipe, um das Observable zu abonnieren. + +```html +

Counter: {{ counter$ | async }}

+ +``` + +Dieser Ansatz bietet einen Mehrwert zum vorherigen Beispiel: Die Komponenten teilen sich nicht nur die Daten, sie können auch reaktiv Änderungen entgegennehmen. Zusätzlich sind wir in der Lage, bei Bedarf die Strategie der Change Detection für die Komponente zu ändern und so in einem komplexeren Szenario gegebenenfalls die Performance zu optimieren. + +### Unveränderlichkeit + +Unser Beispiel hat sich gut entwickelt, hat aber noch ein grundlegendes Designproblem. Wir halten unsere Daten in einem zentralen Objekt, das mit wachsender Größe der Anwendung ebenfalls größer wird. Alle Änderungen werden *direkt* an diesem Objekt durchgeführt, und wir geben es lediglich als Referenzparameter (*Call by Reference*) an die aufrufende Funktion weiter. Wir stellen uns nun vor, das Objekt hätte viele weitere Eigenschaften und eine verschachtelte Datenstruktur. Die Ereignisse zum Ändern der Daten können weiterhin aus diversen Gründen ausgelöst werden. Wie können wir nun effizient herausfinden, ob das Objekt bzw. ein Teil der verschachtelten Datenstruktur verändert wurde? Die Antwort lautet: Wir können dies nicht ohne zusätzlichen Aufwand realisieren. Um eine Änderung festzustellen, ist es notwendig, das Objekt mit einer zuvor erstellten Kopie zu vergleichen. Da wir mit Referenzen arbeiten, müssen wir jede Eigenschaft der verschachtelten Datenstruktur mit dem Gegenstück aus der Kopie vergleichen. + +Das wollen wir ändern, indem wir das Objekt *unveränderlich* (engl. *immutable*) machen. Zur Erstellung von unveränderlichen Objekten gibt es verschiedene Bibliotheken, darunter das Projekt [Immutable.js](https://immutable-js.github.io/immutable-js/) oder die leichtgewichtige Bibliothek [Immer](https://github.com/immerjs/immer). Für ein simples Szenario genügt auch die native JavaScript-Methode `Object.freeze()`. Damit können wir ein Objekt „einfrieren“ und direkte Änderungen an den Daten verhindern. + +Dadurch ändert sich ein grundlegender Aspekt: Da Änderungen nicht mehr direkt am bisherigen Objekt möglich sind, werden wir gezwungen, das Objekt auszutauschen. Wir erzeugen hierfür bei jeder Änderung eine Kopie des vorherigen Objekts mit einer Ausnahme: dem zu ändernden Wert. Eine Änderung festzustellen ist nun sehr einfach: Wir müssen lediglich Referenzen vergleichen. Das ist kein Problem, da wir durch die Unveränderlichkeit sicher sein können, dass keine Änderung durch direkte Manipulation des Objekts möglich sein kann. Versehentliche Änderungen sind damit ebenfalls ausgeschlossen. + +Für die meisten Anwendungsfälle benötigen wir allerdings gar keine echte Unveränderlichkeit! Es reicht im Prinzip schon aus, nur so zu tun, als wäre das Objekt unveränderlich, und dies konsequent beim Programmieren einzuhalten. Wir können hierfür den Spread-Operator nutzen und damit alle Eigenschaften kopieren. + +Im folgenden Listing demonstrieren wir die Verwendung. Die Methode `incrementCounter()` nutzt den Spread-Operator, um eine Kopie des vorherigen Objekts und damit eine neue Referenz zu erzeugen. Im selben Schritt schreiben wir den neuen Wert des Zählers in die Eigenschaft `counter`. + +```ts +// Objekte unveränderlich behandeln +incrementCounter() { + this.state.next({ + ...this.state.value, + counter: this.state.value.counter + 1 + }); +} +``` + +Wir haben durch die „Pseudo-Immutability“ den Weg geebnet, um die Strategie für die Change Detection zu optimieren: Wenn ein Objekt bei einer Änderung stets eine neue Referenz erhält, so können wir in den Kindkomponenten die Strategie `OnPush` einsetzen. Dies kann die Performance der Anwendung entscheidend verbessern. + +Bitte beachten Sie, dass der Spread-Operator stets nur eine flache Kopie (Shallow Copy) eines Objekts erstellt. Ist ein Objekt oder Array verschachtelt, so müssen wir bei Änderungen auch immer das direkt betroffene Objekt kopieren. + +### Nachrichten + +Wir wollen einen Schritt weiter gehen und das System noch mehr entkoppeln. So wie der Service aktuell implementiert ist, muss für jede Aktion auch eine Methode existieren, die von der Komponente aufgerufen wird, z. B. `incrementCounter()`. Idealerweise kennen die Komponenten allerdings gar keine Details über die konkrete Implementierung der Zustandsverwaltung. Koppeln wir die Bausteine zu eng aneinander, so wird es mit wachsender Größe der Anwendung immer aufwendiger, grundlegende Änderungen oder Umstrukturierungen durchzuführen. + +Anstatt also für jede Aktion eine Methode anzulegen, wollen wir eine Reihe von Nachrichten vereinbaren, mit denen die Anwendung Ereignisse signalisieren kann. Welche Routinen als Reaktion auf eine Nachricht anzustoßen sind, das entscheidet allein die Zustandsverwaltung. Die Komponenten teilen lediglich mit, was in der Anwendung passiert. + +Der relevante Unterschied zu einem Methodenaufruf ist die Entkopplung: Dem System steht es frei, auf eine Nachricht zu reagieren oder sie zu ignorieren. Existiert für eine bestimmte Nachricht noch keine Logik, so tritt kein Fehler auf, sondern die Nachricht wird schlichtweg nicht behandelt. Ebenso können mehrere Teile der Anwendung gleichzeitig auf Nachrichten reagieren oder auch zeitversetzt die Nachricht verarbeiten. Zeichnet man die Nachrichten auf, so bleibt durch die Historie der Nachrichten stets ersichtlich, was in welcher Reihenfolge passiert ist. + +Für das Zählerbeispiel können wir beispielsweise die Nachrichten `INCREMENT`, `DECREMENT` und `RESET` vereinbaren, die von den Komponenten zum Service geschickt werden können. Die Methode `dispatch()` werden wir im nächsten Abschnitt noch genauer betrachten. Für den Moment vereinbaren wir, dass sie eine Nachricht entgegennimmt und für die gewünschte Zustandsänderung sorgt. + +```ts +// Nachricht in den Service senden +dispatch(action: { type: string; payload?: any }) { + // ... +} +``` + +Wenn wir diese Architektur genauer betrachten, fällt auf, dass wir Lesen und Schreiben für unser Zustandsobjekt vollständig voneinander getrennt haben: Empfangen wir die Daten, wissen wir nicht, woher die Zustandsänderungen stammen. Beim Auslösen der Nachrichten wissen wir nicht, ob und wie der Zustand geändert wird und wer über die Änderungen informiert wird. Die Verantwortung wurde komplett an die zentrale Zustandsverwaltung übertragen, und wir haben das System stark entkoppelt. + +### Berechnung des Zustands auslagern + +Mit der Idee von Nachrichten zum Datenaustausch und zur (Pseudo-)Unveränderlichkeit im Hinterkopf wollen wir die Verwaltung des Zustands erneut überdenken. Bisher haben wir das State-Objekt direkt als Property im Service gepflegt und bei Änderungen über das Subject ausgegeben. Der Service hat dabei zwei Verantwortlichkeiten: den zentralen State zu halten und alle Änderungen zu berechnen. + +Für unser kurzes Beispiel mit einem Counter ist dies kein Problem, denn wir haben nur wenige Zeilen Code. Wenn allerdings unsere Anwendung und damit die Zustandsverwaltung komplexer wird, so wächst auch der zentrale Service mit jedem Feature immer weiter an. Bald entsteht ein „God Object“, und das müssen wir verhindern. + +Die Lösung des Problems ist, die Berechnung des Zustands in eine weitere unabhängige Funktion auszulagern. Wenn wir die Funktion richtig planen, so können wir die Berechnung bei zunehmender Komplexität auch in viele unabhängige Funktionen aufteilen. Weiterhin sollten diese ausgelagerten Funktionen keinen eigenen Zustand besitzen (engl. *stateless*), sodass sie bei gleichen Eingangswerten stets die gleichen Ausgangswerte erzeugen. Dadurch werden die Funktionen einfacher testbar. + +Über die gesamte Laufzeit der Anwendung betrachtet basiert unser Service auf einem Strom von Nachrichten, die jeweils Zustandsänderungen auslösen können. Wir besitzen die Grundlage für ein reaktives System, nun müssen wir uns diese Eigenschaft nur noch mithilfe unserer ausgelagerten Funktionen zunutze machen. Dazu entwickeln wir zunächst die Funktion, die für jede eintreffende Nachricht entscheidet, ob und wie der Zustand verändert werden soll. + +Den Datenfluss können wir dabei ganz einfach halten: Die Funktion erhält als Argumente den aktuell herrschenden Zustand und die eintreffende Nachricht. Die Fallunterscheidung anhand der Nachricht können wir mit einer *switch/case*-Anweisung realisieren. + +```ts +// Zustand berechnen anhand einer Nachricht +function calculateState(currentState: State, action: any): State { + switch (action.type) { + case 'INCREMENT': + return { ...currentState, counter: currentState.counter + 1 }; + default: + return currentState; + } +} +``` + +Der Zustand wird also durch jede eintreffende Nachricht berechnet. Wenn Änderungen durchgeführt werden sollen, so gibt die Funktion ein neues Objekt zurück, denn wir wollen den Zustand ja unveränderlich behandeln. Trifft eine unbekannte Nachricht ein, so ist keine Änderung notwendig. Wir müssen in diesem Fall das vorherige State-Objekt unverändert zurückgeben. Unser zentraler Service kann also mithilfe der neuen Funktion wie folgt angepasst werden. + +In diesem Schritt wurde unser System in zwei Teile aufgeteilt. Der Service hält weiterhin den State, die Berechnung wird von einer ausgelagerten Funktion durchgeführt. Durch diese Trennung bleibt der Service schlank und übersichtlich. + +### Deterministische Zustandsänderungen + +In JavaScript existiert die Methode `Array.reduce()`. Sie hat die Aufgabe, die Werte eines Arrays auf einen einzigen Wert zu reduzieren. Dabei wird für jeden Wert ein Callback aufgerufen, das die Reduktion durchführt. Die einfachste Form einer solchen Reduktion ist eine Summenbildung. + +Die Signatur unserer zuvor ausgelagerten Funktionen entspricht bereits einem solchen Callback, wie es auch für `Array.reduce()` verwendet wird. Unseren Zustand können wir demnach auch so berechnen: Es existiert ein Array von nacheinander abzuarbeitenden Nachrichten. Mithilfe von `Array.reduce()` summieren wir alle Nachrichten auf und verwenden dafür die Anweisungen aus der Funktion `calculateState()`. + +Mit einer solchen Reducer-Funktion und einer Liste von Nachrichten können wir demnach jeden gewünschten Zustand erzeugen. Wichtig ist dabei vor allem, dass die Reducer-Funktion eine *Pure Function* ist. Sie liefert also für die gleichen Eingabewerte stets die gleiche Ausgabe und erzeugt keine Seiteneffekte. Dazu darf die Funktion ausschließlich die übergebenen Parameter verwenden und keinen eigenen Zustand verwalten. + +In den vorherigen Beispielen haben wir allerdings kein Array von Nachrichten verwendet, sondern alle eingehenden Nachrichten wurden direkt an `calculateState()` weitergegeben. Wir wollen den Service nun etwas umstrukturieren: Dazu setzen wir ein neues Subject ein, das alle Nachrichten nacheinander in einem Datenstrom `messages$` liefert. Die Methode `dispatch()` leitet die Nachrichten also nur an das Subject weiter. + +Wir wollen dann erneut die Funktion `calculateState()` nutzen, um aus der Sammlung aller Nachrichten den jeweils neuen Zustand zu generieren. Dieses Mal greifen wir auf das große Toolset von RxJS zurück und verwenden den Operator `scan()`. Das ehemalige `BehaviorSubject` für den State wird von `shareReplay(1)` abgelöst, um das resultierende Observable mit allen Subscribern zu teilen und den jeweils letzten Wert an alle neuen Subscriber zu übermitteln. Um den Prozess einmalig anzustoßen, nutzen wir außerdem den Operator `startWith()` und erzeugen ein erstes Element im Strom der Nachrichten. + +Das Ergebnis ist das Observable `state$`, das für jede eintreffende Nachricht den neuen Zustand ausgibt, der von der Funktion `calculateState()` berechnet wurde. Ausgehend vom Startzustand werden also alle Nachrichten „auf summiert“ – daraus ergibt sich immer der aktuelle Zustand. Mithilfe von `scan()` müssen wir das zentrale Objekt nicht mehr selbst pflegen; dies erledigt nun RxJS für uns. + +Erneut haben wir unsere Zustandsverwaltung verbessert. Der Zustand ist nun aus den gesendeten Nachrichten abgeleitet. Ist die Historie aller Nachrichten bekannt, so kann man theoretisch jeden bisherigen Zustand jederzeit wieder reproduzieren, sofern unsere Reducer-Funktionen deterministisch sind. Diese Eigenschaften sorgen für ein sehr einfaches und gleichzeitig robustes System. Da die Funktionen sehr simpel sind, sind sie auch sehr einfach zu testen. + +### Zusammenfassung aller Konzepte + +Wir wollen die entwickelte Idee kurz zusammenfassen: + +Wir besitzen einen zentralen Service, der Nachrichten empfängt. Diese Nachrichten können von überall aus der Anwendung gesendet werden: aus Komponenten, anderen Services usw. Der Service kennt den Startzustand der Anwendung, der als ein zentrales Objekt abgelegt ist. Jede eintreffende Nachricht kann Änderungen an diesem Zustand auslösen. Der Service kennt dafür die passenden Anleitungen, wie die Nachricht zu behandeln ist und welche Änderungen am Zustand dadurch ausgelöst werden. Wird ein neuer Zustand erzeugt, wird er an alle Subscriber über ein Observable übermittelt. Jede interessierte Instanz in der Anwendung kann also die Zustandsänderungen abonnieren. + +Der Lesefluss und der Schreibfluss wurden vollständig entkoppelt: Die Komponenten erhalten die Daten über ein Observable und senden Nachrichten in den Service. Der Service ist die *Single Source of Truth* und hat als einziger Teil der Anwendung die Hoheit darüber, Nachrichten und Zustandsänderungen zu verarbeiten. + +Wir haben schrittweise ein robustes Modell für zentrales State Management entwickelt und dabei die Idee des Redux-Patterns kennengelernt. + +## Das Architekturmodell Redux + +Redux ist ein populäres Pattern zur Zustandsverwaltung in Webanwendungen. Die Idee von Redux stammt ursprünglich aus der Welt des JavaScript-Frameworks React, das neben Angular eines der populärsten Entwicklungswerkzeuge für Single-Page-Anwendungen ist. Redux ist dabei zunächst eine Architekturidee, es gibt aber auch eine konkrete Implementierung in Form einer Bibliothek. + +Der zentrale Bestandteil der Architektur ist ein *Store*, in dem der gesamte Anwendungszustand als eine einzige große verschachtelte Datenstruktur hinterlegt ist. Der Store ist die *Single Source of Truth* für die Anwendung und enthält alle Zustände: vom Server heruntergeladene Daten, gesetzte Einstellungen, die aktuell geladene Route oder Infos zum eigenen Account – alles, was sich zur Laufzeit in der Anwendung verändert und den Zustand beschreibt. + +Das State-Objekt im Store hat zwei elementare Eigenschaften: Es ist *immutable* und *read-only*. Wir können die Daten aus dem State nicht verändern, sondern ausschließlich lesend darauf zugreifen. Möchten wir den State „verändern“, so muss das existierende Objekt durch eine Kopie ausgetauscht werden, die die Änderungen enthält. Solche Änderungen am State werden durch Nachrichten ausgelöst, die aus der Anwendung in den Store gesendet werden. Die Grundidee dieser Architektur haben wir bereits in der Einleitung zu diesem Kapitel gemeinsam entwickelt. + +Neben dem zentralen Store mit dem State-Objekt verwendet Redux zwei weitere wesentliche Bausteine: Alle fachlichen Ereignisse in der Anwendung werden mit Nachrichten abgebildet – im Kontext von Redux nennt man diese Nachrichten *Actions*. Eine Action wird von der Anwendung (z. B. von den Komponenten) in den Store gesendet (*dispatch*) und kann eine Zustandsänderung auslösen. Im Store werden die eingehenden Actions von *Reducers* verarbeitet. Diese Funktionen nehmen den aktuellen State und die neue Action als Grundlage und errechnen daraus den neuen State. + +Bringt man diese Bausteine in den Kontext des einführenden Beispiels, so entspricht der zentrale Service dem Store von Redux. Die gesendeten Nachrichten entsprechen den Actions. Die Funktion `calculateState()`, die wir zur Veranschaulichung verwendet haben, ist genauso aufgebaut wie die Reducers von Redux. Der Operator `scan()` ist tatsächlich auch die technische Grundlage des Frameworks NgRx, das wir in diesem Kapitel für das State Management nutzen werden. + +### Redux und Angular + +Die originale Implementierung von Redux stammt aus der Welt von React. Alle enthaltenen Ideen können aber problemlos auch auf die Architektur einer Angular-Anwendung übertragen werden. Es existieren verschiedene Frameworks und Bibliotheken, die ein zentrales State Management für Angular ermöglichen. Sie alle folgen der grundsätzlichen Idee von Redux: + +- Reactive Extensions for Angular (NgRx) +- NGXS +- Akita +- Elf + +NgRx ist das bekannteste Projekt aus dieser Kategorie. Das Framework wurde von Mitgliedern des Angular-Teams aktiv mitentwickelt und gilt als De-facto-Standard für zentrales State Management mit Angular. Es lohnt sich außerdem, einen Blick auf die Community-Projekte NGXS und Elf zu werfen. + +Welches der Frameworks Sie für die Zustandsverwaltung einsetzen sollten, hängt von den konkreten Anforderungen und auch von persönlichen Präferenzen ab. Sie sollten alle Projekte vergleichen und Ihren Favoriten nach Kriterien wie Codestruktur und Features auswählen. Dazu möchten wir Ihnen einen Blogartikel empfehlen, in dem NgRx, NGXS, Akita und eine eigene Lösung mit RxJS gegenübergestellt werden: https://ordina-jworks.github.io/angular/2018/10/08/angular-state-management-comparison.html + +--- + +**Weiter zu Teil 2:** [Global Store mit NgRx](/material/ngrx-global-store) + +*Dieser Artikel ist ein 1:1-Port des Kapitels „State Management mit Redux und NgRx“ aus dem Buch „Angular: Das große Praxisbuch (4. Auflage)“.* +``` \ No newline at end of file From 8f94214938babf443503b52aeab457185876c202 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Fri, 12 Jun 2026 15:12:32 +0200 Subject: [PATCH 02/24] =?UTF-8?q?intro=20=C3=BCberarbeitung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- material/ngrx-intro/README.md | 210 ++++++++++++++++++++++------------ 1 file changed, 138 insertions(+), 72 deletions(-) diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md index 4d6a7022..cc6e2cc1 100644 --- a/material/ngrx-intro/README.md +++ b/material/ngrx-intro/README.md @@ -7,10 +7,11 @@ hidden: true **Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (4. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** -Dieser Artikel ist **Teil 1** einer zweiteiligen Serie zum Thema State Management mit NgRx. +Dieser Artikel ist **Teil 1** einer dreiteiligen Serie zum Thema State Management mit NgRx. - Teil 1: Wie kommen wir zu zentralem State Management? (dieser Artikel) - Teil 2: Global Store mit NgRx → [zum Artikel](/material/ngrx-global-store) +- Teil 3: SignalStore → [zum Artikel](/material/ngrx-signal-store) [[toc]] @@ -29,7 +30,7 @@ Mit wachsender Größe der Anwendung ergeben sich immer wieder folgende Fragen: - Wie reagieren wir an verschiedenen Stellen auf Ereignisse, die in der Anwendung auftreten? - Wie verwalten wir die Daten, die über die gesamte Anwendung verteilt sind? -Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookStoreService`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter gehen: Bislang waren Komponenten die „Hüterinnen“ der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine abgeschottete Einheit zu den anderen Komponenten. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. +Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookStoreService`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter gehen: Bislang waren Komponenten die "Hüterinnen" der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine abgeschottete Einheit zu den anderen Komponenten. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. Wir wollen in diesem Kapitel besprechen, wie eine solche zentrale Zustandsverwaltung (engl. *State Management*) realisiert werden kann. Dabei lernen wir das Architekturmuster *Redux* kennen und nutzen das populäre Framework *Reactive Extensions for Angular (NgRx)*, um den Anwendungszustand zu verwalten und unsere Prozesse zu koordinieren. @@ -42,19 +43,16 @@ Um uns der Idee des zentralen State Managements von Redux zu nähern, wollen wir Um alle Daten und Zustände zu zentralisieren, legen wir in einem zentralen Service ein Zustandsobjekt ab. Wir definieren die Struktur dieses Objekts mit einem Interface, um von einer starken Typisierung zu profitieren. Als möglichst einfaches Beispiel dient uns eine Zahl, die man mithilfe einer Methode hochzählen kann. Dieser *State* kann natürlich noch weitere Eigenschaften besitzen; wir haben dies mit dem Property `anotherProperty` angedeutet. ```ts -// Service mit zentralem Zustand -export interface State { +export interface MyState { counter: number; anotherProperty: string; } -@Injectable({ - providedIn: 'root' -}) +@Injectable({ providedIn: 'root' }) export class StateService { - state: State = { + state: MyState = { counter: 0, - anotherProperty: 'foo' + anotherProperty: 'foobar' }; incrementCounter() { @@ -66,26 +64,23 @@ export class StateService { Unser Service hält ein Objekt mit einem initialen Zustand, das über die Methode `incrementCounter()` manipuliert werden kann. Alle Komponenten können diesen Service anfordern und die Daten aus dem Objekt nutzen und verändern. Die Change Detection von Angular hilft uns dabei, automatisch bei Änderungen die Views der Komponenten zu aktualisieren. ```ts -// Zentralen Zustand in der Komponente verwenden -@Component({ ... }) +@Component({ /* ... */ }) export class MyComponent { - constructor(private stateService: StateService) {} - - get counter() { - return this.stateService.state.counter; - } - - increment() { - this.stateService.incrementCounter(); - } + constructor(public service: StateService) {} } ``` Den injizierten `StateService` können wir dann im Template nutzen, um die Daten anzuzeigen und die Methode `incrementCounter()` auszulösen: +> **Hinweis:** Services sollten nicht direkt im Template verwendet werden, um die Abhängigkeiten auf eine konkrete Implementierung zu verringern. Deshalb werden injizierte Services in der Regel `private` gesetzt. Um das vorliegende Beispiel einfach zu halten, verzichten wir hier allerdings darauf. + ```html -

Counter: {{ counter }}

- +
+ {{ service.state.counter }} +
+ ``` Wir haben in einem ersten Schritt unseren Zustand zentralisiert. Der Mehrwert zu einer isolierten Lösung besteht darin, dass alle Komponenten denselben Datensatz verwenden und anzeigen. Der Ort der Datenhaltung ist klar definiert, und es gibt keine Datensilos bei den einzelnen Komponenten. @@ -101,25 +96,15 @@ Für unser Beispiel eignet sich ein `BehaviorSubject`. Seine wichtigste Eigensch Wir setzen im Service zunächst die Eigenschaft `state` auf privat, sodass man nun gezwungen ist, das Observable `state$` zu verwenden, anstatt direkt auf das Objekt zuzugreifen. Wird `incrementCounter()` aufgerufen und der State aktualisiert, so lösen wir das `BehaviorSubject` mit dem aktuellen State-Objekt aus. So werden alle Subscriber über den neuen Zustand informiert. ```ts -// Zentralen Zustand mit Subject verwenden -import { BehaviorSubject } from 'rxjs'; - -export interface State { /* ... */ } - @Injectable({ providedIn: 'root' }) export class StateService { - private state = new BehaviorSubject({ - counter: 0, - anotherProperty: 'foo' - }); - readonly state$ = this.state.asObservable(); + private state: MyState = { /* ... */ }; + + state$ = new BehaviorSubject(this.state); incrementCounter() { - const current = this.state.value; - this.state.next({ - ...current, - counter: current.counter + 1 - }); + this.state.counter++; + this.state$.next(this.state); } } ``` @@ -127,22 +112,26 @@ export class StateService { Unsere Komponenten können nun die Informationen aus dem Subject beziehen. Der Operator `map()` hilft uns, schon in der Komponentenklasse die richtigen Daten aus dem State-Objekt zu selektieren. So erhalten wir z. B. ein Observable, das nur den fortlaufenden Counter-Wert ausgibt. ```ts -// Zustand vor der Verwendung transformieren -@Component({ ... }) +@Component({ /* ... */ }) export class MyComponent { - counter$ = this.stateService.state$.pipe( + counter$ = this.service.state$.pipe( map(state => state.counter) ); - constructor(private stateService: StateService) {} + // ... } ``` Im Template nutzen wir schließlich die AsyncPipe, um das Observable zu abonnieren. ```html -

Counter: {{ counter$ | async }}

- +
+ {{ counter$ | async }} +
+ + ``` Dieser Ansatz bietet einen Mehrwert zum vorherigen Beispiel: Die Komponenten teilen sich nicht nur die Daten, sie können auch reaktiv Änderungen entgegennehmen. Zusätzlich sind wir in der Lage, bei Bedarf die Strategie der Change Detection für die Komponente zu ändern und so in einem komplexeren Szenario gegebenenfalls die Performance zu optimieren. @@ -151,7 +140,7 @@ Dieser Ansatz bietet einen Mehrwert zum vorherigen Beispiel: Die Komponenten tei Unser Beispiel hat sich gut entwickelt, hat aber noch ein grundlegendes Designproblem. Wir halten unsere Daten in einem zentralen Objekt, das mit wachsender Größe der Anwendung ebenfalls größer wird. Alle Änderungen werden *direkt* an diesem Objekt durchgeführt, und wir geben es lediglich als Referenzparameter (*Call by Reference*) an die aufrufende Funktion weiter. Wir stellen uns nun vor, das Objekt hätte viele weitere Eigenschaften und eine verschachtelte Datenstruktur. Die Ereignisse zum Ändern der Daten können weiterhin aus diversen Gründen ausgelöst werden. Wie können wir nun effizient herausfinden, ob das Objekt bzw. ein Teil der verschachtelten Datenstruktur verändert wurde? Die Antwort lautet: Wir können dies nicht ohne zusätzlichen Aufwand realisieren. Um eine Änderung festzustellen, ist es notwendig, das Objekt mit einer zuvor erstellten Kopie zu vergleichen. Da wir mit Referenzen arbeiten, müssen wir jede Eigenschaft der verschachtelten Datenstruktur mit dem Gegenstück aus der Kopie vergleichen. -Das wollen wir ändern, indem wir das Objekt *unveränderlich* (engl. *immutable*) machen. Zur Erstellung von unveränderlichen Objekten gibt es verschiedene Bibliotheken, darunter das Projekt [Immutable.js](https://immutable-js.github.io/immutable-js/) oder die leichtgewichtige Bibliothek [Immer](https://github.com/immerjs/immer). Für ein simples Szenario genügt auch die native JavaScript-Methode `Object.freeze()`. Damit können wir ein Objekt „einfrieren“ und direkte Änderungen an den Daten verhindern. +Das wollen wir ändern, indem wir das Objekt *unveränderlich* (engl. *immutable*) machen. Zur Erstellung von unveränderlichen Objekten gibt es verschiedene Bibliotheken, darunter das Projekt [Immutable.js](https://immutable-js.github.io/immutable-js/) oder die leichtgewichtige Bibliothek [Immer](https://github.com/immerjs/immer). Für ein simples Szenario genügt auch die native JavaScript-Methode `Object.freeze()`. Damit können wir ein Objekt "einfrieren" und direkte Änderungen an den Daten verhindern. Dadurch ändert sich ein grundlegender Aspekt: Da Änderungen nicht mehr direkt am bisherigen Objekt möglich sind, werden wir gezwungen, das Objekt auszutauschen. Wir erzeugen hierfür bei jeder Änderung eine Kopie des vorherigen Objekts mit einer Ausnahme: dem zu ändernden Wert. Eine Änderung festzustellen ist nun sehr einfach: Wir müssen lediglich Referenzen vergleichen. Das ist kein Problem, da wir durch die Unveränderlichkeit sicher sein können, dass keine Änderung durch direkte Manipulation des Objekts möglich sein kann. Versehentliche Änderungen sind damit ebenfalls ausgeschlossen. @@ -160,16 +149,22 @@ Für die meisten Anwendungsfälle benötigen wir allerdings gar keine echte Unve Im folgenden Listing demonstrieren wir die Verwendung. Die Methode `incrementCounter()` nutzt den Spread-Operator, um eine Kopie des vorherigen Objekts und damit eine neue Referenz zu erzeugen. Im selben Schritt schreiben wir den neuen Wert des Zählers in die Eigenschaft `counter`. ```ts -// Objekte unveränderlich behandeln -incrementCounter() { - this.state.next({ - ...this.state.value, - counter: this.state.value.counter + 1 - }); +@Injectable({ providedIn: 'root' }) +export class StateService { + // ... + + incrementCounter() { + this.state = { + ...this.state, + counter: this.state.counter + 1 + }; + + this.state$.next(this.state); + } } ``` -Wir haben durch die „Pseudo-Immutability“ den Weg geebnet, um die Strategie für die Change Detection zu optimieren: Wenn ein Objekt bei einer Änderung stets eine neue Referenz erhält, so können wir in den Kindkomponenten die Strategie `OnPush` einsetzen. Dies kann die Performance der Anwendung entscheidend verbessern. +Wir haben durch die "Pseudo-Immutability" den Weg geebnet, um die Strategie für die Change Detection zu optimieren: Wenn ein Objekt bei einer Änderung stets eine neue Referenz erhält, so können wir in den Kindkomponenten die Strategie `OnPush` einsetzen. Dies kann die Performance der Anwendung entscheidend verbessern. Bitte beachten Sie, dass der Spread-Operator stets nur eine flache Kopie (Shallow Copy) eines Objekts erstellt. Ist ein Objekt oder Array verschachtelt, so müssen wir bei Änderungen auch immer das direkt betroffene Objekt kopieren. @@ -184,9 +179,13 @@ Der relevante Unterschied zu einem Methodenaufruf ist die Entkopplung: Dem Syste Für das Zählerbeispiel können wir beispielsweise die Nachrichten `INCREMENT`, `DECREMENT` und `RESET` vereinbaren, die von den Komponenten zum Service geschickt werden können. Die Methode `dispatch()` werden wir im nächsten Abschnitt noch genauer betrachten. Für den Moment vereinbaren wir, dass sie eine Nachricht entgegennimmt und für die gewünschte Zustandsänderung sorgt. ```ts -// Nachricht in den Service senden -dispatch(action: { type: string; payload?: any }) { - // ... +@Component({ /* ... */ }) +export class MyComponent { + constructor(private service: StateService) {} + + increment() { + this.service.dispatch('INCREMENT'); + } } ``` @@ -196,7 +195,7 @@ Wenn wir diese Architektur genauer betrachten, fällt auf, dass wir Lesen und Sc Mit der Idee von Nachrichten zum Datenaustausch und zur (Pseudo-)Unveränderlichkeit im Hinterkopf wollen wir die Verwaltung des Zustands erneut überdenken. Bisher haben wir das State-Objekt direkt als Property im Service gepflegt und bei Änderungen über das Subject ausgegeben. Der Service hat dabei zwei Verantwortlichkeiten: den zentralen State zu halten und alle Änderungen zu berechnen. -Für unser kurzes Beispiel mit einem Counter ist dies kein Problem, denn wir haben nur wenige Zeilen Code. Wenn allerdings unsere Anwendung und damit die Zustandsverwaltung komplexer wird, so wächst auch der zentrale Service mit jedem Feature immer weiter an. Bald entsteht ein „God Object“, und das müssen wir verhindern. +Für unser kurzes Beispiel mit einem Counter ist dies kein Problem, denn wir haben nur wenige Zeilen Code. Wenn allerdings unsere Anwendung und damit die Zustandsverwaltung komplexer wird, so wächst auch der zentrale Service mit jedem Feature immer weiter an. Bald entsteht ein "Gottobjekt" (engl. *God Object*), und das müssen wir verhindern. Die Lösung des Problems ist, die Berechnung des Zustands in eine weitere unabhängige Funktion auszulagern. Wenn wir die Funktion richtig planen, so können wir die Berechnung bei zunehmender Komplexität auch in viele unabhängige Funktionen aufteilen. Weiterhin sollten diese ausgelagerten Funktionen keinen eigenen Zustand besitzen (engl. *stateless*), sodass sie bei gleichen Eingangswerten stets die gleichen Ausgangswerte erzeugen. Dadurch werden die Funktionen einfacher testbar. @@ -205,34 +204,100 @@ Die Lösung des Problems ist, die Berechnung des Zustands in eine weitere unabh Den Datenfluss können wir dabei ganz einfach halten: Die Funktion erhält als Argumente den aktuell herrschenden Zustand und die eintreffende Nachricht. Die Fallunterscheidung anhand der Nachricht können wir mit einer *switch/case*-Anweisung realisieren. ```ts -// Zustand berechnen anhand einer Nachricht -function calculateState(currentState: State, action: any): State { - switch (action.type) { - case 'INCREMENT': - return { ...currentState, counter: currentState.counter + 1 }; - default: - return currentState; +function calculateState(state: MyState, message: string): MyState { + switch (message) { + case 'INCREMENT': { + return { + ...state, + counter: state.counter + 1 + }; + } + + case 'DECREMENT': { + return { + ...state, + counter: state.counter - 1 + }; + } + + case 'RESET': { + return { ...state, counter: 0 }; + } + + default: return state; } } ``` -Der Zustand wird also durch jede eintreffende Nachricht berechnet. Wenn Änderungen durchgeführt werden sollen, so gibt die Funktion ein neues Objekt zurück, denn wir wollen den Zustand ja unveränderlich behandeln. Trifft eine unbekannte Nachricht ein, so ist keine Änderung notwendig. Wir müssen in diesem Fall das vorherige State-Objekt unverändert zurückgeben. Unser zentraler Service kann also mithilfe der neuen Funktion wie folgt angepasst werden. +Der Zustand wird also durch jede eintreffende Nachricht berechnet. Wenn Änderungen durchgeführt werden sollen, so gibt die Funktion ein neues Objekt zurück, denn wir wollen den Zustand ja unveränderlich behandeln. Trifft eine unbekannte Nachricht ein, so ist keine Änderung notwendig. Wir müssen in diesem Fall das vorherige State-Objekt unverändert zurückgeben. Unser zentraler Service kann also mithilfe der neuen Funktion wie folgt angepasst werden: + +```ts +@Injectable({ providedIn: 'root' }) +export class StateService { + // ... + + dispatch(message: string) { + this.state = calculateState(this.state, message); + this.state$.next(this.state); + } +} +``` In diesem Schritt wurde unser System in zwei Teile aufgeteilt. Der Service hält weiterhin den State, die Berechnung wird von einer ausgelagerten Funktion durchgeführt. Durch diese Trennung bleibt der Service schlank und übersichtlich. ### Deterministische Zustandsänderungen -In JavaScript existiert die Methode `Array.reduce()`. Sie hat die Aufgabe, die Werte eines Arrays auf einen einzigen Wert zu reduzieren. Dabei wird für jeden Wert ein Callback aufgerufen, das die Reduktion durchführt. Die einfachste Form einer solchen Reduktion ist eine Summenbildung. +In JavaScript existiert die Methode `Array.reduce()`. Sie hat die Aufgabe, die Werte eines Arrays auf einen einzigen Wert zu reduzieren. Dabei wird für jeden Wert ein Callback aufgerufen, das die Reduktion durchführt. Die einfachste Form einer solchen Reduktion ist eine Summenbildung: + +```ts +const values = [1, 2, 3, 4]; +const reducer = (previous: number, current: number) => { + return previous + current; +}; + +// Erwartetes Ergebnis: 1 + 2 + 3 + 4 = 10 +const result = values.reduce(reducer, 0); +``` Die Signatur unserer zuvor ausgelagerten Funktionen entspricht bereits einem solchen Callback, wie es auch für `Array.reduce()` verwendet wird. Unseren Zustand können wir demnach auch so berechnen: Es existiert ein Array von nacheinander abzuarbeitenden Nachrichten. Mithilfe von `Array.reduce()` summieren wir alle Nachrichten auf und verwenden dafür die Anweisungen aus der Funktion `calculateState()`. -Mit einer solchen Reducer-Funktion und einer Liste von Nachrichten können wir demnach jeden gewünschten Zustand erzeugen. Wichtig ist dabei vor allem, dass die Reducer-Funktion eine *Pure Function* ist. Sie liefert also für die gleichen Eingabewerte stets die gleiche Ausgabe und erzeugt keine Seiteneffekte. Dazu darf die Funktion ausschließlich die übergebenen Parameter verwenden und keinen eigenen Zustand verwalten. +```ts +const initialState = { + counter: 0, + anotherProperty: 'foobar' +}; +const messages = ['INCREMENT', 'DECREMENT', 'INCREMENT']; + +const result = messages.reduce(calculateState, initialState); +// Erwartetes Ergebnis: { counter: 1, anotherProperty: 'foobar' } +``` + +Mit einer solchen Reducer-Funktion und einer Liste von Nachrichten können wir demnach jeden gewünschten Zustand erzeugen. Wichtig ist dabei vor allem, dass die Reducer-Funktion eine *Pure Function* ist. Sie liefert also für die gleichen Eingabewerte stets die gleiche Ausgabe und erzeugt keine Seiteneffekte. Dazu darf die Funktion ausschließlich die übergebenen Parameter verwenden und keinen eigenen Zustand verwalten. Wir gehen auf die Eigenschaften einer Pure Function später noch genauer ein. In den vorherigen Beispielen haben wir allerdings kein Array von Nachrichten verwendet, sondern alle eingehenden Nachrichten wurden direkt an `calculateState()` weitergegeben. Wir wollen den Service nun etwas umstrukturieren: Dazu setzen wir ein neues Subject ein, das alle Nachrichten nacheinander in einem Datenstrom `messages$` liefert. Die Methode `dispatch()` leitet die Nachrichten also nur an das Subject weiter. Wir wollen dann erneut die Funktion `calculateState()` nutzen, um aus der Sammlung aller Nachrichten den jeweils neuen Zustand zu generieren. Dieses Mal greifen wir auf das große Toolset von RxJS zurück und verwenden den Operator `scan()`. Das ehemalige `BehaviorSubject` für den State wird von `shareReplay(1)` abgelöst, um das resultierende Observable mit allen Subscribern zu teilen und den jeweils letzten Wert an alle neuen Subscriber zu übermitteln. Um den Prozess einmalig anzustoßen, nutzen wir außerdem den Operator `startWith()` und erzeugen ein erstes Element im Strom der Nachrichten. -Das Ergebnis ist das Observable `state$`, das für jede eintreffende Nachricht den neuen Zustand ausgibt, der von der Funktion `calculateState()` berechnet wurde. Ausgehend vom Startzustand werden also alle Nachrichten „auf summiert“ – daraus ergibt sich immer der aktuelle Zustand. Mithilfe von `scan()` müssen wir das zentrale Objekt nicht mehr selbst pflegen; dies erledigt nun RxJS für uns. +```ts +@Injectable({ providedIn: 'root' }) +export class StateService { + + private messages$ = new Subject(); + private initialState: MyState = { /* ... */ }; + + readonly state$ = this.messages$.pipe( + startWith('INIT'), + scan(calculateState, this.initialState), + shareReplay(1) + ); + + dispatch(message: string) { + this.messages$.next(message); + } +} +``` + +Das Ergebnis ist das Observable `state$`, das für jede eintreffende Nachricht den neuen Zustand ausgibt, der von der Funktion `calculateState()` berechnet wurde. Ausgehend vom Startzustand werden also alle Nachrichten "aufsummiert" – daraus ergibt sich immer der aktuelle Zustand. Mithilfe von `scan()` müssen wir das zentrale Objekt nicht mehr selbst pflegen; dies erledigt nun RxJS für uns. Erneut haben wir unsere Zustandsverwaltung verbessert. Der Zustand ist nun aus den gesendeten Nachrichten abgeleitet. Ist die Historie aller Nachrichten bekannt, so kann man theoretisch jeden bisherigen Zustand jederzeit wieder reproduzieren, sofern unsere Reducer-Funktionen deterministisch sind. Diese Eigenschaften sorgen für ein sehr einfaches und gleichzeitig robustes System. Da die Funktionen sehr simpel sind, sind sie auch sehr einfach zu testen. @@ -252,9 +317,13 @@ Redux ist ein populäres Pattern zur Zustandsverwaltung in Webanwendungen. Die I Der zentrale Bestandteil der Architektur ist ein *Store*, in dem der gesamte Anwendungszustand als eine einzige große verschachtelte Datenstruktur hinterlegt ist. Der Store ist die *Single Source of Truth* für die Anwendung und enthält alle Zustände: vom Server heruntergeladene Daten, gesetzte Einstellungen, die aktuell geladene Route oder Infos zum eigenen Account – alles, was sich zur Laufzeit in der Anwendung verändert und den Zustand beschreibt. -Das State-Objekt im Store hat zwei elementare Eigenschaften: Es ist *immutable* und *read-only*. Wir können die Daten aus dem State nicht verändern, sondern ausschließlich lesend darauf zugreifen. Möchten wir den State „verändern“, so muss das existierende Objekt durch eine Kopie ausgetauscht werden, die die Änderungen enthält. Solche Änderungen am State werden durch Nachrichten ausgelöst, die aus der Anwendung in den Store gesendet werden. Die Grundidee dieser Architektur haben wir bereits in der Einleitung zu diesem Kapitel gemeinsam entwickelt. +Das State-Objekt im Store hat zwei elementare Eigenschaften: Es ist *immutable* und *read-only*. Wir können die Daten aus dem State nicht verändern, sondern ausschließlich lesend darauf zugreifen. Möchten wir den State "verändern", so muss das existierende Objekt durch eine Kopie ausgetauscht werden, die die Änderungen enthält. Solche Änderungen am State werden durch Nachrichten ausgelöst, die aus der Anwendung in den Store gesendet werden. Die Grundidee dieser Architektur haben wir bereits in der Einleitung zu diesem Kapitel gemeinsam entwickelt. -Neben dem zentralen Store mit dem State-Objekt verwendet Redux zwei weitere wesentliche Bausteine: Alle fachlichen Ereignisse in der Anwendung werden mit Nachrichten abgebildet – im Kontext von Redux nennt man diese Nachrichten *Actions*. Eine Action wird von der Anwendung (z. B. von den Komponenten) in den Store gesendet (*dispatch*) und kann eine Zustandsänderung auslösen. Im Store werden die eingehenden Actions von *Reducers* verarbeitet. Diese Funktionen nehmen den aktuellen State und die neue Action als Grundlage und errechnen daraus den neuen State. +> **Hinweis:** Das State-Objekt ist theoretisch mutierbar, wir werden es aber stets immutable behandeln. NgRx setzt also auf die Pseudo-Immutability. + +Neben dem zentralen Store mit dem State-Objekt verwendet Redux zwei weitere wesentliche Bausteine: Alle fachlichen Ereignisse in der Anwendung werden mit Nachrichten abgebildet – im Kontext von Redux nennt man diese Nachrichten *Actions*. Eine Action wird von der Anwendung (z. B. von den Komponenten) in den Store gesendet (*dispatch*) und kann eine Zustandsänderung auslösen. Im Store werden die eingehenden Actions von *Reducers* verarbeitet. Diese Funktionen nehmen den aktuellen State und die neue Action als Grundlage und errechnen daraus den neuen State. Der Datenfluss in der Redux-Architektur ist in der folgenden Abbildung grafisch dargestellt. Hier ist gut erkennbar, dass die Daten stets in eine Richtung fließen und dass Lesen und Schreiben klar voneinander getrennt sind. + + Bringt man diese Bausteine in den Kontext des einführenden Beispiels, so entspricht der zentrale Service dem Store von Redux. Die gesendeten Nachrichten entsprechen den Actions. Die Funktion `calculateState()`, die wir zur Veranschaulichung verwendet haben, ist genauso aufgebaut wie die Reducers von Redux. Der Operator `scan()` ist tatsächlich auch die technische Grundlage des Frameworks NgRx, das wir in diesem Kapitel für das State Management nutzen werden. @@ -269,11 +338,8 @@ Die originale Implementierung von Redux stammt aus der Welt von React. Alle enth NgRx ist das bekannteste Projekt aus dieser Kategorie. Das Framework wurde von Mitgliedern des Angular-Teams aktiv mitentwickelt und gilt als De-facto-Standard für zentrales State Management mit Angular. Es lohnt sich außerdem, einen Blick auf die Community-Projekte NGXS und Elf zu werfen. -Welches der Frameworks Sie für die Zustandsverwaltung einsetzen sollten, hängt von den konkreten Anforderungen und auch von persönlichen Präferenzen ab. Sie sollten alle Projekte vergleichen und Ihren Favoriten nach Kriterien wie Codestruktur und Features auswählen. Dazu möchten wir Ihnen einen Blogartikel empfehlen, in dem NgRx, NGXS, Akita und eine eigene Lösung mit RxJS gegenübergestellt werden: https://ordina-jworks.github.io/angular/2018/10/08/angular-state-management-comparison.html +Welches der Frameworks Sie für die Zustandsverwaltung einsetzen sollten, hängt von den konkreten Anforderungen und auch von persönlichen Präferenzen ab. Sie sollten alle Projekte vergleichen und Ihren Favoriten nach Kriterien wie Codestruktur und Features auswählen. Dazu möchten wir Ihnen einen Blogartikel empfehlen, in dem NgRx, NGXS, Akita und eine eigene Lösung mit RxJS gegenübergestellt werden: [Angular state management comparison](https://ordina-jworks.github.io/angular/2018/10/08/angular-state-management-comparison.html). --- **Weiter zu Teil 2:** [Global Store mit NgRx](/material/ngrx-global-store) - -*Dieser Artikel ist ein 1:1-Port des Kapitels „State Management mit Redux und NgRx“ aus dem Buch „Angular: Das große Praxisbuch (4. Auflage)“.* -``` \ No newline at end of file From 9034bb54b1d1bf49e9e61dfe1978ad09b649eda6 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Fri, 12 Jun 2026 15:20:01 +0200 Subject: [PATCH 03/24] wip --- material/ngrx-global-store/README.md | 1665 ++++++++++++++++++++++---- 1 file changed, 1435 insertions(+), 230 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index 8ff43340..fc0bf436 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -1,258 +1,1463 @@ ---- -title: "State Management mit NgRx – Teil 2: Global Store mit NgRx" -published: "2026-06-11" -lastModified: "2026-06-11" -hidden: true ---- +## NgRx: Reactive Extensions for Angular -**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (4. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** +Das Framework *Reactive Extensions for Angular (NgRx)* ist eine der populärsten Implementierungen für State Management mit Angular. +Durch die gezielte Ausrichtung auf Angular fügt sich der Code gut in die Strukturen und Lebenszyklen einer Angular-Anwendung ein. +NgRx setzt stark auf die Möglichkeiten der reaktiven Programmierung mit RxJS, ist also an vielen Stellen von Observables und Datenströmen geprägt. +Die große Community und eine Reihe von verwandten Projekten machen NgRx zum wohl bekanntesten Werkzeug für Zustandsverwaltung mit Angular. -Dieser Artikel ist **Teil 2** einer zweiteiligen Serie zum Thema State Management mit NgRx. +Wir wollen in diesem Abschnitt die Struktur und die Bausteine in der Welt von NgRx genauer besprechen. +Außerdem wollen wir im BookMonkey einen Aspekt mithilfe von NgRx umsetzen, um so alle Bausteine auch praktisch zu üben. -- Teil 1: Wie kommen wir zu zentralem State Management? → [zum Artikel](/material/ngrx-intro) -- Teil 2: Global Store mit NgRx (dieser Artikel) +### Projekt vorbereiten -[[toc]] +Als Grundlage für diesen praktischen Teil verwenden wir das Beispielprojekt BookMonkey in der Version nach dem Kapitel zu Guards, also bevor wir die Anwendung auf Standalone Components umgestellt haben. +Möchten Sie mitentwickeln, so können Sie Ihr bestehendes Book\-Monkey-Projekt verwenden oder neu starten und den Code über GitHub herunterladen: -Im ersten Teil haben wir ein eigenes Modell für zentrales State Management entwickelt und die Grundprinzipien von Redux kennengelernt. Jetzt setzen wir das Ganze mit **NgRx** in die Praxis um. +\begin{clilisting}[caption={BookMonkey als Grundlage für NgRx verwenden},breakatwhitespace=true,belowskip=0pt] +git clone https://ng-buch.de/bm5-16-guards.git book-monkey-ngrx +cd book-monkey-ngrx +npm install +\end{clilisting} -## NgRx: Reactive Extensions for Angular +### Store einrichten -NgRx ist ein Set von Bibliotheken, das Redux-Architektur mit den reaktiven Möglichkeiten von RxJS in Angular integriert. +Im Projektverzeichnis müssen wir zunächst alle Abhängigkeiten installieren, die wir für die Arbeit mit NgRx benötigen. +NgRx verfügt über eigene Schematics zur Einrichtung in einem bestehenden Angular-Projekt. +Die folgenden Befehle integrieren einen vorbereiteten \mbox{Store} in die bestehende Anwendung: +\begin{clilisting}[style=nocaption, label=lst:ngrx:storeeinrichten] +ng add @ngrx/store --defaults +ng add @ngrx/store-devtools +ng add @ngrx/effects +\end{clilisting} -### Projekt vorbereiten -Wir arbeiten mit der BookMonkey-Variante `16e-ngrx`. -Installiere die notwendigen Pakete: -```bash -npm install @ngrx/store @ngrx/effects @ngrx/store-devtools -``` +Später wollen wir einen zusätzlichen Baustein kennenlernen, der im originalen Redux nicht vorgesehen ist und der spezifisch für NgRx ist: Effects auf Basis von `@ngrx/effects`. +Deshalb haben wir das notwendige Paket in diesem Schritt gleich mit eingefügt. +Die Store DevTools sind hilfreich zum Debugging der Anwendung – wir werden später in \autoref{sec:redux-developer-tools} ab \autopageref{sec:redux-developer-tools} genauer darauf eingehen, um den Lesefluss in diesem Kapitel nicht zu unterbrechen. -### Store einrichten +### Schematics nutzen -Im `AppModule` binden wir den Store ein: - -```ts -// app.module.ts -import { StoreModule } from '@ngrx/store'; -import { EffectsModule } from '@ngrx/effects'; -import { StoreDevtoolsModule } from '@ngrx/store-devtools'; -import { isDevMode } from '@angular/core'; - -@NgModule({ - imports: [ - // ... - StoreModule.forRoot({}, {}), - EffectsModule.forRoot([]), - StoreDevtoolsModule.instrument({ - maxAge: 25, - logOnly: !isDevMode() - }) - ], - // ... -}) -export class AppModule { } -``` +Um nach der Einrichtung die Bausteine von NgRx mithilfe der Angular CLI anzulegen, können wir das Paket `@ngrx/schematics` nutzen. +Es erweitert die Fähigkeiten der Angular CLI, sodass wir unsere Actions, Reducers und Effects bequem mithilfe von `ng generate` anlegen können. +Auch diese Abhängigkeit wird mittels `ng add` installiert. +\begin{clilisting}[style=nocaption] +ng add @ngrx/schematics +\end{clilisting} -### Feature anlegen -Wir legen für das Books-Feature einen eigenen Store-Bereich an. +\footnote{Schematic Collection festlegen}Die Schematics von NgRx werden durch diesen Aufruf automatisch im Projekt registriert. +\pagebreak +Jeder Aufruf von `ng generate` durchsucht dann auch die Skripte in diesem Paket. +So können wir bequem einen Befehl wie `ng \mbox{generate` \mbox{action}} verwenden, ohne die Zielkollektion explizit angeben zu müssen. +Die Collection wird mit einem Eintrag in der Datei \file{angular.json} festgelegt, den Sie jederzeit wieder löschen oder ändern können, falls Sie die Skripte von NgRx nicht mehr nutzen möchten. -```ts -// books/store/book.reducer.ts -import { createReducer, on } from '@ngrx/store'; -import { Book } from '../../shared/book'; -import * as BookActions from './book.actions'; +### Grundstruktur -export const bookFeatureKey = 'book'; +Die ausgeführten Befehle haben bereits alles Nötige eingerichtet, sodass wir sofort mit der Implementierung beginnen können. +Vorher wollen wir jedoch einen Blick auf die Änderungen werfen, die von den Schematics an unserem Projekt vorgenommen wurden. -export interface State { - books: Book[]; - loading: boolean; -} +Neben allen benötigten Abhängigkeiten in der \file{package.json} sind einige neue Imports im `AppModule` hinzugekommen. +Das `StoreModule` bringt den Kern des NgRx-Stores in die Anwendung. +Die verwendete Methode `forRoot()` erwartet zwei Argumente: +Im ersten Objekt können wir angeben, welche Reducers für welchen Teil des State-Objekts verantwortlich sind (eine sogenannte `ActionReducerMap`).\footnote{ActionReducerMap} +Üblicherweise nutzen wir dieses Objekt nicht, um die State-Struktur für unsere Features zu definieren, denn dafür existiert ein anderer, dynamischerer Weg. +Verwenden wir allerdings das Paket `@ngrx/router-store`, so müssen wir das Mapping für den Router hier statisch im `AppModule` konfigurieren.\footnote*{Auf das Paket `@ngrx/router-store` gehen wir zum Ende dieses Kapitels ab \autopageref{sec:redux:routing} noch ein.} +Im zweiten Argument von `forRoot()` können wir ein Konfigurationsobjekt übergeben.\footnote{Konfiguration für den Store} +Da wir diese beiden Aspekte momentan nicht nutzen wollen, sind lediglich zwei leere Objekte als Argumente angegeben. -export const initialState: State = { - books: [], - loading: false -}; - -export const reducer = createReducer( - initialState, - - on(BookActions.loadBooks, (state): State => { - return { ...state, loading: true }; - }), - - on(BookActions.loadBooksSuccess, (state, action): State => { - return { - ...state, - books: action.data, - loading: false - }; - }), - - on(BookActions.loadBooksFailure, (state): State => { - return { ...state, loading: false }; - }) -); -``` - -### Actions definieren - -```ts -// books/store/book.actions.ts -import { createAction, props } from '@ngrx/store'; -import { Book } from '../../shared/book'; - -export const loadBooks = createAction('[Book] Load Books'); - -export const loadBooksSuccess = createAction( - '[Book] Load Books Success', - props<{ data: Book[] }>() -); - -export const loadBooksFailure = createAction( - '[Book] Load Books Failure', - props<{ error: string }>() -); -``` - -### Reducer im Feature-Module registrieren - -```ts -// books/books.module.ts -import { StoreModule } from '@ngrx/store'; -import { EffectsModule } from '@ngrx/effects'; -import * as fromBook from './store/book.reducer'; -import { BookEffects } from './store/book.effects'; - -@NgModule({ - imports: [ - // ... - StoreModule.forFeature(fromBook.bookFeatureKey, fromBook.reducer), - EffectsModule.forFeature([BookEffects]) - ] -}) -export class BooksModule { } -``` - -### Selectors schreiben - -```ts -// books/store/book.selectors.ts -import { createFeatureSelector, createSelector } from '@ngrx/store'; -import * as fromBook from './book.reducer'; - -export const selectBookState = createFeatureSelector( - fromBook.bookFeatureKey -); - -export const selectBooksLoading = createSelector( - selectBookState, - state => state.loading -); - -export const selectAllBooks = createSelector( - selectBookState, - state => state.books -); -``` - -### Komponente mit Store verbinden - -```ts -// books/book-list/book-list.component.ts -import { Component } from '@angular/core'; -import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; - -import { Book } from '../../shared/book'; -import { loadBooks } from '../store/book.actions'; -import { selectAllBooks, selectBooksLoading } from '../store/book.selectors'; - -@Component({ - selector: 'bm-book-list', - templateUrl: './book-list.component.html' -}) -export class BookListComponent { - books$: Observable; - loading$: Observable; - - constructor(private store: Store) { - this.books$ = this.store.select(selectAllBooks); - this.loading$ = this.store.select(selectBooksLoading); - - this.store.dispatch(loadBooks()); - } -} -``` - -Template: - -```html -

Books

-
    -
  • - -
  • -
- -
Loading ...
-``` - -### Effects für HTTP-Seiteneffekte - -```ts -// books/store/book.effects.ts -import { Injectable } from '@angular/core'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; -import { catchError, map, switchMap } from 'rxjs/operators'; -import { of } from 'rxjs'; - -import * as BookActions from './book.actions'; -import { BookStoreService } from '../../shared/book-store.service'; - -@Injectable() -export class BookEffects { - - loadBooks$ = createEffect(() => { - return this.actions$.pipe( - ofType(BookActions.loadBooks), - switchMap(() => - this.service.getAll().pipe( - map(data => BookActions.loadBooksSuccess({ data })), - catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) - ) - ) - ); - }); - - constructor( - private actions$: Actions, - private service: BookStoreService - ) {} +Außerdem sind zwei weitere Imports für `Effects\-Module` und `Store\-DevTools\-Module` eingetragen worden. +Diese beiden Module binden Effects und die Store DevTools ein, wir wollen uns aber zu diesem Zeitpunkt noch nicht detaillierter damit auseinandersetzen. + +\lstinputlisting[caption={Imports für NgRx im `AppModule` \textup{(\file{app.module.ts})}}, label=lst:ngrx:appmoduleimports, language=js,breakatwhitespace=true,belowskip=1em]{\currentBm/src/app/app.module.ts.snippet} + +Mit dieser Konfiguration ist der Store zwar schon aktiv, aber wir haben noch nicht festgelegt, wie das zentrale State-Objekt strukturiert sein soll. +Da wir auch mit NgRx modular entwickeln, setzen wir diese Änderungen allerdings nicht direkt im `AppModule` um. +Stattdessen lagern wir alle neuen Bausteine in eigene Dateien aus und nutzen dafür die Angular~CLI. + +\begin{kasten} +\caption{NgRx und Standalone Components}Im Zusammenhang mit Standalone Components kann es sein, dass die Anwendung kein `AppModule` besitzt, sondern die `AppComponent` direkt gebootstrappt wird, siehe dazu der Abschnitt ab \autopageref{sec:17standalone:ohnemodule}. +Damit wir in diesem Fall nicht die Module importieren müssen, bietet NgRx verschiedene Funktionen an, um den Store einzurichten: `provideStore()`, `provideEffects()` und `provide\-State()`. +\end{kasten} + +### Feature anlegen + +Unsere Anwendung ist bereits in Module strukturiert, die einzelne Features der Anwendung kapseln. +Diese Einteilung findet sich auch wieder, wenn es um die Einrichtung des Stores für NgRx geht. +Jedes Feature erhält einen eigenen Satz an Actions, Reducers und Effects, die auch nur für genau dieses Feature und den zugehörigen State zuständig sind. +So verhindert man eine monolithische Struktur, in der verschiedene Zuständigkeiten ungewollt vermischt werden. +Um NgRx für ein existierendes Feature-Modul aufzusetzen, verwenden wir den folgenden Befehl: +\begin{clilisting}[style=nocaption,breakatwhitespace=true] +ng g feature books/store/book --module books/books --api --defaults +\end{clilisting} + +Dieser Aufruf legt das Feature `book` im Ordner \dir{src/app/books/store} an. +Wir haben hier bewusst den Unterordner \dir{store} gewählt, um alle Bestandteile von NgRx sauber in einem gemeinsamen Unterordner zu gruppieren. +\footnote{Feature-Name im Singular}Wichtig ist, dass der Feature-Name `book` hier im Singular angegeben wird, denn die CLI wird beim Anlegen automatisch ein Plural-s für einige Bausteine hinzufügen. +Die nötigen Imports und die Verdrahtung in der Anwendung sollen in das zugehörige `Books\-Module` integriert werden, auf das wir mit der Option \mbox{`-{`-module}} verweisen. +Mit der Option `-{`-api} generieren wir außerdem das nötige Grundgerüst, um Daten zu behandeln, die von einer API abgerufen werden. +Wie sich das auswirkt, werden wir gleich noch betrachten. +Die letzte Option \mbox{`-{`-defaults}} setzt weitere notwendige Einstellungen auf die vordefinierten Standardwerte. + +Die Dateistruktur in der Anwendung sieht nun wie folgt aus:\nopagebreak +\DDirtree{% + .1 \dir{src} . + .2 \dir{app} . + .3 \dir{books} . + .4 **\dir{store**} . + .5 **book.actions.ts** . + .5 **book.reducers.ts** . + .5 **book.selectors.ts** . + .5 **book.effects.ts** . + .5 \dots . + .4 books.module.ts . + .4 \dots . + .3 app.module.ts . + .3 \dots . } -``` -### Debugging mit Redux DevTools +Neben neuen Dateien sind im `BooksModule` weitere Imports hinzugekommen. +Hier wurde ebenfalls das `StoreModule` importiert, allerdings mit der Methode `forFeature()`. +Außerdem wurde eine Effects-Klasse mit dem `EffectsModule` registriert. + +\lstinputlisting[caption={NgRx einrichten im BooksModule \textup{(\file{books.module.ts})}}, label=lst:ngrx:booksmodule, language=js,breakatwhitespace=true]{\currentBm/src/app/books/books.module.ts.snippet} + +\footnote{State dynamisch erweitern}Dieser Aufruf von `forFeature()` ist essenziell, denn er definiert die Struktur des globalen State-Objekts. +Die Konstante `fromBook.bookFeatureKey` aus der Datei \file{book.reducer.ts} enthält den String `book`. +Damit wird festgelegt, unter welchem Namen die Zustände dieses Features im globalen State-Objekt zu finden sein werden. +Das zentrale State-Objekt wird also durch diesen Aufruf von `forFeature()` automatisch erweitert, und die Reducers aus dem Feature werden in die Anwendung integriert. +Diese dynamische Erweiterung ist nötig, damit ein Feature-Modul auch mithilfe von Lazy Loading asynchron zur Laufzeit nachgeladen werden kann.\footnote{Lazy Loading} +Der Feature-Key `book` verweist auf den Teilbaum im State-Objekt, in den der Feature-State eingebaut wird. + +### Struktur des Feature-States definieren + +Nun folgt der erste inhaltliche Schritt auf dem Weg zum State Management: +Wir müssen die Struktur des Feature-States für das Feature `book` definieren. +Dazu befindet sich in der Datei \file{books/store/book.reducer.ts} ein Interface mit dem Namen `State`. +Dieser Feature-State ist der erste Zweig des zentralen Objekt-Baums. + +\footnote{State-Interface}Im Interface `State` legen wir fest, welche Zustände und Daten wir speichern möchten. +Es soll zunächst nur darum gehen, die Buchliste vom Server abzurufen, im Store zu speichern und schließlich darzustellen. +Wir benötigen also eine Liste von Büchern und integrieren außerdem einen Ladeindikator. + +\footnote{Initialzustand}Direkt darunter befindet sich die Variable `initialState`. +Damit das System weiß, welche Zustände direkt nach dem Start herrschen, müssen wir hier einen initialen Zustand definieren. +Für unsere Anwendung ist die Buchliste beim Start leer und der Ladeindikator steht auf `false`: + +\lstinputlisting[firstline=8,lastline=16,caption={Feature-State und initialer Zustand \textup{(\file{book.reducer.ts})}}, label=lst:ngrx:featstateinitial, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.reducer.ts} + +Damit haben wir die Struktur unseres Feature-States definiert. +Die restlichen Inhalte der Datei ignorieren wir zunächst, darum werden wir uns im übernächsten Schritt kümmern. +Unser *gesamter* State der Anwendung hat jetzt den folgenden Aufbau: + +\pagebreakbeforelisting +\lstinputlisting[caption={Aufbau des gesamten State-Objekts}, label=lst:ngrx:statestructure, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/state-structure.ts} + +Der Name `book` wird durch den Feature-Key definiert, den wir an den Aufruf von `forFeature()` übergeben haben. + +### Actions: Kommunikation mit dem Store + + +Alle relevanten Ereignisse in der Anwendung werden durch Actions repräsentiert, die in den Store gesendet werden. +Das umfasst Aktionen, die direkt in der Oberfläche ausgeführt werden, und auch technische Ereignisse wie Antworten von der HTTP-Schnittstelle. + +\footnote{Actions beschreiben fachliche Ereignisse.}Dabei beschreiben Actions idealerweise eine abstrakte Sicht auf das Geschehen. +Actions sollten keine technischen Kommandos für das System darstellen, sondern die dahinterliegende Intention beschreiben. +Für unser Beispiel raten wir etwa von der Action `Show Loading Spinner` ab, denn sie beschreibt ein technisches Implementierungsdetail und kein fachliches Ereignis. + +Actions bilden die Grundlage für die Kommunikation mit dem \mbox{Store} und können Änderungen am Anwendungszustand auslösen. +Dieses Konzept entspricht den Nachrichten, die wir im einführenden Beispiel an den Service gesendet haben. +Die folgende Auflistung zeigt einige Beispiele für Actions, die in einer Anwendung vorkommen könnten: + +\vskip-4mm\begin{tabular}[t]{@{}p{.4\textwidth}p{.4\textwidth}@{}} + \begin{itemize} + \item Load Books + \item Load Books Success + \item Load Books Failure + \item Session Expired + \item Router Navigation + \item Window Resize + \item User Login + \item User Login Success + \end{itemize} + & + \begin{itemize} + \item User Login Failure + \item Add item to cart + \item Remove item from cart + \item Create book + \item Update book + \item Set language + \item Increment counter + \end{itemize} +\end{tabular} + +\vskip-2mm\noindent +Technisch ist eine solche Action immer ein Objekt mit einer bestimmten vorgegebenen Struktur. +Verpflichtend ist die Eigenschaft `type`, die den Namen der Nachricht angibt und die Nachricht unverwechselbar macht. +Jeder Type muss eindeutig sein und er darf immer nur einmal vergeben werden! +Zusätzlich können weitere optionale Eigenschaften definiert werden, um Daten in der Action zu transportieren: der sogenannte Payload, der im folgenden Beispiel `data` genannt wird. + +\pagebreakbeforelisting +\lstinputlisting[caption={Grundaufbau einer Action}, label=lst:ngrx:actionstructure, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-structure.ts} + +\footnote{Action Creator}Um eine starke Typisierung zu ermöglichen und Tippfehler zu vermeiden, notieren wir die Objekte jedoch nicht direkt im Code. +Stattdessen nutzen wir einen sogenannten *Action Creator* – eine Funktion, die das Objekt mit der richtigen Struktur erzeugt. + +Dafür stellt NgRx die Funktion `createAction()` zur Verfügung. +Als erstes Argument geben wir hier immer den Namen der Action an. +Dieser `type` muss in der gesamten Anwendung eindeutig sein. +Um die Nachvollziehbarkeit zu erhöhen und mögliche Kollisionen zu verhindern, wird üblicherweise die Quelle der Action in eckigen Klammern im Namen notiert. +Damit erzeugen wir eine Art Namespace für den Action-Typ – es handelt sich dabei aber nur um eine Konvention. + +\lstinputlisting[caption={Action ohne Payload}, label=lst:ngrx:actionwithoutpayload, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-withoutpayload.ts} + +Diese Action besitzt noch keinen Payload. +Wollen wir weitere Daten in der Action verpacken, können wir die Struktur des Payloads im zweiten Argument von `createAction()` festlegen: + +\lstinputlisting[caption={Action mit Payload}, label=lst:ngrx:actionwithpayload, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-withpayload.ts} + +Der generische Typparameter der Funktion `props()` gibt an, welche zusätzlichen Eigenschaften die Action enthalten soll. +Wir haben hier den generischen Namen `data` gewählt, Sie können die Payload-Propertys allerdings nach Belieben benennen. +Das erzeugte Action-Objekt hat den folgenden Aufbau: + +\lstinputlisting[caption={Struktur der Action}, label=lst:ngrx:actionstructurelbs, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-structure-lbs.ts} + +Zur Abfrage einer HTTP-Schnittstelle benötigt man normalerweise drei zusammengehörige Actions, die diesem Muster folgen: +\begin{itemize} + \item `Load XXX`: Daten anfragen + \item `Load XXX Success`: Daten sind erfolgreich vom Server eingetroffen. + \item `Load XXX Failure`: Das Laden der Daten ist fehlgeschlagen. +\end{itemize} + +Die Actions werden in einer oder mehreren Dateien gesammelt. +Beim Anlegen des Features mit `ng generate feature` wurde eine solche Datei bereits erstellt: +\file{books/store/book.actions.ts}. +Da wir das Feature mit der Option `-{`-api} angelegt haben, sind in dieser Datei bereits die ersten drei Actions vorbereitet. +Wir können dieses Grundgerüst nutzen und die Signaturen der Actions für unseren Anwendungsfall anpassen. + +Die Success-Action erhält als Payload eine Buchliste `Book[]`, die Failure-Action transportiert einen Fehler vom Typ `string`.\footnote*{In der Praxis ist es sinnvoll, hier ein eigenes Fehlerobjekt zu verwenden, das mehr Informationen beinhaltet als nur den Fehlertext.} +Die Ac\-tion `loadBooks` benötigt keine weiteren Daten, denn die Intention wird schon durch den Action-Typ vollständig ausgedrückt. + +\lstinputlisting[caption={Actions für das Feature `book` \textup{(\file{book.actions.ts})}}, label=lst:ngrx:bookactions, language=js,breakatwhitespace=true,belowskip=0pt]{\currentBm/src/app/books/store/book.actions.ts} + +### Dispatch: Actions in den Store senden + + +Um mit dem Store zu kommunizieren und Zustandsänderungen anzustoßen, müssen die Actions von den Komponenten in den Store gesendet werden. +Der Store kann dazu als Service in die Komponente injiziert werden. + +Der Store verfügt über eine Methode `dispatch()`, mit der wir eine Action in den Store dispatchen können. +Beim Aufruf der `BookList\-Component` soll das Laden der Buchliste angestoßen werden. +Deshalb lösen wir dort gleich im Konstruktor die Action `loadBooks` aus. + +Wichtig ist, dass das exportierte `loadBooks` aus der Datei \file{book"".\mbox{actions}"".ts} selbst noch keine Action ist, sondern ein Action Creator, der ein Action-Objekt erzeugen kann. +Dazu muss die Funktion aufgerufen werden. +Hat die Action einen Payload, so wird er als Argument an den Action Creator übergeben: + +\lstinputlisting[caption={Action Creator verwenden}, label=lst:ngrx:actioncreatoruse, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-creator-use.ts} + +Wir müssen die Funktion `loadBooks` also aufrufen, um ein Action-Objekt zu erhalten, das wir dispatchen können: + +\lstinputlisting[caption={Action dispatchen \textup{(\file{book-list"".component.ts})}}, label=lst:ngrx:dispatchloadbooksblc, language=js,breakatwhitespace=true]{\currentBm/src/app/books/book-list/book-list.component.ts.snippet} + +\footnote{Redux DevTools}Wenn Sie bereits die Redux DevTools installiert haben, so können Sie nun überprüfen, ob die ausgelöste Action tatsächlich im Store eingetroffen ist. +Wir betrachten die DevTools separat in \autoref{sec:redux-developer-tools} ab \autopageref{sec:redux-developer-tools}. + +### Reducers: den State aktualisieren + + +Nachdem wir die erste Action in den Store dispatcht haben, ist es nun an der Zeit, einen Reducer zu entwickeln, um den State zu verändern. +Ein Reducer im Kontext von Redux ist eine Funktion mit zwei Eingabewerten: der aktuelle Zustand und die neu eintreffende Action. +Die Aufgabe des Reducers ist es, anhand der Action und des Zustands einen neuen Zustand zu berechnen und zurückzugeben: + +\pagebreakbeforelisting +\lstinputlisting[caption={Signatur eines Reducers}, label=lst:ngrx:reducersignature, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/reducer-signature.ts} + +Ein Reducer ist dabei immer für einen Teilbaum des States zuständig. +Das Feature `book` besitzt einen eigenen Reducer, der ausschließlich diesen State verarbeitet. +\footnote{State-Slice}Ein solcher Feature-State wird auch *Slice* genannt. Wir verwenden die beiden Begriffe synonym. + +In unserem einführenden Beispiel haben wir zur Unterscheidung der Nachrichten ein *switch/case*-Statement verwendet. +Traditionell wird in Redux auch genau dieser Ansatz genutzt. +Der Einsatz von *switch/case* ist jedoch etwas gewöhnungsbedürftig, und auch die Menge an erforderlichem Code ist recht hoch. +Daher stellt NgRx die Funktion `create\-Reducer()` zur Verfügung, um Reducers sehr kompakt und typsicher zu implementieren. +Die grundsätzliche Idee ist jedoch dieselbe: +Der Reducer unterscheidet nach dem Action-Typ und nimmt für verschiedene Actions verschiedene Anpassungen am State vor. +Wir entwickeln also für jede Action eine kleine Reducer-Funktion. + +\footnote{Reducers sind Pure Functions.}Die wichtigste Eigenschaft der Reducer-Funktionen ist ihre Reinheit : Reducers sind Pure Functions. +Dieses Konzept ist von drei wesentlichen Einschränkungen geprägt: +\begin{itemize} + \item **deterministisch:** Die Funktion liefert für gleiche Eingabewerte stets die gleiche Ausgabe. Es dürfen also keine Werte verarbeitet werden, die nicht zweifelsfrei aus den Eingaben ableitbar sind, z.\,B. Zufallswerte oder die Uhrzeit. + \item **keine äußeren Zustände:** Es werden nur die Daten verarbeitet, die als Argumente an die Funktion übergeben werden (also hier: State und Action). Es darf nicht auf andere Variablen zugegriffen werden, die außerhalb der Funktion liegen. Eine Ausnahme bilden ausgelagerte Hilfsfunktionen, die allerdings dieselben Anforderungen an eine Pure Function erfüllen müssen. + \item **keine Seiteneffekte:** Die Funktion darf keine Aktionen ausführen, die einen Effekt außerhalb ihres Gültigkeitsbereichs haben. HTTP-Requests, Logging, Authentifizierung oder Dispatchen von Actions sind also in den Reducers *nicht* erlaubt. Zu Seiteneffekten zählt auch, das State-Objekt direkt zu manipulieren! +\end{itemize} + +Die Einhaltung dieser Einschränkungen ist besonders wichtig, um eine hohe Stabilität des Systems sicherzustellen. +Nur wenn wir den strikten Regeln von Redux folgen, kann der Anwendungszustand zuverlässig kontrolliert werden. + +Ein Reducer darf deshalb ausschließlich den aktuellen State und die eintreffende Action verarbeiten. +Alle notwendigen Informationen, um den neuen State zu erzeugen, müssen in State oder Action vorliegen. +\footnote{Kopie erzeugen}Außerdem muss der Reducer bei Änderungen stets eine *Kopie* des States zurückgeben, die die gewünschten Änderungen beinhaltet. +Es dürfen niemals Änderungen direkt auf dem Objekt ausgeführt werden. +Diese Eigenschaft der Immutability haben wir bereits in der Einleitung besprochen. +Wir setzen mit NgRx in der Regel nicht auf echte Unveränderlichkeit, sondern wenden Disziplin an und behandeln die Objekte lediglich als unveränderlich – auch wenn sie prinzipiell veränderlich sind. +Während der Entwicklung sind außerdem sogenannte *Runtime Checks*\shortlinkFootnote{link-ngrx-runtimechecks} aktiv, die den State auf Unveränderlichkeit und Serialisierbarkeit prüfen. +Jede versehentliche Änderung am State führt dann direkt zu einer Exception. + +Um das State-Objekt vor der Verwendung und Änderung zu klonen, können wir den Spread-Operator einsetzen. +Bitte beachten Sie, dass dieses Werkzeug stets nur eine flache Kopie (Shallow Copy) erzeugt. +Wollen wir Änderungen an tiefer verzweigten Teilen des States vornehmen, müssen wir explizit eine tiefe Kopie (Deep Copy) des Objekts erzeugen.\footnote*{Ein verschachteltes Objekt kann mit dem Spread-Operator geklont werden, indem wir jeden Zweig des Objekts einzeln kopieren. Wird das zu komplex, empfehlen wir Ihnen, ein Hilfsmittel zu verwenden wie die native Funktion `structuredClone()`. Wir haben die verschiedenen Möglichkeiten in einem Blogartikel zusammengefasst: \shortlinkInline{link-schule-immutable}.} + +\footnote{Reducers für die Anwendung}Wir wollen nun auch für den BookMonkey passende Reducers entwickeln. +Dazu überlegen wir zunächst, welche Zustandsänderungen von den Actions ausgelöst werden: +\begin{itemize} + \item `loadBooks`: Ladeindikator auf `true` setzen + \item `loadBooksSuccess`: Buchliste einfügen und Ladeindikator auf `false` setzen + \item `loadBooksFailure`: Ladeindikator auf `false` setzen +\end{itemize} + +Die Implementierung bringen wir in der Datei \file{books/""store/""book"".\mbox{reducers}.ts} unter, in der das Grundgerüst der Reducer-Funktion bereits vorbereitet ist. +Statt *switch/""case* wird hier die Funktion `create\-Reducer()` genutzt. +Für jede Fallunterscheidung existiert ein Block, der in ein~`on()` gekapselt ist. +Als erstes Argument geben wir hier immer die Action an, die behandelt werden soll. +Im zweiten Argument ist die Reducer-Funktion notiert, die schließlich anhand der eingehenden Action den neuen State generiert. +Für die einzelnen Reducer-Funktionen sollten wir den Body in geschweiften Klammern definieren und von dort das State-Objekt direkt returnen. +Außerdem sollte der Rückgabetyp der Funktion immer explizit mit `State` definiert sein.{\tolerance5000\par} + +\pagebreakbeforelisting +\lstinputlisting[firstline=18,lastline=36,caption={Reducers für die Anwendung \textup{(\file{book.reducer.ts})}}, label=lst:ngrx:bookreducer, language=js,breakatwhitespace=true,belowskip=0pt]{\currentBm/src/app/books/store/book.reducer.ts} + +\definition{reducermeherereactions}{Reducers für mehrere Actions}{ +Übrigens wird jeder Reducer immer für jede Action durchlaufen. +Das bedeutet, dass Sie in einem Reducer auch auf mehrere Actions oder sogar auf Actions aus einem anderen Bereich der Anwendung reagieren können. +Dazu können Sie im `on()` mehrere Actions nacheinander als einzelne Argumente angeben. +Es gibt jedoch keine direkte Möglichkeit, in einem Reducer auf einen anderen Slice des States zuzugreifen.\footnotemark}\footnotetext{Um in einem Reducer einen anderen State-Slice zu lesen, müsste zunächst die Art und Weise geändert werden, wie NgRx die verschiedenen Reducers intern zusammenfasst. Die Funktion `combineReducers()` bietet dafür einen Ansatz.} + +Haben Sie die Implementierung abgeschlossen, so können Sie die State-Änderung in den Redux DevTools nachvollziehen. +Für die dispatchte Action `loadBooks` ändert sich das `loading`-Flag im State von \mbox{`false`} auf \mbox{`true`}. + +Die Action `loadBooksSuccess` wird aktuell niemals ausgelöst; das werden wir im übernächsten Schritt mit einem Effect lösen. +Wir haben trotzdem bereits definiert, was in diesem Fall mit dem State passieren soll. +Dasselbe gilt für `loadBooksFailure`. + +Für alle unbekannten Actions liefert der Reducer automatisch den aktuellen State unverändert zurück. +Wenn es also für eine Action keinen passenden Reducer gibt, führt das nicht zu einem Fehler. +Die Bestandteile der Architektur sind so stark entkoppelt, dass sie unabhängig voneinander entwickelt werden können. + +### Selektoren: Daten aus dem State lesen + +Lassen Sie uns kurz zusammenfassen, wie weit wir bisher gekommen sind: +Wir haben Action Creators definiert und die Action `loadBooks` von der `BookListComponent` aus in den Store dispatcht. +Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. + +Um den Kreislauf des Datenflusses zu schließen, wollen wir die Daten aus dem State nun auslesen und in der Komponente darstellen. +Dazu benötigen wir demnächst etwas Theorie und einen Blick hinter die Kulissen: +Der Kernbestandteil des Stores ist ein `BehaviorSubject` in Kombination mit dem Operator `scan()`. +Alle eingehenden Actions werden reduziert ( aufsummiert), indem die Reducer-Funktionen angewendet werden. +Auf diese Weise wird der jeweils aktuelle Zustand erzeugt und über das Observable ausgegeben. + +\footnote{Datenstrom von States}Der Store ist selbst ein Observable, das wir verwenden können, um in den Komponenten auf State-Änderungen zu reagieren. +Der Datenstrom gibt allerdings stets den vollständigen State aus. +Um einzelne Teile daraus zu selektieren, benötigen wir eine Projektion, die sich z.\,B. mit dem Operator `map()` realisieren lässt. +Das folgende Beispiel erstellt ein Observable, das nur das `loading`-Flag liefert: + +\lstinputlisting[caption={State auswählen mit `map()`}, label=lst:ngrx:selectmap, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/select-map.ts} + +\footnote{Datenfluss optimieren}Obwohl diese Herangehensweise theoretisch funktioniert, bringt sie ein konzeptionelles Problem mit sich: +Der Store gibt bei *jeder* Änderung den gesamten State aus. +Das Observable `loading\$` emittiert also auch dann einen neuen Wert, wenn sich das `loading`-Flag gar nicht geändert hat. +Werden die Anwendung und der State komplexer, führt das dazu, dass bei jeder noch so trivialen State-Änderung alle Observables in den Komponenten feuern und in der Folge die Change Detection von Angular getriggert wird. + +Das gilt es zu verhindern! +Wir wollen nur dann einen Wert ausgeben, wenn wirklich eine relevante Änderung an den Daten vorliegt. +Dazu könnten wir den Operator `distinctUntilChanged()` verwenden. +Das Framework NgRx bringt auf seinem `Store` allerdings eine eigene Methode mit, die alle nötigen Funktionalitäten bereits kombiniert: `\mbox{select`()}. + +\lstinputlisting[caption={Die Methode `select()` verwenden}, label=lst:ngrx:selectselect, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/select-select.ts} + +Das resultierende Observable gibt das Ergebnis nur aus, wenn der Wert sich tatsächlich verändert hat, wie die \autoref{fig:redux-selectorsgap} zeigt. + +\begin{fig} + {fig:redux-selectorsgap} + {Selektor mit `store.select()` verwenden} + {Selektor mit `store.select()` verwenden} + {10pt} + \vskip-1pt + \includegraphics[width=\textwidth]{images/statemanagement/selectors-gap} +\end{fig} + +\suppressindent +\footnote{Typisierung}Wenn wir das Beispiel so implementieren, stoßen wir allerdings auf ein weiteres Problem: +Die Struktur des States wird dynamisch verändert, wenn einzelne Feature-Module mithilfe von `forFeature()` einen Teilbaum des States registrieren. +Durch eine statische Typanalyse allein ist es also nicht möglich, die vollständige Struktur des States zu ermitteln: Der TypeScript-Compiler weiß nicht, dass das Property `book` im State existiert. + +\footnote{Komplexe Lesezugriffe}Dazu kommt, dass wir an dieser Stelle lediglich einen einfachen Teil des States selektiert haben. +In der Praxis werden aber die Lesezugriffe auf den State komplexer. +Mitunter wollen wir Daten nicht nur einfach auslesen, sondern Projektionen über verschiedene Teile des States ausführen. +Stellen Sie sich vor, Sie besitzen eine Liste von Autoren und Autorinnen und eine Liste von Büchern – wollen aber nun nur die Bücher ausgeben, die von einer bestimmten Person verfasst wurden. +Dazu ist zusätzliche Logik nötig, die nicht in die Komponenten gehört. +Stattdessen soll diese Logik in separate Funktionen ausgelagert werden, die unabhängig von den Komponenten sind. +Sie können eine solche Funktion mit einer Datenbankabfrage vergleichen: Die Query wird einmal definiert und kann beliebig komplex sein. +Verschiedene Teile der Anwendung können diese Query nutzen und die Daten genau im benötigten Format erhalten. +Die Funktionen zur Abfrage von Daten aus dem Store werden *Selektoren* genannt. + +Selektoren werden ebenfalls in eigenen Dateien untergebracht, die unabhängig von den Komponenten sind. +Dazu wurde bereits die Datei \file{books/store/book.selectors.ts} erstellt. +Zur Definition von Selektoren bringt NgRx eigene Hilfsfunktionen mit: `createFeatureSelector()` und `createSelector()`. +Sie sorgen unter anderem dafür, dass die selektierten Daten korrekt typisiert sind, auch wenn der State zur Laufzeit dynamisch erweitert wird. + +\footnote{Memoization}In einem solchen Selektor versteckt sich außerdem ein wichtiges Konzept, das sich *Memoization* nennt. +Damit aufwendige Projektionen nicht bei jeder State-Änderung neu ausgeführt werden, speichert jeder Selektor seine zuletzt verarbeiteten Eingabewerte. +Haben sich diese Werte nicht geändert, so wird auch die Projektion nicht neu ausgeführt, sondern das zuletzt berechnete Ergebnis wird erneut ausgegeben. +Das Prinzip der Memoization ist in die Selektoren bereits eingebaut, wenn wir die mitgelieferten Funktionen verwenden. +Damit das allerdings funktioniert, müssen wir Selektoren als Pure Functions definieren: Sie dürfen nur die Werte verarbeiten, die sie als Argumente erhalten, und es dürfen keine Seiteneffekte ausgeführt werden. + +\footnote{Feature-Selektor}Ausgehend davon, dass der Store immer den gesamten State liefert, müssen wir zunächst einen bestimmten Feature-State aus dem großen Objekt selektieren. +Dazu nutzen wir die Funktion `create\-Feature\-Selector()`. +Sie erstellt einen Selektor, der ein Feature anhand seines Namens auswählt. +Dabei verwenden wir den Feature-Key, den wir bereits bei `forFeature()` im `BooksModule` genutzt haben. +Damit diese Selektion typsicher funktioniert, müssen wir außerdem das Interface für den Feature-State angeben. +Um den State-Slice für das Feature `book` auszuwählen, benötigen wir also den folgenden Feature-Selektor, der bereits automatisch vorbereitet wurde:{\tolerance1000\par} + +\lstinputlisting[firstline=1,lastline=6,caption={Feature-Selektor erstellen \textup{(\file{book"".selectors.ts})}}, label=lst:ngrx:bookfeatselector, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.selectors.ts} + +Anschließend können wir weitere Selektoren mithilfe der Funktion `create\-Selector()` bauen. +Diese Funktion erhält als Argumente mehrere andere Selektoren. +Das Kernstück des Selektors ist schließlich die Projektionsfunktion, die im letzten Argument übergeben wird. +Sie erhält die Daten von allen zuvor angegebenen Selektoren als Argumente und liefert den daraus berechneten Wert zurück:{\tolerance1000\par} + +\lstinputlisting[caption={Schematischer Aufbau eines Selektors}, label=lst:ngrx:selectorstructure, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/selector-structure.ts} + +Wir können auf diese Weise ein komplexes Gerüst von Selektoren definieren. +Die Selektoren bauen aufeinander auf und kombinieren die Daten aus dem State so, wie sie in den Komponenten benötigt werden. +Jeder einzelne Selektor nutzt die Memoization, um die berechneten Werte zu cachen. +Die Berechnung in der Projektionsfunktion wird nur neu ausgeführt, wenn sich einer der Eingabewerte tatsächlich ändert. +So sind auch komplexe Projektionen in großen Anwendungen ohne Nachteile in der Performance möglich. + +Für den BookMonkey wollen wir zwei einfache Selektoren entwickeln, die uns Zugriff auf das `loading`-Flag und auf die Buchliste geben: + +\lstinputlisting[firstline=8,lastline=16,caption={Selektoren für die Anwendung \textup{(\file{book.selectors.ts})}}, label=lst:ngrx:bookselectors, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.selectors.ts} + +\footnote{Selektoren verwenden}In der `BookListComponent` verwenden wir nun diese beiden Funktionen, um Observables zu erstellen, die uns die benötigten Daten liefern. +Die Methode `store.select()` erhält dazu als Argument einen der eben definierten Selektoren. +Wir legen alle Observables direkt in der Komponentenklasse ab, denn wir wollen die Subscription im Template mit der Async\-Pipe erledigen. +Wenn Sie Ihre Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. +Sie müssen also nur selten in der Komponentenklasse direkt auf ein Observable aus dem Store sub\-scriben. + +Da wir das Observable `books\$` nun aus dem Store beziehen, benötigen wir den `BookStoreService` nicht mehr. +Wir können das Argument des Konstruktors also entfernen. + +\lstinputlisting[caption={Selektoren verwenden \textup{(\file{book-list"".component.ts})}}, label=lst:ngrx:useselectorsblc, language=js,breakatwhitespace=true]{\currentBm/src/app/books/book-list/book-list.component.ts.2.snippet} + +Im Template der Komponente nutzen wir die Observables, um die Daten darzustellen. +Das Observable `books\$` wird bereits korrekt verarbeitet. +Für den Ladeindikator können wir ein neues Element erstellen, das wir direkt mit `ngIf` ein- und ausblenden. +Unser globales Stylesheet bietet dafür die passende \acs{CSS}-Klasse `loader` an. + +\lstinputlisting[caption={Daten anzeigen \textup{(\file{book-list"".component.html})}}, label=lst:ngrx:blchtml, language=js,breakatwhitespace=true]{\currentBm/src/app/books/book-list/book-list.component.html.snippet} + +Wenn Sie die Anwendung nun aufrufen, sehen Sie den Ladeindikator. +Die Komponente ist nun deutlich loser gekoppelt als vorher, denn sie braucht keine eigene spezifische Logik mehr, um den Indikator anzuzeigen oder Bücher zu laden. +Sie bezieht lediglich Daten über Observables aus dem Store und löst Actions aus. + +### Effects: Seiteneffekte ausführen + +Wir haben nun alle Bausteine von Redux betrachtet und den Weg der Daten von der Action bis zum Selektor verfolgt. +Jeder Baustein hat klare Zuständigkeiten, und es gibt strikte Regeln dazu, welche Aufgaben in welchen Teil der Anwendung gehören. + +\footnote{Kommunikation mit der Außenwelt}Was unserer Anwendung allerdings bis hierhin noch fehlt, ist die Kommunikation mit der Außenwelt: +Wird die Action `loadBooks` ausgelöst, so soll die Buchliste per HTTP vom Server geladen werden. +Bei genauer Betrachtung fällt jedoch auf, dass Reducers und Selektoren nicht die richtigen Orte für diese Aufgabe sind: Beide sind durch Pure Functions definiert, die keine Seiteneffekte ausführen dürfen. + +Die grundsätzliche Aufgabe lässt sich wie folgt zusammenfassen: +Wir wollen auf die Action `loadBooks` reagieren und einen HTTP-Request auslösen, um die Buchliste vom Server zu laden. +Ist die HTTP-Antwort eingetroffen, so soll die Buchliste mit einer Action `loadBooksSuccess` in den Store gesendet werden. +Schlägt die HTTP-Kommunikation fehl, so soll die Action `loadBooksFailure` ausgelöst werden. + +Um diese Aufgabe anzugehen, bietet NgRx ein ausgereiftes Modell zur Ausführung von Seiteneffekten: +Mit `@ngrx/effects` können wir asynchrone Ereignisse und andere Seiteneffekte in der Anwendung koordinieren. +In einem Effect ist alles\ erlaubt: HTTP-Kommunikation, Logging, Authentifizierung, LocalStorage, Routing, Anzeige von Meldungen usw. +Effects sind kein Bestandteil der eigentlichen Redux-Architektur, sondern agieren außerhalb des Stores. + +%\vspace{3\baselineskip}\suppressindent +\footnote{Effect: reaktiver Datenstrom von Actions}Mit Effects kommmen wir in den Genuss des vollen Erlebnisses von reaktiver Programmierung. +Ein Effect ist ein reaktiver Datenstrom, der +\begin{itemize} + \item auf Actions und andere Ereignisse reagiert, + \item Seiteneffekte ausführen kann und + \item neue Actions generiert. +\end{itemize} + +Technisch ist ein Effect also immer ein `Observable`. +Alle so erzeugten Actions werden automatisch in den Store dispatcht – darum kümmert sich das Framework. +Effects werden in einer eigenen Klasse untergebracht, die wie ein Service aufgebaut ist: Sie trägt den Decorator `@Injectable()` und kann über ihren Konstruktor Abhängigkeiten anfordern. +Damit die Effects funktionieren, muss die Klasse allerdings explizit registriert werden. +Dazu muss jede Effects-Klasse in einem Modul mithilfe des `EffectsModule` eingebunden werden. +Für das `BooksModule` wurde dieser Schritt bereits bei der Einrichtung erledigt: +\begin{widefigtop} + {fig:redux:flowfull} + {Datenfluss in NgRx mit Effects} + \includegraphics[width=\totalwidth]{images/statemanagement/ngrx-flow-full} +\end{widefigtop} + +\vspace*{4\baselineskip} +\lstinputlisting[caption={Effect in der Anwendung registrieren \textup{(\file{books.module.ts})}}, label=lst:ngrx:booksmoduleeffects, language=js,breakatwhitespace=true]{\currentBm/src/app/books/books.module.ts.2.snippet} + +Die Klasse `BookEffects` befindet sich in der Datei \file{books/""store/""book""\mbox{.effects}.ts}. +Ein Effect wird als ein Property in dieser Klasse definiert. +Der Name des Propertys spielt keine Rolle, sollte aber passend zur Aufgabe benannt werden. +Das Ziel ist es, hier ein Observable zu entwickeln, das Actions ausgibt. +Jeder Effect wird mit der Funktion `create\-Effect()` gekapselt und wird so automatisch in den Lebenszyklus von NgRx integriert. +Ein erster Effect zum Laden von Daten wurde hier bereits automatisch generiert, weil wir beim Anlegen die Einstellung \mbox{`-{`-api}} verwendet haben. +Dieses Grundgerüst wollen wir vervollständigen, um die Buchliste per HTTP zu laden.{\tolerance1000\par} + +Zunächst benötigen wir eine Instanz des `BookStoreService`, der die HTTP-Kommunikation kapselt: + +\pagebreakbeforelisting +\lstinputlisting[caption={Service in den Effect injizieren \textup{(\file{book.effects.ts})}}, label=lst:ngrx:bookeffectsbss, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.ts.snippet} + +\footnote{Datenstrom von Actions}In die Klasse wurde bereits der Service `Actions` injiziert. +Damit erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. +Dieser Datenstrom ist meist die Grundlage für unsere Effects.\footnote*{Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: \shortlinkInline{link-schule-effects}.} + +Der Effect `loadBooks\$` soll auf die Action `loadBooks` reagieren, den passenden HTTP-Request auslösen und Actions zurück in den Store leiten (Success und Failure). +Wir zeigen zunächst den vollständigen Code und gehen die Implementierung anschließend Schritt für Schritt durch: + +\lstinputlisting[caption={Effect zum Laden der Buchliste \textup{(\file{book.effects.ts})}}, label=lst:ngrx:bookeffectsfull, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.ts} + +Der Datenfluss beginnt beim Strom aller Actions aus der gesamten Anwendung: `this.actions\$`. +\footnote{Actions filtern}Wir interessieren uns hier allerdings nur für Actions mit dem bestimmten Typ `loadBooks`. +Um den Datenstrom zu filtern, eignet sich der Operator `filter()`. +Das führt allerdings zu sehr technischem Code, erschwert die korrekte Typisierung und sagt wenig über die tatsächliche Semantik aus. +NgRx bringt deshalb einen eigenen Operator mit, mit dem wir den Datenstrom nach dem Action-Typ filtern können: `ofType()`. +Als Argument übergeben wir hier einen Action Creator. +Anschließend liegt ein Datenstrom vor, der nur \mbox{Actions} vom Typ `loadBooks` ausgibt. + +\footnote{Flattening-Operator auswählen}Für jede dieser Actions wollen wir nun einen HTTP-Request ausführen und die Ergebnisse verarbeiten. +Damit betreten wir erneut das Terrain der Higher-Order Observables. +Wir müssen uns sorgfältig für einen der vier Flattening-Operatoren entscheiden. +Unsere Wahl fällt hier auf `switchMap()`: Wird die Buchliste noch geladen und während\-dessen erneut angefragt, soll nur die zuletzt gesendete Anfrage be\-arbei\-tet werden. +Verwenden Sie bitte nicht immer `switchMap()` in Ihren Effects, sondern wägen Sie sorgfältig ab, welcher Flattening-Operator am besten zu Ihrem jeweiligen Problem passt. + +\footnote{HTTP-Request}Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookStoreService` mit der Methode `getAll()` einsetzen, die ein Observable zurückgibt. +Unser Effect ist nun ein Observable, das ein Array von Büchern liefert. +Damit die Buchliste im Store verarbeitet werden kann, müssen wir sie in eine Action verpacken. +Wir nutzen dazu den Operator `map()` und wandeln damit die Buchliste um in eine Action `loadBooksSuccess`, die die Liste enthält. + +\footnote{Fehlerbehandlung}Ähnlich gehen wir für den Fehlerfall vor: Hier nutzen wir den Operator `catchError()`, um einen Fehler in der Ausführung abzufangen. +Wir werfen den Fehler allerdings an der Stelle nicht weiter, sondern kapseln ihn in eine Action `loadBooksFailure`, die wir als reguläres Element des Datenstroms weitergeben. +Dabei verwenden wir das Property `message` auf dem Fehlerobjekt, um die Meldung auszulesen. + +Auf diese Weise erzeugen wir einen Datenstrom, der nacheinander verschiedene Actions ausgeben kann. +Die Funktion `createEffect()` sorgt dafür, dass dieser Datenstrom automatisch abonniert wird. +Die resultierenden Actions werden in den Store dispatcht und durchlaufen dort die Reducers. +In einem Effect sollten wir also niemals selbst die Methode `store.dispatch()` aufrufen. + +Bei der Fehlerbehandlung ist es wichtig, den Fehler so dicht wie möglich dort abzufangen, wo er entsteht. +Wir setzen `catchError()` deshalb nicht im Hauptdatenstrom ein, sondern hängen `map()` und `catch\-Error()` direkt an den Serviceaufruf an. +Das `switchMap()` verarbeitet also ein Observable, das in jedem Fall eine Action liefert – so wird der Hauptdatenstrom nicht durch Fehler gefährdet. +Wird ein Effect durch einen Fehler im Hauptdatenstrom gestört, so wird das zugrunde liegende Observable beendet, und die Anwendung kann nicht mehr auf eine weitere Nachricht durch den beendeten Effect reagieren. + +Probieren Sie die Anwendung nun aus und beobachten Sie auch die Actions in den Redux DevTools! +Nachdem die Buchliste geladen wurde, wird `loadBooksSuccess` dispatcht, und der Reducer fügt die Buchliste in den State ein. +Die Selektoren in der Komponente reagieren darauf, und die Bücher werden dargestellt. + +\footnote{Loop of Death}Bitte beachten Sie, dass Sie mit einem ungünstig programmierten Effect schnell eine Endlosschleife erzeugen können, die den Browser zum Glühen\ bringt, wenn parallel `ng serve` läuft und die Anwendung sofort aktualisiert. +Der folgende Effect empfängt alle Actions und leitet sie direkt in den Store zurück – es entsteht eine Endlosschleife: + +\lstinputlisting[caption={\mbox{Effect mit Endlosschleife}}, label=lst:ngrx:effectloopofdeath, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/effect-loopofdeath.ts} + +Wir können übrigens einen Effect so konfigurieren, dass er die ausgegebenen Elemente des Datenstroms nicht als Actions dispatcht. +Das ist immer dann nützlich, wenn ein Effect lediglich Seiten\-effekte ausführt, aber keine weiteren Aktionen zur Folge hat. +Denken Sie z.\,B. daran, dass wir nach dem erfolgreichen Anlegen eines Buchs zur Detailseite navigieren möchten – nach diesem Schritt sollen keine weiteren Actions getriggert werden. +Dazu können wir im zweiten Argument von `create\-Effect()` die Einstellung `\{ dispatch: false \`} setzen. + +\lstinputlisting[caption={Dispatchen von Actions deaktivieren}, label=lst:ngrx:effectdispatchfalse, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/effect-dispatchfalse.ts} + +\noindent +Ein Tipp aus der Praxis: Nutzen Sie diese Einstellung während der Entwicklung, um einen *Loop of Death* zu verhindern. +Sie können einen komplexen Effect dann in Ruhe debuggen, ohne dass die Actions in den Store geleitet werden. +Erst wenn die Implementierung fertig ist, aktivieren Sie den Effect, indem Sie das `\{ dispatch: false \`} wieder entfernen. + +\subsubsection*{Achtung: Proprietäres Verhalten von TypeScript} + +Bei allen gezeigten Effects haben wir die Syntax mit dem alten proprietären Verhalten von TypeScript (`useDefineForClassFields: false`) verwendet. +Zum Zeitpunkt der Fertigstellung dieses Buchs wurde diese Schreibweise in der offiziellen Dokumentation, in der Demo-Anwen\-dung und in den Schematics von NgRx empfohlen. +Wir haben uns daher dazu entschlossen, möglichst wenig von den offiziellen Quellen abzuweichen, auch wenn es womöglich bei geänderten Standardeinstellungen zu Fehlern kommen kann. +In \autoref{sub:ww:useDefineForClassFields} ab \autopageref{sub:ww:useDefineForClassFields} haben wir beschrieben, unter welchen Umständen dieser Fehler entsteht. + +Sollten die Beispiele in Zukunft nicht mehr lauffähig sein, so können Sie den Code leicht umstellen. +Das folgende Listing zeigt eine Variante, bei der wir die Klasse `Actions` und den `BookStoreService` mithilfe der Funktion `inject()` anfordern. + +\lstinputlisting[caption={Effect zum Laden der Buchliste mit `inject()` \textup{(\file{book.effects.ts})}}, label=lst:ngrx:bookeffectsfull-inject, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.inject.snippet} + +### Geschafft! + +Sobald der Effect aktiv ist, wird unsere Anwendung die Bücher über HTTP laden und die Liste mittels `loadBooksSuccess` an den Reducer übergeben. +Dieser wird einen neuen Zustand mit den geladenen Büchern berechnen. +Abschließend wird mithilfe eines Selektors die Oberfläche aktualisiert, und die neuen Bücher werden angezeigt. + +Und damit haben Sie die Rundreise durch alle Bausteine von Redux und NgRx geschafft. +Mithilfe eines zentralen Stores haben wir die Zustände der Anwendung zentralisiert und so die Buchliste vom Server abgerufen. +Die Komponenten beinhalten mit dieser Architektur nur noch wenig Logik – alles Nötige passiert im Store. + +Vielleicht haben Sie nun das Gefühl, dass wir für eine kleine Aufgabe unnötig viel Code erzeugt haben. +Betrachtet man das vorliegende Beispiel isoliert, so ist diese Empfindung richtig: Für kleine Anwendungen ist der Einsatz von Redux und NgRx nicht zwingend sinnvoll. +Steigt allerdings die Komplexität des Projekts, so kann ein zentrales State Management mit klaren Regeln und einer durchdachten Architektur eine sinnvolle Investition sein. +Prüfen Sie also für ein Projekt sorgfältig, ob sich der Einsatz von Redux wirklich lohnt. + + +## Debugging mit den Redux DevTools + + + +Wir möchten Ihnen zum Abschluss des Praxisteils ein wichtiges Hilfsmittel zeigen, das beim Debugging von Anwendungen mit NgRx hilfreich ist: die *Redux DevTools*. + +Ein besonderer Vorteil der Redux-Architektur ist das sogenannte *Time Travel Debugging*. +Da stets durch eintreffende Actions ein neuer State im Store erzeugt wird, ist es möglich, die Historie der Ereignisse und Zustände nachzuvollziehen. +Die Redux DevTools unterstützen uns dabei mit einer grafischen Oberfläche, über die wir alle Actions und Zustandsänderungen kontrollieren und debuggen können. + +### Die DevTools installieren +Die Redux DevTools sind eine Erweiterung für Google Chrome. +\footnote{Erweiterung für Google~Chrome}Um sie zu verwenden, besuchen wir den Webstore und installieren die Extension im Browser.\shortlinkFootnote{link-redux-devtools} +\begin{widefig} + {fig:dev-tools:redux} + {Redux DevTools installieren} + % {Die Redux DevTools installieren} + % {10pt} + \includegraphics[width=\totalwidth]{images/statemanagement/webstore-devtools.png} +\end{widefig} + +### Die DevTools in der Anwendung registrieren + +Um Informationen aus unserem Store mit dem Entwicklungswerkzeug zu teilen, müssen wir in der Anwendung die passende Schnittstelle schaffen. +Dazu bringt NgRx das Modul `@ngrx/store-devtools` mit, das diese Schnittstelle zur Kommunikation implementiert. + +Der einfachste Weg zur Einrichtung ist es, das Modul mithilfe von `ng~add` in das Projekt einzufügen. +Als wir im Praxisteil auf \autopageref{lst:ngrx:storeeinrichten} die Pakete für NgRx installiert haben, haben wir diesen Schritt bereits erledigt. + +\begin{clilisting}[caption={NgRx DevTools installieren}] +ng add @ngrx/store-devtools +\end{clilisting} + +Damit werden die benötigten Abhängigkeiten installiert, und das zugehörige `StoreDevtoolsModule` wird in das `AppModule` der Anwendung eingebunden. + +### Die DevTools nutzen + +Die *Redux DevTools* integrieren sich in die Chrome DevTools, und wir finden nach der Installation hier einen neuen Reiter *Redux*. + +\begin{widefig} + {fig:dev-tools:redux:actions} + {Actions in den DevTools anzeigen} + \includegraphics[width=\totalwidth]{images/statemanagement/developer-tool-action.png} +\end{widefig} + +\footnote{Historie aller Actions}In der \autoref{fig:dev-tools:redux:actions} sind fünf Actions in der Historie auf der linken Seite zu sehen. +Die ersten drei Einträge mit dem Präfix `@ngrx` stammen vom System und werden automatisch bei der Initialisierung dispatcht. +Danach folgen zwei Aktionen, die wir selbst in der Anwendung ausgelöst haben. +Anhand dieser Liste kann genau nachvollzogen werden, was bisher geschehen ist: +Es wurde die Aufforderung zum Abrufen der Buchliste in den Store geschickt, und die Liste kam schließlich vom Server zurück. + +\suppressindent +Wählen wir eine Action aus, werden auf der rechten Seite die Änderungen zum vorhergehenden Zustand visualisiert. +Nachdem die Buchliste geladen wurde, wurde das `loading`-Flag von `true` auf `false` geändert und die Buchliste wurde in das Property `books` eingefügt. +Mit den Reitern *Action* und *State* können wir das Action-Objekt und den State betrachten, wie er nach der Verarbeitung dieser Action aussah. + +\footnote{Time Travel Debugging}Mit der Historie der Actions können wir außerdem das Time \mbox{Travel} Debugging durchführen. +Dazu besitzt jeder Eintrag in der Liste zwei Buttons: *Jump* und *Skip*. +Damit können wir einzelne Actions überspringen oder den Zustand zu einem bestimmten Zeitpunkt rekonstruieren. +Die Anwendung reagiert live auf die Zustandsänderungen, sodass wir das Ergebnis direkt im Browser betrachten können. +Am unteren Rand befindet sich außerdem ein Slider, mit dem wir schrittweise durch die Zustände der Anwendung navigieren können. + +In \autoref{fig:dev-tools:skip-acktion} wurde die Action `[Book] Load Books Success` mit der Schaltfläche *Skip* übersprungen. +Nun ist links in der Anwendung der Ladeindikator zu sehen. +\begin{widefig} + {fig:dev-tools:skip-acktion} + {Time Travel Debugging} + \includegraphics[width=\totalwidth]{images/statemanagement/developer-tool-skip-action.png} +\end{widefig} + +\suppressindent +\footnote{States exportieren}Weitere interessante Features verstecken sich hinter den Buttons am unteren Rand: +Damit können wir unter anderem alle Actions und Zustände als JSON-Datei exportieren und später wieder importieren. +Ein solcher Export kann z.\,B. an ein anderes Teammitglied übermittelt werden, das damit den gleichen Anwendungsstatus erzeugen kann wie wir. + +\vspace{\baselineskip}\noindent +Mit diesem Debugging-Werkzeug haben wir also an einem Ort einen zentralen Überblick über alle Ereignisse der Anwendung. +Wir können den Zustand des Systems überwachen und so auch Fehler bei der Entwicklung einfacher aufspüren. + + +## Redux und NgRx: Wie geht's weiter? + +Wir haben in diesem Kapitel die Grundlagen der Redux-Architektur und des Frameworks NgRx kennengelernt. +Tatsächlich geht die Reise aber noch viel weiter! +Wir möchten Ihnen in diesem Abschnitt einen Ausblick geben, welche Tools und Konzepte Ihnen dabei noch begegnen werden. + + +### Actions gruppieren mit `createActionGroup()` + + +Wir haben die Funktion `createAction()` kennengelernt, um einzelne Actions zu erstellen. +Zum Laden der Buchliste haben wir auf diese Weise drei einzelne Actions definiert, siehe \autoref{lst:ngrx:bookactions} auf \autopageref{lst:ngrx:bookactions}. +Dafür war viel repetitiver Code notwendig: Wir mussten den Variablennamen definieren, das Präfix `[Book]` setzen und den Type der Action als String angeben. + +Um die Erzeugung der Actions zu vereinfachen, wurde mit NgRx~13.2 die Funktion `createActionGroup()` eingeführt. +Damit können wir eine Gruppe von Actions mit einem gemeinsamen Präfix anlegen. +Die drei Actions aus dem vorherigen Beispiel lassen sich also wie folgt definieren:{\tolerance2000\par} + +\lstinputlisting[caption={Actions als Gruppe definieren \textup{(\file{book.actions.ts})}}, label=lst:ngrx:bookactionsgroup, language=js,breakatwhitespace=true,belowskip=0pt]{\snippets/reduxngrx/ngrx/action-group.ts} + +Die Besonderheit an diesem Ansatz ist, dass wir die Variablennamen nicht mehr von Hand notieren müssen. +Wir können die Actions als z.\,B. `BookActions.loadBooks()` verwenden – der Name des Propertys wird automatisch aus dem Action Type ermittelt. +Auf diese Weise sparen wir uns viel Tipparbeit. + +Bitte beachten Sie: Um die Actions zu verwenden, müssen wir nun immer das gesamte Objekt `BookActions` importieren, nicht die einzelnen Actions. +In den Komponenten, Reducers und Effects sind also auch Anpassungen nötig, um diesen Ansatz zu nutzen. + + +### Routing + + + + +Der Anwendungszustand setzt sich nicht nur aus geladenen Daten und Einstellungen zusammen – die aktuell geladene Route und ihre Parameter gehören ebenfalls in den State. +Beachtet man dies nicht, so sieht man gegebenenfalls zum richtigen State die falsche Komponente auf der Oberfläche. +Deshalb sollten Informationen zum Routing auch im NgRx-Store abgelegt werden. +Den passenden Adapter zwischen Router und Store erhalten wir mit dem Modul `@ngrx/router-store`: +\begin{clilisting}[caption={Router-Store installieren}] +ng add @ngrx/router-store +\end{clilisting} + +Das Modul verfügt über eigene Actions und Reducers und kommuniziert direkt mit dem Router. +Dadurch werden alle Aktivitäten des Routers mit Actions abgebildet. +Es werden unter anderem die folgenden Actions ausgelöst: + +\begin{itemize} + \item `routerRequestAction` + \item `routerNavigationAction` + \item `routerNavigatedAction` +\end{itemize} + +Die passenden Reducers fügen anschließend die Routeninformationen in den State ein, sodass wir z.\,B. die Routenparameter in den Komponenten, Reducers und Effects auslesen können. + +### Entity Management + + + +Verwalten wir Entitäten in unserem State (z.\,B. Bücher), so müssen wir in den Reducers für jede Entität wiederkehrende Routinen implementieren: +Wir müssen eine Liste in den State schreiben und die Liste pflegen, indem wir Objekte einfügen, aktualisieren oder löschen. +Bei der Suche nach einem bestimmten Objekt müssen wir stets durch die Liste laufen und dabei z.\,B. `Array.find()` verwenden. + +Diese wiederkehrenden Aufgaben sind für fast alle Entitäten notwendig und verleiten zu redundantem Code. +Um diese Zugriffe auf den State zu vereinfachen, existiert das Paket `@ngrx/entity`: +\begin{clilisting}[caption={Entity-Verwaltung installieren}] +ng add @ngrx/entity +\end{clilisting} + +Anstatt die Entitäten selbst im State zu deklarieren, können wir die vorgegebene Struktur des Entity-Adapters nutzen. +Jedes State-Interface, in dem Entitäten verwaltet werden, muss dazu das Interface `Entity\-State` erweitern: + +\lstinputlisting[caption={`EntityState` implementieren}, label=lst:ngrx:entitystate, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/state.ts} + +Der `EntityState` gibt eine Datenstruktur vor, in der die Entitäten in einem Objekt abgelegt werden; als Schlüssel werden die IDs der Entitäten verwendet. +Ein Objekt vereinfacht die Suche nach einer Entität mit einer bestimmten~ID. +Die geordnete Reihenfolge einer Liste geht allerdings bei einem Objekt verloren. +Deshalb werden zusätzlich die~IDs in einem Array im State abgelegt, um daraus die Reihenfolge jederzeit ablesen zu können. + +Durch den `EntityState` wird die folgende Struktur vorgegeben. +Der Typ `Dictionary` stammt von NgRx und beschreibt das Objekt, in dem die Entitäten gespeichert sind. + +\lstinputlisting[caption={Struktur des `EntityState`}, label=lst:ngrx:entitystatestructure, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/state-structure.ts} + +Damit wir die Daten in dieser Struktur nicht eigenhändig pflegen müssen, stellt das Modul einen Adapter zur Verfügung, den wir zusätzlich initialisieren müssen. +\footnote{Primärschlüssel festlegen}Der Adapter nutzt automatisch das Property~`id` als Primärschlüssel der Entität. +Ist die ID allerdings in einem anderen Property enthalten, z.\,B. bei einem Buch die ISBN, so müssen wir eine Funktion `selectId` angeben, die die richtige ID auswählt. + +\lstinputlisting[caption={`EntityAdapter` erzeugen}, label=lst:ngrx:entitycreateadapter, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/create-adapter.ts} + +Mithilfe des Adapters können wir zunächst den initialen Zustand erzeugen. +Beinhaltet unser State noch weitere Eigenschaften, so geben wir als Argument ein Objekt an: + +\lstinputlisting[caption={Initialzustand erzeugen}, label=lst:ngrx:entitygetinitialstate, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/get-initialstate.ts} + +Die Mächtigkeit von `@ngrx/entity` entfaltet sich schließlich, wenn es um die Implementierung der Reducers geht. +Dazu stellt der Adapter eine Reihe von Methoden zur Verfügung, mit denen wir die Kollektion von Entitäten im State verwalten können: +\begin{flushleft} + \tabcolsep4pt + \vskip-4mm + \begin{longtable}{@{}lp{89mm}@{}} + `addOne` & eine Entität hinzufügen \\ + `addMany` & mehrere Entitäten hinzufügen \\ + `setAll` & Kollektion vollständig ersetzen \\ + `setOne` & eine Entität hinzufügen oder ersetzen \\ + `setMany` & mehrere Entitäten hinzufügen oder ersetzen \\ + `removeOne` & eine Entität aus der Kollektion entfernen \\ + `removeMany` & mehrere Entitäten aus der Kollektion entfernen\newline (nach ID oder Prädikatsfunktion) \\ + `removeAll` & Kollektion leeren \\ + `updateOne` & eine Entität aktualisieren \\ + `updateMany` & mehrere Entitäten aktualisieren \\ + `upsertOne` & eine Entität hinzufügen oder aktualisieren\newline (mit partiellen Änderungen) \\ + `upsertMany` & mehrere Entitäten hinzufügen oder aktualisieren \\ + `map` & Kollektion aktualisieren mithilfe einer Projektionsfunktion + \end{longtable} +\vskip-9mm +\end{flushleft} + +Alle Funktionen erhalten den aktuellen State und die zu verarbeitenden Werte als Argument und erzeugen einen neuen State. +Wir können den so erzeugten State also direkt aus dem Reducer zurückgeben oder verwenden, um einen komplexeren State mit weiteren Änderungen zu erzeugen: + +\lstinputlisting[caption={Reducer mit Adapter-Funktionen}, label=lst:ngrx:entityreducer, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/reducer.ts} + +Der EntityAdapter stellt außerdem eine Reihe von Selektoren zur Verfügung, die wir nutzen können, um die Kollektion auszulesen. +Die Selektoren werden mithilfe von `adapter.getSelectors()` erzeugt, müssen dann allerdings noch in einzelne Selektoren verpackt werden, die den jeweiligen Feature-State als Grundlage nutzen: + +\lstinputlisting[caption={Selektoren mit `getSelectors()`}, label=lst:ngrx:entityselectors, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/selectors.ts} + +Auf diese Weise können wir die Verwaltung von Entitäten im State sehr effizient durchführen. + +### Testing + + +Alle auf Grundlage von NgRx entwickelten Bausteine sollten auch getestet werden. +Dafür möchten wir Ihnen in diesem Abschnitt einige Hinweise geben. +Grundsätzlich werden bei der Initialisierung mit den Schematics von NgRx bereits Grundgerüste für die Unit-Tests angelegt – Sie können also direkt loslegen. + +\subsubsection*{Actions} +Die Action Creators beinhalten keine spezifische Logik, die getestet werden muss. +In manchen Blogartikeln wird vorgeschlagen, die korrekte Zusammensetzung der Objekte zu prüfen, damit eine Action stets den richtigen `type` besitzt. +Wir sind der Meinung, dass solche trivialen Tests keinen Mehrwert liefern und deshalb nicht notwendig sind. + +\subsubsection*{Reducers} + + +Ein Reducer ist eine Pure Function, liefert also für die gleiche Eingabe immer die gleiche klar vorhersehbare Ausgabe. +Diese Eigenschaft kommt uns beim Testing zugute, denn wir können Reducers isoliert testen, ohne dass Angular dafür benötigt wird. +Dazu rufen wir die Reducer-Funktion mit einem Startzustand und einer Action auf und prüfen den erzeugten neuen Zustand. + +\lstinputlisting[caption={Reducer testen \textup{(\file{book.reducer.spec.ts})}}, label=lst:ngrx:testingreducer, language=js,breakatwhitespace=true,belowskip=0pt]{\currentBm/src/app/books/store/book.reducer.spec.ts} + +\subsubsection*{Selektoren} +Zum Testen von Selektoren gibt es verschiedene Herangehensweisen. +Auch wenn ein Selektor mithilfe von `createSelector()` erstellt wurde, so ist er weiterhin nur eine Funktion, die den State als Argument erhält und Berechnungen über die Daten ausführt. +Wir können also auch Selektoren isoliert testen, ohne dass wir einen Store oder das Angular-Framework benötigen. +Dazu erstellen wir im Test ein State-Objekt, das alle benötigten Daten enthält. +Wir wenden den Selektor darauf an und prüfen das Ergebnis. +Wichtig ist, dass wir in diesem Stub stets die Struktur des Root-States abbilden, denn der ist ja auch der Ausgangspunkt eines jeden Selektors. + +Die Hilfsfunktion `book()` haben wir selbst definiert. Sie generiert einfache Buch-Objekte mit Beispieldaten, sodass wir schnell Testdaten erzeugen können. + +\pagebreakbeforelisting +\lstinputlisting[caption={Einfache Selektoren testen \textup{(\file{book.selectors.spec.ts})}}, label=lst:ngrx:testingselector, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.selectors.spec.ts} + +Bei komplexeren Selektoren, die mehrere andere Selektoren als Grundlage verwenden, liegt die kritische Logik in der Projektionsfunktion, die im letzten Argument von `createSelector()` angegeben wird. +Es ist deshalb in den meisten Fällen ausreichend, nur diese Projektion zu testen. +Zugriff auf die Funktion erhalten wir mit `selector.projector`: + +\lstinputlisting[caption={Projektionsfunktion testen \textup{(\file{book.selectors.spec.ts})}}, label=lst:ngrx:testingselectorprojector, language=js,breakatwhitespace=true,firstline=4,lastline=12,belowskip=0pt]{\currentBm/src/app/books/store/book.selectors.1.spec.ts} + +\subsubsection*{Effects} + +Effects sind nur schwierig isoliert zu testen, denn sie greifen auf verschiedene Abhängigkeiten aus der Anwendung zu. +Dazu ist nicht nur das Modul `@ngrx/effects` nötig, sondern auch alle verwendeten HTTP-Services. +Außerdem muss das Observable `actions\$: Actions` mit einem Strom von Actions versorgt werden, und wir wollen keinen vollständigen Store aufsetzen. + +\footnote{`provideMockActions()`}NgRx bietet dafür die Funktion `provideMockActions()`, die einen gemockten Strom von Actions bereitstellt. +Tests für Effects müssen mithilfe von `TestBed` definiert werden, sodass wir die Dependency Injection von Angular nutzen können. +Alle verwendeten Services müssen selbstverständlich auch durch Mocks oder Stubs ersetzt werden, damit keine echten HTTP-Requests ausgeführt werden. + +\pagebreakbeforelisting +\lstinputlisting[caption={Grundgerüst zum Testen von Effects \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingeffect, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.spec.ts.1.snippet} + +Für die konkreten Tests müssen wir ein Observable von Actions bereitstellen und das ausgegebene Observable sowie ggf. die ausgeführten Seiteneffekte prüfen. +Das \autoref{lst:ngrx:testingeffectFull} zeigt einen vollständigen Testaufbau. +Hier stellen wir sicher, dass der Effect die gewünschte Action ausgibt und die Daten wie gewünscht über den `BookStoreService` bezieht.%blank line down + +\lstinputlisting[caption={Vollständiger Test für einen Effect \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingeffectFull, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.spec.ts.2.snippet} + +\footnote{Marble Testing}Der Aufbau lässt sich vereinfachen, wenn wir nur die Datenströme miteinander vergleichen. +Dazu eignet sich das Konzept des *Marble Testing*: +Anstatt ein Observable wie üblich zu erzeugen und dann darauf zu subscriben, notieren wir den geplanten Datenstrom als Marble-Diagramm direkt im Test und definieren damit die Eingabe und die erwartete Ausgabe. +Die technische Grundlage dafür bietet das Paket \mbox{`jasmine-marbles`}.\shortlinkFootnote{link-jasmine-marbles} +Das Projekt stellt auch den Matcher `toBeObserv\-able()` zur Verfügung, mit dem wir in der Expectation direkt gegen das erzeugte Observable prüfen können: + +\lstinputlisting[caption={Effects testen mit Marbles \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingmarble, language=js,breakatwhitespace=true,belowskip=0pt]{\currentBm/src/app/books/store/book.effects.marble.spec.ts.snippet} + +\subsubsection*{Store} + +Komponenten und Effects greifen häufig auf den Store zu. +Unter anderem werden Actions in den Store gesendet (dispatcht), und der aktuelle Zustand wird mithilfe von Selektoren ermittelt. +Wenn man für solche Komponenten oder Effects einen Unit-Test definieren will, benötigt man einen Ersatz für den echten Store. +\footnote{`provideMockStore()`}NgRx bietet uns hierfür die Funktion `provideMockStore()` an: +Wir können damit einen gemockten Store registrieren, der einen von uns definierten Zustand besitzt. +Dieser Zustand bleibt so lange unverändert, bis wir mittels `store.setState()` einen anderen Zustand setzen. +Als Beispiel wollen wir den bekannten Effect zum Laden der Bücher so erweitern, dass er nur dann Bücher lädt, wenn die Buchliste im State noch leer ist. +Hat das Array bereits Einträge, soll nichts passieren. + +Es ist für dieses Szenario notwendig, dass wir den bestehenden State im Effect berücksichtigen. +Dies können wir mit dem Operator `concat\-Latest\-From()` aus dem Paket `@ngrx/effects` realisieren: +Wir übergeben ein anderes Observable als Argument, und der Operator reichert den Hauptdatenstrom mit dem jeweils letzten Element aus diesem Observ\-able an. +Das bedeutet also, dass uns in den Effects neben dem Payload aus den Actions auch zusätzlich Daten aus dem Store zur Verfügung stehen. +Wir verwenden hier direkt unseren Selektor `selectAllBooks`, um die aktuelle Buchliste aus dem Store zu erhalten. +Anhand der Buchliste können wir dann mit dem Operator `filter()` entscheiden, ob die Bücher neu heruntergeladen werden sollen oder nicht. + +\pagebreakbeforelisting +\lstinputlisting[caption={Effect zum Laden der Buchliste mit Filter \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingeffectwithFilter, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.alternative.ts.snippet} + +Für diesen Effect sollten wir sicherstellen, dass bei einem leeren Array auch tatsächlich die Action `loadBooksSuccess` gefeuert wird – und dass dies bei einem gefüllten Array eben gerade nicht passiert. +Mittels `provide\-MockStore()` stellen wir vor jeder Spezifikation zunächst den vorgetäuschten\ Store bereit. +Für die zweite Spezifikation ändern wir den State anschließend noch einmal mittels `store.setState()` ab:%blank line down + +\lstinputlisting[caption={Effects testen mit dem `MockStore` \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingMockStore1, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.alternative.spec.ts.snippet} + +Ebenso können wir mit dem `MockStore` die Rückgabewerte von Selektoren überschreiben. +Unser letztes Beispiel zum Thema zeigt, wie wir sicherstellen können, dass die `BookListComponent` tatsächlich den Text * Loading~...* anzeigt, wenn im State das Loading-Flag entsprechend gesetzt ist (siehe \autoref{lst:ngrx:useselectorsblc} und \autoref{lst:ngrx:blchtml} ab \autopageref{lst:ngrx:useselectorsblc}). +Es ist dabei nicht notwendig, den State im `MockStore` zu setzen, da wir auf diesen nicht direkt zugreifen. +Die Komponente bezieht ihre Daten über die beiden Selektoren `select\-All\-Books` und `select\-Books\-Loading`, die wir entsprechend "ausmocken": + +\lstinputlisting[caption={Selektoren mocken mit dem `MockStore` \textup{(\file{\mbox{book-list}"".component.spec.ts})}}, label=lst:ngrx:ngrx:testingMockStore2, language=js,breakatwhitespace=true]{\currentBm/src/app/books/book-list/book-list.component.spec.ts.snippet} + +Wie Sie sehen, stellt NgRx einige Hilfen und Konzepte bereit, um die einzelnen Bausteine effektiv zu testen. +Vor allem die Funktionen `provide\-Mock\-Actions()` und `provide\-Mock\-Store()` erleichtern uns das Testing für komplexe Szenarien in NgRx. + +### Hilfsmittel für Komponenten: `@ngrx/component` + + + +Das Paket `@ngrx/component` stellt eine Sammlung von nützlichen Helfern zur Verfügung, um mit reaktiven Datenströmen in Komponenten zu arbeiten. +Die Werkzeuge sind dabei nicht auf den Einsatz mit NgRx beschränkt, sondern können auch unabhängig davon eingesetzt werden. +Das Paket beinhaltet zwei elementare Bausteine: die Let-Direktive und die PushPipe. + +\footnote{AsyncPipe}Üblicherweise verwenden wir die AsyncPipe von Angular, um Observables direkt im Template einer Komponente aufzulösen. +Das ist der Weg, den wir vor allem beim Einsatz von NgRx wählen, denn die Daten kommen bereits vollständig aufbereitet durch Observables in die Komponente. +Die AsyncPipe kann in jedem Template-Ausdruck eingesetzt werden, also in der Interpolation, in Property Bindings und in Direktiven. +Gemeinsam mit `ngIf` und der `as`-Syntax können wir das Ergebnis eines Observables so in einem DOM-Container verfügbar machen. + +\lstinputlisting[caption={AsyncPipe verwenden}, label=lst:ngrx:compasncpipe, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrxcomponent/asyncpipe.html} + +So praktisch die AsyncPipe allerdings auch ist – sie birgt einige praktische Probleme. + +\subsubsection*{ngrxLet: Observables auflösen mit falsy Werten} + + +Die oben beschriebene Kombination von AsyncPipe und `ngIf` ist hilfreich, um die Daten aus dem Observable `numbers\$` im Template verfügbar zu machen. +Liefert das Observable allerdings einen *falsy* Wert – also `false`, `0`, `null`, `undefined`, `NaN` oder einen leeren String – , so wird der jeweilige Container durch `ngIf` gar nicht angezeigt. + +Um dieses Problem zu umgehen, kann die Direktive `ngrxLet` eingesetzt werden. +Sie löst ein Observable im Template auf und stellt die empfangenen Daten in einer lokalen Variable zur Verfügung. +Im Gegensatz zu `ngIf` bleibt das DOM-Element dabei stets sichtbar und wird nicht ausgeblendet. +Liefert `numbers\$` also eine `0`, so wird der Container im folgenden Beispiel trotzdem angezeigt. +Darüber hinaus erhält man mit `ngrxLet` bei Bedarf auch Zugriff auf mögliche Error- und Complete-Ereignisse des Observables. + +\lstinputlisting[caption={Direktive `ngrxLet` verwenden}, label=lst:ngrx:completdirective, language=js,breakatwhitespace=true,belowskip=0pt]{\snippets/reduxngrx/ngrxcomponent/letdirective.html} + +\subsubsection*{PushPipe: Observables auflösen ohne Zone.js} + + +Die AsyncPipe von Angular hat den Vorteil, dass sie automatisch die betroffene Komponente als *dirty* markiert, sobald ein neuer Wert im Observable ausgegeben wird. +Daraufhin wird diese Komponente beim nächsten Durchlauf der Change Detection geprüft, sodass die View aktualisiert wird. +Durch diesen Mechanismus ist es möglich, in allen Komponenten die Strategie `OnPush` für die Change Detection zu aktivieren. +Die AsyncPipe ist allerdings stets abhängig von der Bibliothek Zone.js, die für bestimmte Ereignisse in der Anwendung automatisch die Change Detection triggert. + +Im Abschnitt zur Change Detection ab \autopageref{sub:cd:zoneless} beschreiben wir einen Weg, um Angular ohne Zone.js zu verwenden. +Entwickelt man die Anwendung durchweg mit den Prinzipien der Reaktiven Programmierung, so kann es sinnvoll sein, auf die automatische Change Detection zu verzichten. +Hier setzt die Pipe `ngrxPush` von NgRx an, die als Alternative zur AsyncPipe genutzt werden kann. +\footnote{Zoneless Angular}Der elementare Unterschied: Die PushPipe triggert die Change Detection direkt, anstatt die Komponente nur für den nächsten Durchlauf als *dirty* zu markieren. +Die PushPipe ist damit unabhängig von Zone.js und kann in einer *zoneless* Umgebung eingesetzt werden. +Das kann sich positiv auf die Performance der Anwendung auswirken. + +Übrigens ermittelt die PushPipe automatisch, ob Zone.js in der Anwendung aktiviert ist, und kann dadurch auch das Verhalten der Async\-Pipe annehmen. +Auch die zuvor beschriebene Direktive `ngrxLet` baut auf derselben Grundlage auf und kann ohne Zone.js eingesetzt werden. + +\pagebreakbeforelisting +\lstinputlisting[caption={Pipe `ngrxPush` verwenden}, label=lst:ngrx:comppushpipe, language=js,breakatwhitespace=true,belowskip=0pt]{\snippets/reduxngrx/ngrxcomponent/pushpipe.html} + +### Facades: Zustandsverwaltung abstrahieren + +Um den Anwendungszustand mit NgRx zu verwalten, kommunizieren die Komponenten mit dem zentralen Store: Sie senden Actions in das System und lesen Daten mithilfe von Observables aus. +Dafür müssen wir die Klasse `Store` als direkte Abhängigkeit in den Komponenten verwenden. +Außerdem müssen wir die Actions und Selektoren importieren. + +Damit sind die Komponenten stets abhängig von den Bausteinen des Frameworks NgRx. +Das erschwert insbesondere das Testing und die Wiederverwendbarkeit. +Außerdem müssen wir uns für das State Management auf eine spezifische Lösung festlegen. +Jeglicher State wird global im Store abgelegt, und ein Mischbetrieb mit anderen Ansätzen führt potenziell zu unsauberem Code in der Komponente. + +\footnote{Facade: Schnittstelle für die Komponenten}Um diese Unschönheiten zu lösen, können wir eine *Facade* verwenden: +Dafür erstellen wir einen Service, der eine Schnittstelle zwischen der Komponente und dem Store bildet. +Die Komponente kommuniziert über Methoden und Propertys mit der Facade. +Das Framework NgRx bleibt dabei verborgen, und wir halten die Komponente frei von frameworkspezifischem Code. + +\begin{fig} + {fig:redux-facade} + {NgRx Flow mit Facade} + {NgRx Flow mit Facade} + {10pt} + \includegraphics[width=\textwidth]{images/statemanagement/facade} +\end{fig} + +\suppressindent +Zur Implementierung nutzen wir einen einfachen Service, der mit dem Store kommuniziert. +Als öffentliche Schnittstelle werden gut lesbare Methoden und Propertys angeboten. + +\pagebreakbeforelisting +\lstinputlisting[caption={Beispiel für eine Facade}, label=lst:ngrx:facade, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/books.facade.ts} + +In der Komponente nutzen wir die Facade, um die Daten aus den Propertys zu lesen und Aktionen mithilfe der angebotenen Methoden auszulösen. + +\lstinputlisting[caption={Facade in der Komponente verwenden}, label=lst:ngrx:facadeusage, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/facade-usage.ts} + +Im Unit-Test können wir einen solchen Service leicht durch einen Stub ersetzen. +Die Propertys und Methoden können dabei in einem einfachen Objekt nachgebildet werden, ohne die Schnittstelle des Stores ausmocken zu müssen. +Im Kapitel zum Testing haben wir ab \autopageref{sec:testing:replacestub} auf diese Weise einen Stub für den `BookStoreService` bereitgestellt. + +\lstinputlisting[caption={Unit-Testing: Stub für die Facade}, label=lst:ngrx:facadestub, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/facade-stub.ts} + +Ein weiterer großer Vorteil dieser Idee liegt darin, dass wir die Implementierung für das State Management jederzeit austauschen oder variieren können. +So ist es zum Beispiel möglich, bestimmte Teile des States rein lokal in der Facade zu verarbeiten und gar nicht an den zentralen NgRx Store zu übermitteln. +Einen Ansatz für die Verwaltung von lokalem State bietet das Projekt `@ngrx/component-store`, das wir am Ende dieses Kapitels kurz vorstellen. + + +\footnote{Manueller Provider}Für eine Facade empfehlen wir übrigens, keinen Tree-Shakable Provider zu verwenden. +Stattdessen tragen wir die Facade unter `providers` in dem Modul ein, das auch den Feature-State bereitstellt. +Auf diese Weise ist garantiert, dass der Feature-State korrekt registriert ist, wenn wir die Facade verwenden. + + +\section*{Fazit} + +Mithilfe von Redux und NgRx können wir die Zustandsverwaltung in der Anwendung zentralisieren. +Redux setzt auf ein unveränderliches Zustandsobjekt, das mithilfe von Pure Functions im Store verwaltet wird. +Alle Ereignisse der Anwendung werden durch Actions signalisiert, die Änderungen am Zustand auslösen können. +Dabei erzeugen die Reducers einen neuen Zustand, der über ein Observable an alle Interessierten ausgegeben wird. +Jeder Baustein von NgRx hat eine klar definierte Aufgabe, was eine konsistente Struktur in das Projekt bringt. +Die Teile der Architektur sind stark entkoppelt, sodass sie unabhängig voneinander entwickelt und gewartet werden können. + +Mit Redux lassen sich komplexe Strukturen in Projekten harmonisieren. +Das bedeutet allerdings nicht, dass Redux für jede Anwendung die passende Architektur ist. +Bewerten Sie deshalb für ein Projekt zunächst, ob sich der Einsatz von Redux wirklich lohnt. +Die Thematik ist nicht trivial – um eine solche zentrale Zustandsverwaltung sicher und effektiv einzusetzen, braucht es Zeit und Übung. +Die offizielle Dokumentation von NgRx\shortlinkFootnote{link-ngrxio} ist ein guter Ausgangspunkt, um alle Funktionen und Konzepte weiter zu studieren. + +Zusätzlich haben wir das Beispiel aus diesem Kapitel auf GitHub zur Verfügung gestellt, sodass Sie den Code vollständig nachvollziehen können. + +\qrdemo{bm5-ngrx} + + +\input{chapter/redux/componentstore} + +%%% Local Variables: +%%% TeX-PDF-mode: t +%%% mode: latex +%%% TeX-master: "../../angularBuch" +%%% End: + + +================================================ +File: /tex/chapter/tipps/prettier.tex +================================================ +\chapter[Powertipp: Codeformatierung mit Prettier]{Powertipp:\newline Codeformatierung mit Prettier} + + + +Wir haben bei der Projekteinrichtung ab \autopageref{sec:eslint} bereits das Projekt ESLint kennengelernt und eingerichtet. +ESLint führt eine syntaktische und semantische Prüfung des Codes durch, die sich nach dem Styleguide sowie gängigen Best Practices für JavaScript und TypeScript richtet. + +\footnote{Einheitliche Formatierung mit Prettier}ESLint kümmert sich allerdings nicht um die reine Formatierung des Codes: Wie werden Klammern gesetzt? Wie werden Blöcke eingerückt? Werden optionale Klammern bei Arrow Functions gesetzt? Für solche Fragen müssen wir im Entwicklungsteam einen Konsens finden und die Formatierung idealerweise automatisieren. +Im Web-Umfeld hat sich in den letzten Jahren das Projekt Prettier\shortlinkFootnote{link-prettier} durchgesetzt, um Quellcode automatisch und einheitlich zu formatieren. + +Prettier ist auf die reine Formatierung spezialisiert, während ESLint vor allem inhaltliche Prüfungen durchführt. +Trotzdem ist auch ESLint in der Lage, bestimmte Formatierungsregeln zu prüfen und zu korrigieren. +Damit beide Tools nicht gegeneinander arbeiten, sondern miteinander und unabhängig von der Reihenfolge der Ausführung immer zum gleichen Ergebnis führen, müssen wir bei der Installation und Konfiguration von Prettier einige Punkte beachten. +Die folgenden Schritte setzen voraus, dass ESLint bereits im Projekt eingerichtet ist. + +\footnote{Installation}Zunächst müssen wir Prettier mit den zugehörigen Plug-ins im Projekt installieren: + +\begin{clilisting}[style=nocaption] +npm install prettier prettier-eslint eslint-config-prettier eslint-plugin-prettier --save-dev +\end{clilisting} + +\footnote{ESLint mit Prettier konfigurieren}Wir müssen als Nächstes einige Änderungen an der Datei \file{.\mbox{eslintrc}"".json} vornehmen. +Sie sind für ein harmonisches Zusammenspiel von Prettier und ESLint notwendig. +Details und Hintergründe dazu finden Sie in der Dokumentation zum Paket `angular-eslint`.\shortlinkFootnote{link-angular-eslint-prettier} + +\pagebreakbeforelisting +\lstinputlisting[caption={ESLint-Konfiguration anpassen \textup{(\file{.eslintrc.json})}},label=lst:prettier:eslintrc]{\snippets/prettier/eslintrc.snippet.json} + +Im Anschluss ist Prettier einsatzbereit und arbeitet Hand in Hand mit ESLint. +Wir können das Tool nun über die Kommandozeile aufrufen. +Damit der Befehl mit allen Argumenten nicht immer wieder per Hand eingegeben werden muss, empfehlen wir, ein NPM-Skript in der Datei \file{package.json} zu hinterlegen: + +\pagebreakbeforelisting +\lstinputlisting[caption={NPM-Skript `format` \textup{(\file{package.json})}},label=lst:prettier:npmformat]{\snippets/prettier/package.snippet.json} + +Im Anschluss können wir Prettier wie folgt ausführen: + +\begin{clilisting}[style=nocaption] +npm run format +\end{clilisting} + +Um die Formatierung direkt im Editor Visual Studio Code durchzuführen, können wir die offizielle Extension für Prettier installieren: `esbenp.prettier-vscode`. +Diesen Namen können Sie in der Suche im Extension Browser von Visual Studio Code eingeben. + +Außerdem empfehlen wir, die folgenden Einstellungen in der Datei \file{.vscode/settings.json} im Projekt zu setzen: + +\lstinputlisting[caption={Automatische Formatierung durch Visual Studio Code \textup{(\file{.vscode/settings.json})}},label=lst:prettier:vscsettings]{\snippets/prettier/settings.json} + +Nun wird der Quellcode bei jedem Speichern der Datei mit Prettier formatiert. +Das ist während der Entwicklung besonders praktisch, weil Leerzeichen, Klammern und Einrückungen beim Tippen zunächst vernachlässigt werden können. Beim Speichern sorgt Prettier automatisch für die korrekte Formatierung. + +\footnote{Prettier konfigurieren}Das Verhalten von Prettier kann konfiguriert werden, um den individuellen Stil des Teams zu berücksichtigen. +Dazu legen wir im Hauptverzeichnis des Projekts eine Datei mit dem Namen \file{.prettierrc} an. +Alle verfügbaren Optionen sind in der offiziellen Dokumentation zu finden.\shortlinkFootnote{link-prettier-config} + +%%% Local Variables: +%%% TeX-PDF-mode: t +%%% mode: latex +%%% TeX-master: "../../angularBuch" +%%% End: + + +================================================ +File: /tex/chapter/eap.tex +================================================ +\section*{EAP -- Early Access Programm} +Lieber \{\{ Leser \}\}, \\ +\\ +wir freuen uns sehr, dass du uns bei der Verbesserung unseres Buchs unterstützen möchtest. +Mit unserem Buch möchten wir der Community einen soliden Start mit Angular anbieten. +Wir legen großen Wert auf Verständlichkeit und Praxisnähe -- daher freuen wir uns über jedes Feedback von dir.\\ +\\ +Dies ist die Vorabversion der **dritten Auflage** des Buchs. +Wir haben viele Aktualisierungen vorgenommen und teilweise auch ganze Kapitel neu aufgerollt oder strukturiert. +Viele Inhalte befinden sich noch in Arbeit oder werden von uns überdacht. +Dieses Buch wird -- wie Angular -- stetig korrigiert, verbessert und erweitert, bis es seine endgültige Form erhalten hat. + +Die **Deadline** für die Abgabe beim Verlag ist der **12. Juni 2020**. +Bis dahin müssen wir alle Korrekturen eingepflegt haben. +Das Buch erscheint dann voraussichtlich im September im Buchhandel. + +### Einreichen von Feedback +Bei Ideen, Anregungen und oder Korrekturen sende bitte eine *E-Mail* an: + +\begin{center} + **\author{\href{mailto:trello@angular-buch.com**{trello@angular-buch.com}}}. +\end{center} + +Am besten ist es, wenn Du Kommentare direkt in das PDF schreibst. +Du kannst aber gerne auch ein separates Textdokument anlegen und jeweils auf die Abschnittsnummer und die Seitenzahl verweisen. + +\begin{center} + Viel Spaß beim Lesen und vielen Dank für deine Unterstützung! +\end{center} + +### Bekannte Probleme + +\begin{itemize} +\item Die Nummerierung der Fußnoten ist teilweise fehlerhaft. +\item Es gibt zu große Abstände zu Überschriften, unschöne Seitenumbrüche oder leere Seiten. +\item Die automatische Silbentrennung erzeugt falsche Worttrennungen. +\item Tabellen und Abbildungen finden sich teilweise an unerwarteten Stellen wieder. +\end{itemize} + +Fehler in der Ausrichtung und im Satz sind momentan gar nicht interessant -- es geht zunächst um den Inhalt! + + + +%%% Local Variables: +%%% TeX-PDF-mode: t +%%% mode: latex +%%% TeX-master: "../angularBuch" +%%% End: + + + + +================================================ +File: /tex/chapter/vorwort/raetsel.tex +================================================ +\makeatletter +\@openrightfalse +\makeatother +\chapter*{Rätselzeit} % bitte nicht ins TOC + +\vspace{-2cm} +\includegraphics[width=\textwidth]{images/raetsel.pdf} + +\begin{flushleft} + \begin{tabular}{@{}rp{98mm}@{}} + **1** & Lifecycle-Methode, die bei Initialisierung der Komponente ausgeführt wird \\ + **2** & Teil der Template-Syntax, wird in runden Klammern notiert\\ + **3** & Programmiersprache von Angular\\ + **4** & Direktive zur Verlinkung zwischen Routen\\ + **5** & Sprachelement, mit dem z.\,B. eine Klasse als Komponente oder Injectable markiert wird, wird mit `@` notiert\\ + **6** & Baustein von Angular, um das Verhalten eines DOM-Elements zu ändern, besitzt aber kein eigenes Template\\ + **7** & Baustein von Reactive Forms, verwaltet mehrere Controls mit einer festen Reihenfolge\\ + **8** & Baustein aus RxJS, mit dem Multicasting realisiert werden kann, ist ein Hot Observable\\ + **9** & Konzept, um Bestandteile einer Anwendung zur Laufzeit nachzuladen, sobald sie benötigt werden\\ + **10** & Helfer im Template, um ein Datum zu formatieren\\ + \end{tabular} +\end{flushleft} + +\suppressindent +Alle Begriffe müssen ohne Leerzeichen angegeben werden.\\**Viel Spaß beim Knobeln!** + + +%%% Local Variables: +%%% TeX-PDF-mode: t +%%% mode: latex +%%% TeX-master: "../../angularBuch" +%%% End: + + +================================================ +File: /tex/chapter/vorwort/changelog-archiv.tex +================================================ + +\section*{Aktualisierungen in der zweiten Auflage} + +### Neue Kapitel + +Folgende Kapitel und Abschnitte sind in der **zweiten Auflage** neu hinzugekommen: +\begin{itemize} + \item \changelogallref{ch:09interceptors} + \item \changelogallref{sec:universal} + \item \changelogallref{P_ww} \begin{itemize} + \item \changelogallref{sec:cli:schematics} + \item \changelogallref{sec:ww:containerpresentational} + \item \changelogallref{sec:ww:ngfortrackby} + \item \changelogallref{sec:ww:third-party} + \item \changelogallref{sub:ww:update} + \end{itemize} +\end{itemize} + + +### Vollständig neu geschriebene Kapitel + +Einige bereits in der ersten Auflage existierende Kapitel wurden für die **zweite Auflage** vollständig neu aufgerollt: + +\paragraph*{\changelogallref{sec:schnellstart}} +Der Schnellstart basierte in der ersten Auflage auf einer lokalen Lösung mit SystemJS und Paketen aus einem CDN. Der neue Schnellstart setzt auf die Online-Plattform StackBlitz zum schnellen Prototyping von Webanwendungen. + +\paragraph*{\changelogallref{ch:08rxjs}} Das Prinzip der reaktiven Programmierung und das Framework RxJS haben in den letzten Jahren weiter an Bedeutung gewonnen. Das alte Kapitel zu RxJS lieferte nur einen kurzen Überblick, ohne auf Details einzugehen. Mit dieser Neufassung finden Sie jetzt eine ausführliche Einführung in die Prinzipien von reaktiver Programmierung und Observables, und es werden alle wichtigen Konzepte anhand von Beispielen erklärt. Im Gegensatz zur ersten Auflage verwenden wir die neuen *Pipeable Operators*. + +\paragraph*{\changelogallref{subsection:formularverarbeitung}} In der ersten Auflage haben wir sowohl *Template-Driven Forms* als auch *Reactive Forms* gleichbedeutend vorgestellt. Wir empfehlen mittlerweile nicht mehr den Einsatz von Template-Driven Forms. Daher stellen wir zwar beide Ansätze weiterhin vor, legen aber im Kapitel zur Formularverarbeitung einen stärkeren Fokus auf Reactive Forms. Das Praxisbeispiel wurde neu entworfen, um eine saubere Trennung der Zuständigkeiten der Komponenten zu ermöglichen. Die Erläuterungen im Grundlagenteil wurden neu formuliert, um besser für die Anforderungen aus der Praxis geeignet zu sein. + +\paragraph*{\changelogallref{sec:redux}} In den letzten zwei Jahren hat sich unserer Ansicht nach das Framework *NgRx* gegen weitere Frameworks wie *angular-redux* klar durchgesetzt. Während die erste Auflage in diesem Kapitel noch auf angular-redux setzte, arbeitet das Kapitel der zweiten Auflage durchgehend mit den *\foreignlanguage{english*Reactive Extensions for Angular (NgRx)}. Wir erarbeiten in der Einführung schrittweise ein Modell für zentrales State Management, um die Architektur von Redux zu erläutern, ohne eine konkrete Bibliothek zu nutzen. + +### Stark überarbeitete und erweiterte Kapitel + +\paragraph*{\changelogallref{chapter:typescript}} Das Grundlagenkapitel zu TypeScript wurde neu strukturiert und behandelt zusätzlich auch neuere Features von ECMAScript/TypeScript, z.\,B. Destrukturierung, Spread-Operator und Rest-Syntax.{\tolerance2000\par} + +\paragraph*{\changelogallref{ch:07http}} Das HTTP-Kapitel setzt durchgehend auf den `HttpClient`, der mit Angular 4.3 eingeführt wurde. +Dabei wird der Blick auch auf die erweiterten Features des Clients geworfen. Themen, die spezifisch für RxJS sind, wurden aus diesem Kapitel herausgelöst und werden nun im RxJS-Kapitel behandelt. + +\paragraph*{\changelogallref{sec:routing:resolver}} Resolver sind aus unserer Sicht nicht die beste Wahl, um reguläre Daten über HTTP nachzuladen. Der Iterationsschritt zu Resolvern wurde deshalb aus dem Beispielprojekt entfernt, und das Thema wird in dieser Auflage nur noch in der Theorie behandelt. + +\paragraph*{\changelogallref{ch:i18n}} Die Möglichkeiten zur Konfiguration des Builds wurden mit Angular~6.0 stark vorangebracht. +Viele zuvor notwendige Kommando\-zeilen\-para\-meter sind nun nicht mehr notwendig, die Konfigurationsdatei \mbox{\file{angular.json}} löst diese ab. Dadurch konnten wir das Kapitel zur Internationalisierung (i18n) kürzen und verständlicher gestalten. Im Gegensatz zur ersten Auflage zeigen wir nicht mehr, wie man eine Anwendung im JIT-Modus internationalisiert, der hauptsächlich für die Entwicklung vorgesehen ist, aber nicht für produktive Anwendungen.{\tolerance5000\par} + +\paragraph*{\changelogallref{sec:testing}} Das Kapitel zum Testen von Angular-Anwendungen wurde stark erweitert. Neben den reinen Werkzeugen wird der Fokus besonders auf Philosophien, Patterns und Herangehensweisen gelegt. Zusätzlich werden die mitgelieferten Tools zum Testen von HTTP und Routing betrachtet. + +\paragraph*{\changelogallref{ww:change-detection}} Das Kapitel zur Change Detection wurde für besseres Verständnis neu strukturiert. Insbesondere wird auf Debugging und Strategien zur Optimierung eingegangen. + + +================================================ +File: /tex/chapter/vorwort/ueber.tex +================================================ +% {\parindent0pt +% \thispagestyle{empty}% +% \vspace*{90mm}% +% \hspace*{49mm}% +% \parbox{10em}{\centering{\itshape It's just \glq Angular\grq.}\par\nobreak\vskip1ex\relax Igor Minar}\par +% } + +\Chapter*{Vorwort} +\ngcite{Angular is one of the most adopted frameworks on the planet.}{Brad Green}{ehem. Angular Engineering Director} + +\noindent +Angular ist eines der populärsten Frameworks für die Entwicklung von Single-Page-Appli\-ka\-ti\-onen. +Das Framework wird weltweit von großen Unternehmen eingesetzt, um modulare, skalierbare und gut wart\-bare Applikationen zu entwickeln. +Mit Angular in Version~2.0.0 setzte Google im Jahr 2016 einen Meilenstein in der Welt der modernen Web\-entwick\-lung: +Das Framework nutzt die Programmiersprache TypeScript, bietet ein ausgereiftes Tooling und ermöglicht die komponentenbasierte Entwicklung von Single-Page-Anwendungen für den Browser und für Mobilgeräte. + +In kurzer Zeit haben sich rund um Angular ein umfangreiches Ökosystem und eine vielfältige Community gebildet. +\footnote{React und Vue.js}Angular gilt neben \mbox{React.js} und \mbox{Vue.js} als eines der weltweit beliebtesten Web\-frame\-works. +Sie haben also die richtige Entscheidung getroffen, als Sie Angular für die Entwicklung Ihrer Projekte ins Auge gefasst haben. + +Der Einstieg in Angular ist umfangreich, aber die Konzepte sind durchdacht und konsequent. +\footnote{Opinionated Framework}Häufig verwendet man im Zusammenhang mit Angular das Attribut *opinionated*, das wir im Deutschen mit dem Begriff *meinungsstark* ausdrücken können: +Angular ist ein meinungsstarkes Framework, das viele klare Richtlinien zu Architektur, Codestruktur und Best Practices definiert. +Das kann zu Anfang umfangreich erscheinen, sorgt aber dafür, dass in der gesamten Community einheitliche Konventionen herrschen, Standardlösungen existieren und bestehende Bibliotheken vorausgewählt wurden. + +\footnote{Beispielanwendung}Sie werden in diesem Buch lernen, wie Sie mit Angular komponentenbasierte Single-Page-Applikationen erstellen. +Dazu entwickeln wir mit Ihnen gemeinsam eine Anwendung, anhand derer wir Ihnen die Konzepte und Features von Angular beibringen. +Wir führen Sie Schritt für Schritt durch das Framework – vom Pro\-jekt\-setup über Komponenten, Routing, Formulare und HTTP bis hin zum Testing und Deployment der Anwendung. +Auf dem Weg stellen wir Ihnen eine Reihe von Tools, Tipps und Best Practices vor, die wir in mehr als sechs Jahren Praxisalltag mit Angular sammeln konnten. +Die umfangreichen Theorie\-teile eignen sich auch später als Nachschlagewerk im Entwicklungs\-alltag. + +Nach dem Lesen dieses Praxisbuchs sind Sie in der Lage, +\begin{itemize} + \item das Zusammenspiel der Funktionen von Angular sowie das Konzept hinter dem Framework zu verstehen, + \item modulare, strukturierte und wartbare Webanwendungen mithilfe von Angular zu entwickeln sowie + \item durch die Entwicklung von Tests qualitativ hochwertige Anwendungen zu erstellen. +\end{itemize} + +Die Entwicklung mit Angular macht vor allem eines: *Spaß!* +Diesen Enthusiasmus für das Framework und für Webtechnologien möchten wir Ihnen in diesem Buch vermitteln – wir nehmen Sie mit auf die Reise in die Welt der modernen Webentwicklung! + +\section*{Versionen und Namenskonvention: Angular~vs.~AngularJS} + + +In diesem Buch dreht sich alles um das Framework Angular. +Die Geschichte dieses Projekts reicht zurück bis ins Jahr 2009 zur Vorgängerversion *AngularJS*. +Bis auf den ähnlichen Namen und einige Konzepte haben die beiden Frameworks aber nichts miteinander zu tun: +Angular ab Version~2 ist eine vollständige Neuentwicklung und ist nicht mit dem alten AngularJS kompatibel. + +\footnote{It's just "`Angular"'.}Die offizielle Bezeichnung für das Framework ist *Angular*, ohne Angabe der Programmiersprache und ohne eine spezifische Versionsnummer. +Angular erschien im September 2016 in der Version~2.0.0 und hat viele neue Konzepte und Ideen in die Community gebracht. +Um Verwechslungen auszuschließen, gilt also die folgende Konvention: +\begin{itemize} + \item **Angular** – das Angular-Framework ab **Version~2 und höher** (dieses Buch ist durchgängig auf dem Stand von Angular~15) + \item **AngularJS** – das Angular-Framework in der **Version~1.x.x** +\end{itemize} + +AngularJS wird seit Januar 2022 offiziell nicht mehr weiterentwickelt und sollte nicht mehr verwendet werden.\shortlinkFootnote{link-version-support-status} +Sie haben also die richtige Entscheidung getroffen, Angular ab Version~2.0.0 einzusetzen. + +\footnote{Semantic Versioning}Die Versionsnummer~*x.y.z* basiert auf *Semantic Versioning*.\shortlinkFootnote{link-0} +Der Re\-lease-Zyklus von Angular ist kontinuierlich geplant: +Im Rhythmus von ungefähr sechs Monaten erscheint eine neue Major-Version~*x*. +Die Minor-Versionen *y* werden monatlich herausgegeben, nachdem eine Major-Version erschienen ist. +Jede Major-Version wird planmäßig für 1,5~Jahre unterstützt und weiterentwickelt (Long-Term Support). +\begingroup\renewcommand*{\thefigure}{\arabic{figure}} +\makeatletter\def\DP@pageref{1}\makeatother + +\begin{widefig} + {fig:angular-timeline} + {Zeitleiste der Entwicklung von Angular} + \epsfig{width=\totalwidth, file=images/vorwort/timeline-angular} +\end{widefig} +\endgroup + +\vspace{-2\baselineskip} +\section*{Umgang mit Aktualisierungen} + +Das Release einer neuen Major-Version von Angular bedeutet keineswegs, dass alle Ideen verworfen werden und Ihre Software nach einem Update nicht mehr funktioniert. +Auch wenn Sie eine neuere Angular-Version verwenden, behalten die in diesem Buch beschriebenen Konzepte ihre Gültigkeit. +Die Grundideen von Angular sind seit Version~2.0.0 konsistent und auf Beständigkeit über einen langen Zeitraum ausgelegt. +Alle Updates zwischen den Major-Versionen waren in der Vergangenheit problemlos möglich, ohne dass Breaking Changes die gesamte Anwendung unbenutzbar machen. +Gibt es doch gravierende Änderungen, so werden stets ausführliche Informationen und Tools zur Migration angeboten. + +Auf der Website zu diesem Buch finden Sie die Codebeispiele für das Beispielprojekt und viele weiterführende Informationen. +Unter anderem veröffentlichen wir dort zu jeder Major-Version einen Artikel mit den wichtigsten Neuerungen in Angular. +Wir empfehlen Ihnen aus diesem Grund, unbedingt einen Blick auf die Begleitwebsite zu werfen, bevor Sie beginnen, sich mit den Inhalten des Buchs zu beschäftigen: + +\qrlink[Die Begleitwebsite zum~Buch]{https://angular-buch.com} + +\section*{An wen richtet sich das Buch?} + +\footnote{Erfahrung in Softwareentwicklung}Dieses Buch richtet sich an Menschen, die bereits grundlegende Kenntnisse in der Softwareentwicklung mitbringen. +Vorwissen zu JavaScript und HTML ist von Vorteil – es ist aber keine Voraussetzung, um mit diesem Buch Angular zu lernen. +Wenn Sie jedoch bereits mit der Webentwicklung vertraut sind, werden Sie mit diesem Buch schnell starten können. +Falls Sie gar keine Erfahrung in HTML und JavaScript mitbringen, empfehlen wir Ihnen, zunächst die grundlegenden Kenntnisse in diesen Bereichen zu festigen. + +\footnote{TypeScript}Für die Entwicklung mit Angular nutzen wir die populäre Programmiersprache TypeScript. +Doch keine Angst: TypeScript ist eine Erweiterung von JavaScript, und die Konzepte sind sehr eingängig und schnell gelernt. +Wenn Sie bereits eine stark typisierte Sprache wie Java oder C\# kennen, wird Ihnen der Einstieg in TypeScript nicht schwerfallen. + +Sie benötigen \footnote{Keine Angular-""Vorkenntnisse nötig!}*keinerlei* Vorkenntnisse im Umgang mit Angular bzw. AngularJS. +Ebenso müssen Sie sich nicht vorab mit benötigten Tools und Hilfsmitteln für die Entwicklung von Angular-Applikationen vertraut machen. +Das nötige Wissen darüber wird Ihnen in diesem Buch vermittelt. + +\footnote{Kein klassisches Nachschlagewerk}Wir erschließen uns die Welt von Angular praxisorientiert anhand eines Beispielprojekts. +Jedes Thema wird zunächst ausführlich in der Theorie behandelt, sodass Sie die Grundlagen auch losgelöst vom Beispielprojekt nachlesen können. +Wir wollen einen soliden Einstieg in Angular bieten, Best Practices zeigen und Schwerpunkte bei speziellen fortgeschrittenen Themen setzen. +Die meisten Aufgaben aus dem Entwicklungsalltag werden Sie mit den vielen praktischen Beispielen souverän meistern können. + +Wir hoffen, dass dieses Buch Ihre tägliche Begleitung bei der Arbeit mit Angular wird. +\footnote{Offizielle Angular-""Dokumentation}Für Details zu den einzelnen Framework-Funktionen empfehlen wir immer auch einen Blick in die offizielle Dokumentation.\shortlinkFootnote{link-1} + + + +\section*{Wie ist dieses Buch zu lesen?} + +\footnote{Einführung, Tools und Schnellstart}Im ersten Teil des Buchs lernen Sie die verwendeten Tools und die benötigten Werkzeuge kennen. +Im Schnellstart tauchen wir sofort in Angular ein und nehmen Sie mit zu einem einfachen Einstieg in das Framework und den Grundaufbau einer Anwendung. + +\footnote{Einführung in TypeScript}Der zweite Teil vermittelt Ihnen einen Einstieg in TypeScript. +Sie werden hier mit den Grundlagen dieser typisierten Skriptsprache vertraut gemacht. +Wenn Sie bereits Erfahrung im Umgang mit TypeScript haben, können Sie diesen Teil auch überspringen und bei Bedarf später einzelne Themen nachlesen. + +\footnote{Beispielanwendung}Der dritte Teil ist der Hauptteil des Buchs: +Hier werden wir mit Ihnen zusammen eine Beispielanwendung entwickeln. +Die Konzepte und Technologien von Angular wollen wir dabei direkt am Beispiel vermitteln. +\footnote{17~Praxiskapitel}Wir haben das Projekt in 17~einzelne Kapitel eingeteilt. +In jedem Teil setzen wir gemeinsam mit Ihnen neue Anforderungen und technische Aspekte im Beispielprojekt um. + +\begin{itemize}\raggedright + \item \nameref{ch:01comp} {\small (ab~S.~\pageref{ch:01comp})} + \item \nameref{ch:02prop} {\small (ab~S.~\pageref{ch:02prop})} + \item \nameref{ch:03eb} {\small (ab~S.~\pageref{ch:03eb})} + \item \nameref{ch:04modules} {\small (ab~S.~\pageref{ch:04modules})} + \item \nameref{ch:05di} {\small (ab~S.~\pageref{ch:05di})} + \item \nameref{ch:06routing} {\small (ab~S.~\pageref{ch:06routing})} + \item \nameref{ch:07http} {\small (ab~S.~\pageref{ch:07http})} + \item \nameref{ch:08rxjs} {\small (ab~S.~\pageref{ch:08rxjs})} + \item \nameref{ch:09interceptors} {\small (ab~S.~\pageref{ch:09interceptors})} + \item \nameref{ch:10tdf} {\small (ab~S.~\pageref{ch:10tdf})} + \item \nameref{ch:11rf} {\small (ab~S.~\pageref{ch:11rf})} + \item \nameref{ch:12val} {\small (ab~S.~\pageref{ch:12val})} + \item \nameref{ch:13pipes} {\small (ab~S.~\pageref{ch:13pipes})} + \item \nameref{ch:14directives} {\small (ab~S.~\pageref{ch:14directives})} + \item \nameref{ch:15lazy} {\small (ab~S.~\pageref{ch:15lazy})} + \item \nameref{ch:16guards} {\small (ab~S.~\pageref{ch:16guards})} + \item \nameref{ch:17standalone} {\small (ab~S.~\pageref{ch:17standalone})} +\end{itemize} + +Jedes dieser Kapitel besteht immer aus einem umfangreichen Theorieteil und der praktischen Implementierung im Beispielprojekt. +Neben fachlichen Themen führen wir Refactorings durch, die die Architektur oder den Codestil der Anwendung verbessern. +\footnote{Powertipps}In mehreren *Powertipps* zwischen den Kapiteln zeigen wir außerdem hilfreiche Werkzeuge, die uns bei der Entwicklung zur Seite stehen. + +\footnote{Projektübergreifende Themen}Nachdem alle Praxiskapitel erfolgreich absolviert wurden, widmen wir uns einer Auswahl von projektübergreifenden Themen: +Im Kapitel zu *Softwaretests* erfahren Sie, wie Sie Ihre Angular-Anwendung automatisiert testen und so die Softwarequalität sichern können. +Dieses Kapitel kann sowohl nach der Entwicklung des Beispielprojekts als auch parallel dazu bestritten werden. +Außerdem widmen wir uns ausführlich der *Barrierefreiheit*: In diesem Kapitel stellen wir Grundlagen und konkrete Maßnahmen vor, um die Anwendung für möglichst viele Menschen zugänglich zu machen. +Zum Schluss werfen wir einen differenzierten Blick auf die *Lokalisierung* und *Internationalisierung*, um die Anwendung für den mehrsprachigen Betrieb vorzubereiten. + + +\footnote{Deployment}Im fünften Teil des Buchs dreht sich alles um das Deployment einer Angular-Anwendung. +Dabei betrachten wir die Hintergründe und Konfiguration des Build-Prozesses und erläutern die Bereitstellung mithilfe von Docker. + +\footnote{Fortgeschrittene Themen: NgRx, SSR und~PWA}Im sechsten Teil möchten wir Ihnen einige Ansätze näherbringen, die über eine Standardanwendung hinausgehen. +Hier stellen wir zunächst das *Redux*-Pattern und das populäre Framework *NgRx* vor, mit dem wir den Anwendungszustand zentral und gut wartbar verwalten können. +Mit *Server-Side Rendering (SSR)* machen Sie Ihre Anwendung fit für Suchmaschinen und verbessern zusätzlich die wahrgenommene Geschwindigkeit beim initialen Start. +Außerdem werfen wir einen ausführlichen Blick auf *Progressive Web Apps (PWA)*, um eine Webanwendung elegant auf Mobilgeräten zu nutzen. +%Schließlich betrachten wir fortgeschrittene Aspekte der Angular CLI: Monorepos und Schematics. + +\footnote{Wissenswertes}Im letzten Teil Wissenswertes\ finden Sie weitere Informationen zu wissenswerten und begleitenden Themen. +Hier haben wir weiterführende Inhalte zusammengetragen, auf die wir im Beispielprojekt nicht ausführlich eingehen. -Nach der Installation von `StoreDevtoolsModule` kannst du die Redux DevTools im Browser nutzen. Du siehst alle dispatched Actions, den State vor und nach der Action und kannst Time-Travel-Debugging betreiben. -### Weiterführende Themen +\section*{Selbst tippen statt Copy \& Paste} -Im Buch werden außerdem behandelt: +Der gesamte Code für das Beispielprojekt steht auf der Plattform GitHub zur Verfügung. +Wir wissen genau, wie groß die Versuchung ist, größere Teile des Codes von dort zu kopieren und so die Tipparbeit zu sparen. +\footnote{Abtippen heißt Lernen und Verstehen.}Aber: Kopieren und Einfügen ist nicht dasselbe wie *Lernen* und *Verstehen*. +Wenn Sie die Codebeispiele selbst *tippen*, werden Sie besser verstehen, wie Angular funktioniert, und werden die Software später erfolgreich in der Praxis einsetzen können. +Jeder einzelne Quelltext, den Sie abtippen, trainiert Ihre Hände, Ihr Gehirn und Ihre Sinne. +Wir möchten Sie deshalb ermutigen: Betrügen Sie sich nicht selbst. Der bereitgestellte Quelltext im Repository sollte lediglich der Überprüfung dienen. +Wir wissen, wie schwer das ist, aber vertrauen Sie uns: Es zahlt sich aus, denn Übung macht den Meister! -- `createActionGroup` für gruppierte Actions -- Entity-Management mit `@ngrx/entity` -- Testing von Reducers, Selectors und Effects (auch mit Marble-Tests) -- `@ngrx/component` und `@ngrx/component-store` -- Facades zur Abstraktion des Stores +\section*{Angular.Schule: Workshops und Beratung} ---- +Wir, die Autoren dieses Buchs, arbeiten seit Langem als Berater und Trainer für Angular. +Wir haben die Erfahrung gemacht, dass man Angular in kleinen Gruppen am effektivsten lernen kann. +In einem Workshop kann auf individuelle Fragen und Probleme direkt eingegangen werden – und es macht auch am meisten Spaß! -**Zurück zu Teil 1:** [Wie kommen wir zu zentralem State Management?](/material/ngrx-intro) +Schauen Sie auf *\url{https://angular.schule*} vorbei. +Dort bieten wir Ihnen Angular-Schulungen in den Räumen Ihres Unternehmens, in offenen Gruppen oder als Online-Kurs an. +Das Angular-Buch verwenden wir dabei in unseren Kursen zur Nacharbeit. +Wir freuen uns auf Ihren Besuch! -*Dieser Artikel ist ein möglichst literaler Port des Kapitels „State Management mit Redux und NgRx“ aus dem Buch „Angular: Das große Praxisbuch (4. Auflage)“.* -``` \ No newline at end of file From 03e8ccbc1486b282f0bc72ed920f1522ea315753 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Fri, 12 Jun 2026 15:32:08 +0200 Subject: [PATCH 04/24] material(ngrx): Teil 2 (Global Store) als sauberen 1:1-Port + Teil-3-Stub - Teil 2 aus dem rohen Tool-Dump zu sauberem Markdown aufbereitet: - alle LaTeX-Artefakte entfernt (\lstinputlisting, \footnote, \mbox, \file, \begin{...}, \autoref, \index, Layout-Makros usw.) - alle Code-Listings aus den Buch-Snippets eingesetzt (vorher fehlten sie komplett, es standen nur die \lstinputlisting-Direktiven da) - versehentlich mitkopierte Fremdkapitel (Prettier, EAP, Raetsel, Changelog, Vorwort) entfernt - Frontmatter, Serien-Header (3-teilig) und [[toc]] ergaenzt - DevTools-Screenshots eingebunden, 4 Diagramme als TODO markiert - Shortlinks aufgeloest, gerade Anfuehrungszeichen, z. B. statt z.\,B. - Teil 3 (SignalStore) als Stub mit Frontmatter und Platzhalter angelegt --- material/ngrx-global-store/README.md | 2186 ++++++++++++-------------- material/ngrx-signal-store/README.md | 22 + 2 files changed, 1013 insertions(+), 1195 deletions(-) create mode 100644 material/ngrx-signal-store/README.md diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index fc0bf436..d5a1aa20 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -1,1463 +1,1259 @@ +--- +title: "State Management mit NgRx – Teil 2: Global Store mit NgRx" +published: "2026-06-11" +lastModified: "2026-06-12" +hidden: true +--- + +**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (4. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** + +Dieser Artikel ist **Teil 2** einer dreiteiligen Serie zum Thema State Management mit NgRx. + +- Teil 1: Wie kommen wir zu zentralem State Management? → [zum Artikel](/material/ngrx-intro) +- Teil 2: Global Store mit NgRx (dieser Artikel) +- Teil 3: SignalStore → [zum Artikel](/material/ngrx-signal-store) + +[[toc]] + +Im ersten Teil haben wir uns Schritt für Schritt ein eigenes Modell für zentrales State Management erarbeitet und dabei die Grundprinzipien von Redux kennengelernt. Jetzt setzen wir diese Ideen mit dem Framework NgRx in die Praxis um. + ## NgRx: Reactive Extensions for Angular -Das Framework *Reactive Extensions for Angular (NgRx)* ist eine der populärsten Implementierungen für State Management mit Angular. -Durch die gezielte Ausrichtung auf Angular fügt sich der Code gut in die Strukturen und Lebenszyklen einer Angular-Anwendung ein. -NgRx setzt stark auf die Möglichkeiten der reaktiven Programmierung mit RxJS, ist also an vielen Stellen von Observables und Datenströmen geprägt. -Die große Community und eine Reihe von verwandten Projekten machen NgRx zum wohl bekanntesten Werkzeug für Zustandsverwaltung mit Angular. +Das Framework *Reactive Extensions for Angular (NgRx)* ist eine der populärsten Implementierungen für State Management mit Angular. Durch die gezielte Ausrichtung auf Angular fügt sich der Code gut in die Strukturen und Lebenszyklen einer Angular-Anwendung ein. NgRx setzt stark auf die Möglichkeiten der reaktiven Programmierung mit RxJS, ist also an vielen Stellen von Observables und Datenströmen geprägt. Die große Community und eine Reihe von verwandten Projekten machen NgRx zum wohl bekanntesten Werkzeug für Zustandsverwaltung mit Angular. -Wir wollen in diesem Abschnitt die Struktur und die Bausteine in der Welt von NgRx genauer besprechen. -Außerdem wollen wir im BookMonkey einen Aspekt mithilfe von NgRx umsetzen, um so alle Bausteine auch praktisch zu üben. +Wir wollen in diesem Abschnitt die Struktur und die Bausteine in der Welt von NgRx genauer besprechen. Außerdem wollen wir im BookMonkey einen Aspekt mithilfe von NgRx umsetzen, um so alle Bausteine auch praktisch zu üben. ### Projekt vorbereiten -Als Grundlage für diesen praktischen Teil verwenden wir das Beispielprojekt BookMonkey in der Version nach dem Kapitel zu Guards, also bevor wir die Anwendung auf Standalone Components umgestellt haben. -Möchten Sie mitentwickeln, so können Sie Ihr bestehendes Book\-Monkey-Projekt verwenden oder neu starten und den Code über GitHub herunterladen: +Als Grundlage für diesen praktischen Teil verwenden wir das Beispielprojekt BookMonkey in der Version nach dem Kapitel zu Guards, also bevor wir die Anwendung auf Standalone Components umgestellt haben. Möchten Sie mitentwickeln, so können Sie Ihr bestehendes BookMonkey-Projekt verwenden oder neu starten und den Code über GitHub herunterladen: -\begin{clilisting}[caption={BookMonkey als Grundlage für NgRx verwenden},breakatwhitespace=true,belowskip=0pt] +```bash git clone https://ng-buch.de/bm5-16-guards.git book-monkey-ngrx cd book-monkey-ngrx npm install -\end{clilisting} +``` ### Store einrichten -Im Projektverzeichnis müssen wir zunächst alle Abhängigkeiten installieren, die wir für die Arbeit mit NgRx benötigen. -NgRx verfügt über eigene Schematics zur Einrichtung in einem bestehenden Angular-Projekt. -Die folgenden Befehle integrieren einen vorbereiteten \mbox{Store} in die bestehende Anwendung: -\begin{clilisting}[style=nocaption, label=lst:ngrx:storeeinrichten] +Im Projektverzeichnis müssen wir zunächst alle Abhängigkeiten installieren, die wir für die Arbeit mit NgRx benötigen. NgRx verfügt über eigene Schematics zur Einrichtung in einem bestehenden Angular-Projekt. Die folgenden Befehle integrieren einen vorbereiteten Store in die bestehende Anwendung: + +```bash ng add @ngrx/store --defaults ng add @ngrx/store-devtools ng add @ngrx/effects -\end{clilisting} - - +``` - -Später wollen wir einen zusätzlichen Baustein kennenlernen, der im originalen Redux nicht vorgesehen ist und der spezifisch für NgRx ist: Effects auf Basis von `@ngrx/effects`. -Deshalb haben wir das notwendige Paket in diesem Schritt gleich mit eingefügt. -Die Store DevTools sind hilfreich zum Debugging der Anwendung – wir werden später in \autoref{sec:redux-developer-tools} ab \autopageref{sec:redux-developer-tools} genauer darauf eingehen, um den Lesefluss in diesem Kapitel nicht zu unterbrechen. +Später wollen wir einen zusätzlichen Baustein kennenlernen, der im originalen Redux nicht vorgesehen ist und der spezifisch für NgRx ist: Effects auf Basis von `@ngrx/effects`. Deshalb haben wir das notwendige Paket in diesem Schritt gleich mit eingefügt. Die Store DevTools sind hilfreich zum Debugging der Anwendung – wir werden später im Abschnitt zu den Redux DevTools genauer darauf eingehen, um den Lesefluss in diesem Kapitel nicht zu unterbrechen. ### Schematics nutzen -Um nach der Einrichtung die Bausteine von NgRx mithilfe der Angular CLI anzulegen, können wir das Paket `@ngrx/schematics` nutzen. -Es erweitert die Fähigkeiten der Angular CLI, sodass wir unsere Actions, Reducers und Effects bequem mithilfe von `ng generate` anlegen können. -Auch diese Abhängigkeit wird mittels `ng add` installiert. -\begin{clilisting}[style=nocaption] -ng add @ngrx/schematics -\end{clilisting} +Um nach der Einrichtung die Bausteine von NgRx mithilfe der Angular CLI anzulegen, können wir das Paket `@ngrx/schematics` nutzen. Es erweitert die Fähigkeiten der Angular CLI, sodass wir unsere Actions, Reducers und Effects bequem mithilfe von `ng generate` anlegen können. Auch diese Abhängigkeit wird mittels `ng add` installiert. +```bash +ng add @ngrx/schematics +``` -\footnote{Schematic Collection festlegen}Die Schematics von NgRx werden durch diesen Aufruf automatisch im Projekt registriert. -\pagebreak -Jeder Aufruf von `ng generate` durchsucht dann auch die Skripte in diesem Paket. -So können wir bequem einen Befehl wie `ng \mbox{generate` \mbox{action}} verwenden, ohne die Zielkollektion explizit angeben zu müssen. -Die Collection wird mit einem Eintrag in der Datei \file{angular.json} festgelegt, den Sie jederzeit wieder löschen oder ändern können, falls Sie die Skripte von NgRx nicht mehr nutzen möchten. +Die Schematics von NgRx werden durch diesen Aufruf automatisch im Projekt registriert. Jeder Aufruf von `ng generate` durchsucht dann auch die Skripte in diesem Paket. So können wir bequem einen Befehl wie `ng generate action` verwenden, ohne die Zielkollektion explizit angeben zu müssen. Die Collection wird mit einem Eintrag in der Datei `angular.json` festgelegt, den Sie jederzeit wieder löschen oder ändern können, falls Sie die Skripte von NgRx nicht mehr nutzen möchten. ### Grundstruktur -Die ausgeführten Befehle haben bereits alles Nötige eingerichtet, sodass wir sofort mit der Implementierung beginnen können. -Vorher wollen wir jedoch einen Blick auf die Änderungen werfen, die von den Schematics an unserem Projekt vorgenommen wurden. +Die ausgeführten Befehle haben bereits alles Nötige eingerichtet, sodass wir sofort mit der Implementierung beginnen können. Vorher wollen wir jedoch einen Blick auf die Änderungen werfen, die von den Schematics an unserem Projekt vorgenommen wurden. -Neben allen benötigten Abhängigkeiten in der \file{package.json} sind einige neue Imports im `AppModule` hinzugekommen. -Das `StoreModule` bringt den Kern des NgRx-Stores in die Anwendung. -Die verwendete Methode `forRoot()` erwartet zwei Argumente: -Im ersten Objekt können wir angeben, welche Reducers für welchen Teil des State-Objekts verantwortlich sind (eine sogenannte `ActionReducerMap`).\footnote{ActionReducerMap} -Üblicherweise nutzen wir dieses Objekt nicht, um die State-Struktur für unsere Features zu definieren, denn dafür existiert ein anderer, dynamischerer Weg. -Verwenden wir allerdings das Paket `@ngrx/router-store`, so müssen wir das Mapping für den Router hier statisch im `AppModule` konfigurieren.\footnote*{Auf das Paket `@ngrx/router-store` gehen wir zum Ende dieses Kapitels ab \autopageref{sec:redux:routing} noch ein.} -Im zweiten Argument von `forRoot()` können wir ein Konfigurationsobjekt übergeben.\footnote{Konfiguration für den Store} -Da wir diese beiden Aspekte momentan nicht nutzen wollen, sind lediglich zwei leere Objekte als Argumente angegeben. +Neben allen benötigten Abhängigkeiten in der `package.json` sind einige neue Imports im `AppModule` hinzugekommen. Das `StoreModule` bringt den Kern des NgRx-Stores in die Anwendung. Die verwendete Methode `forRoot()` erwartet zwei Argumente: Im ersten Objekt können wir angeben, welche Reducers für welchen Teil des State-Objekts verantwortlich sind (eine sogenannte `ActionReducerMap`). Üblicherweise nutzen wir dieses Objekt nicht, um die State-Struktur für unsere Features zu definieren, denn dafür existiert ein anderer, dynamischerer Weg. Verwenden wir allerdings das Paket `@ngrx/router-store`, so müssen wir das Mapping für den Router hier statisch im `AppModule` konfigurieren. Im zweiten Argument von `forRoot()` können wir ein Konfigurationsobjekt übergeben. Da wir diese beiden Aspekte momentan nicht nutzen wollen, sind lediglich zwei leere Objekte als Argumente angegeben. -Außerdem sind zwei weitere Imports für `Effects\-Module` und `Store\-DevTools\-Module` eingetragen worden. -Diese beiden Module binden Effects und die Store DevTools ein, wir wollen uns aber zu diesem Zeitpunkt noch nicht detaillierter damit auseinandersetzen. +Außerdem sind zwei weitere Imports für `EffectsModule` und `StoreDevtoolsModule` eingetragen worden. Diese beiden Module binden Effects und die Store DevTools ein, wir wollen uns aber zu diesem Zeitpunkt noch nicht detaillierter damit auseinandersetzen. -\lstinputlisting[caption={Imports für NgRx im `AppModule` \textup{(\file{app.module.ts})}}, label=lst:ngrx:appmoduleimports, language=js,breakatwhitespace=true,belowskip=1em]{\currentBm/src/app/app.module.ts.snippet} +```ts +// app.module.ts +import { NgModule, isDevMode } from '@angular/core'; +import { StoreModule } from '@ngrx/store'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { EffectsModule } from '@ngrx/effects'; -Mit dieser Konfiguration ist der Store zwar schon aktiv, aber wir haben noch nicht festgelegt, wie das zentrale State-Objekt strukturiert sein soll. -Da wir auch mit NgRx modular entwickeln, setzen wir diese Änderungen allerdings nicht direkt im `AppModule` um. -Stattdessen lagern wir alle neuen Bausteine in eigene Dateien aus und nutzen dafür die Angular~CLI. +@NgModule({ + // ... + imports: [ + // ... + StoreModule.forRoot({}, {}), + StoreDevtoolsModule.instrument( + { maxAge: 25, logOnly: !isDevMode() } + ), + EffectsModule.forRoot([]), + ], + // ... +}) +export class AppModule { } +``` -\begin{kasten} -\caption{NgRx und Standalone Components}Im Zusammenhang mit Standalone Components kann es sein, dass die Anwendung kein `AppModule` besitzt, sondern die `AppComponent` direkt gebootstrappt wird, siehe dazu der Abschnitt ab \autopageref{sec:17standalone:ohnemodule}. -Damit wir in diesem Fall nicht die Module importieren müssen, bietet NgRx verschiedene Funktionen an, um den Store einzurichten: `provideStore()`, `provideEffects()` und `provide\-State()`. -\end{kasten} +Mit dieser Konfiguration ist der Store zwar schon aktiv, aber wir haben noch nicht festgelegt, wie das zentrale State-Objekt strukturiert sein soll. Da wir auch mit NgRx modular entwickeln, setzen wir diese Änderungen allerdings nicht direkt im `AppModule` um. Stattdessen lagern wir alle neuen Bausteine in eigene Dateien aus und nutzen dafür die Angular CLI. -### Feature anlegen +> **NgRx und Standalone Components:** Im Zusammenhang mit Standalone Components kann es sein, dass die Anwendung kein `AppModule` besitzt, sondern die `AppComponent` direkt gebootstrappt wird. Damit wir in diesem Fall nicht die Module importieren müssen, bietet NgRx verschiedene Funktionen an, um den Store einzurichten: `provideStore()`, `provideEffects()` und `provideState()`. -Unsere Anwendung ist bereits in Module strukturiert, die einzelne Features der Anwendung kapseln. -Diese Einteilung findet sich auch wieder, wenn es um die Einrichtung des Stores für NgRx geht. -Jedes Feature erhält einen eigenen Satz an Actions, Reducers und Effects, die auch nur für genau dieses Feature und den zugehörigen State zuständig sind. -So verhindert man eine monolithische Struktur, in der verschiedene Zuständigkeiten ungewollt vermischt werden. -Um NgRx für ein existierendes Feature-Modul aufzusetzen, verwenden wir den folgenden Befehl: -\begin{clilisting}[style=nocaption,breakatwhitespace=true] -ng g feature books/store/book --module books/books --api --defaults -\end{clilisting} - -Dieser Aufruf legt das Feature `book` im Ordner \dir{src/app/books/store} an. -Wir haben hier bewusst den Unterordner \dir{store} gewählt, um alle Bestandteile von NgRx sauber in einem gemeinsamen Unterordner zu gruppieren. -\footnote{Feature-Name im Singular}Wichtig ist, dass der Feature-Name `book` hier im Singular angegeben wird, denn die CLI wird beim Anlegen automatisch ein Plural-s für einige Bausteine hinzufügen. -Die nötigen Imports und die Verdrahtung in der Anwendung sollen in das zugehörige `Books\-Module` integriert werden, auf das wir mit der Option \mbox{`-{`-module}} verweisen. -Mit der Option `-{`-api} generieren wir außerdem das nötige Grundgerüst, um Daten zu behandeln, die von einer API abgerufen werden. -Wie sich das auswirkt, werden wir gleich noch betrachten. -Die letzte Option \mbox{`-{`-defaults}} setzt weitere notwendige Einstellungen auf die vordefinierten Standardwerte. - -Die Dateistruktur in der Anwendung sieht nun wie folgt aus:\nopagebreak -\DDirtree{% - .1 \dir{src} . - .2 \dir{app} . - .3 \dir{books} . - .4 **\dir{store**} . - .5 **book.actions.ts** . - .5 **book.reducers.ts** . - .5 **book.selectors.ts** . - .5 **book.effects.ts** . - .5 \dots . - .4 books.module.ts . - .4 \dots . - .3 app.module.ts . - .3 \dots . -} - -Neben neuen Dateien sind im `BooksModule` weitere Imports hinzugekommen. -Hier wurde ebenfalls das `StoreModule` importiert, allerdings mit der Methode `forFeature()`. -Außerdem wurde eine Effects-Klasse mit dem `EffectsModule` registriert. +### Feature anlegen -\lstinputlisting[caption={NgRx einrichten im BooksModule \textup{(\file{books.module.ts})}}, label=lst:ngrx:booksmodule, language=js,breakatwhitespace=true]{\currentBm/src/app/books/books.module.ts.snippet} +Unsere Anwendung ist bereits in Module strukturiert, die einzelne Features der Anwendung kapseln. Diese Einteilung findet sich auch wieder, wenn es um die Einrichtung des Stores für NgRx geht. Jedes Feature erhält einen eigenen Satz an Actions, Reducers und Effects, die auch nur für genau dieses Feature und den zugehörigen State zuständig sind. So verhindert man eine monolithische Struktur, in der verschiedene Zuständigkeiten ungewollt vermischt werden. Um NgRx für ein existierendes Feature-Modul aufzusetzen, verwenden wir den folgenden Befehl: -\footnote{State dynamisch erweitern}Dieser Aufruf von `forFeature()` ist essenziell, denn er definiert die Struktur des globalen State-Objekts. -Die Konstante `fromBook.bookFeatureKey` aus der Datei \file{book.reducer.ts} enthält den String `book`. -Damit wird festgelegt, unter welchem Namen die Zustände dieses Features im globalen State-Objekt zu finden sein werden. -Das zentrale State-Objekt wird also durch diesen Aufruf von `forFeature()` automatisch erweitert, und die Reducers aus dem Feature werden in die Anwendung integriert. -Diese dynamische Erweiterung ist nötig, damit ein Feature-Modul auch mithilfe von Lazy Loading asynchron zur Laufzeit nachgeladen werden kann.\footnote{Lazy Loading} -Der Feature-Key `book` verweist auf den Teilbaum im State-Objekt, in den der Feature-State eingebaut wird. +```bash +ng g feature books/store/book --module books/books --api --defaults +``` + +Dieser Aufruf legt das Feature `book` im Ordner `src/app/books/store` an. Wir haben hier bewusst den Unterordner `store` gewählt, um alle Bestandteile von NgRx sauber in einem gemeinsamen Unterordner zu gruppieren. Wichtig ist, dass der Feature-Name `book` hier im Singular angegeben wird, denn die CLI wird beim Anlegen automatisch ein Plural-s für einige Bausteine hinzufügen. Die nötigen Imports und die Verdrahtung in der Anwendung sollen in das zugehörige `BooksModule` integriert werden, auf das wir mit der Option `--module` verweisen. Mit der Option `--api` generieren wir außerdem das nötige Grundgerüst, um Daten zu behandeln, die von einer API abgerufen werden. Wie sich das auswirkt, werden wir gleich noch betrachten. Die letzte Option `--defaults` setzt weitere notwendige Einstellungen auf die vordefinierten Standardwerte. + +Die Dateistruktur in der Anwendung sieht nun wie folgt aus: + +``` +src/ +└── app/ + ├── books/ + │ ├── store/ + │ │ ├── book.actions.ts + │ │ ├── book.reducer.ts + │ │ ├── book.selectors.ts + │ │ ├── book.effects.ts + │ │ └── ... + │ ├── books.module.ts + │ └── ... + ├── app.module.ts + └── ... +``` + +Neben neuen Dateien sind im `BooksModule` weitere Imports hinzugekommen. Hier wurde ebenfalls das `StoreModule` importiert, allerdings mit der Methode `forFeature()`. Außerdem wurde eine Effects-Klasse mit dem `EffectsModule` registriert. + +```ts +// books/books.module.ts +import { StoreModule } from '@ngrx/store'; +import * as fromBook from './store/book.reducer'; +import { EffectsModule } from '@ngrx/effects'; +import { BookEffects } from './store/book.effects'; + +@NgModule({ + // ... + imports: [ + // ... + StoreModule.forFeature(fromBook.bookFeatureKey, fromBook.reducer), + EffectsModule.forFeature([BookEffects]) + ] +}) +export class BooksModule { } +``` + +Dieser Aufruf von `forFeature()` ist essenziell, denn er definiert die Struktur des globalen State-Objekts. Die Konstante `fromBook.bookFeatureKey` aus der Datei `book.reducer.ts` enthält den String `book`. Damit wird festgelegt, unter welchem Namen die Zustände dieses Features im globalen State-Objekt zu finden sein werden. Das zentrale State-Objekt wird also durch diesen Aufruf von `forFeature()` automatisch erweitert, und die Reducers aus dem Feature werden in die Anwendung integriert. Diese dynamische Erweiterung ist nötig, damit ein Feature-Modul auch mithilfe von Lazy Loading asynchron zur Laufzeit nachgeladen werden kann. Der Feature-Key `book` verweist auf den Teilbaum im State-Objekt, in den der Feature-State eingebaut wird. ### Struktur des Feature-States definieren -Nun folgt der erste inhaltliche Schritt auf dem Weg zum State Management: -Wir müssen die Struktur des Feature-States für das Feature `book` definieren. -Dazu befindet sich in der Datei \file{books/store/book.reducer.ts} ein Interface mit dem Namen `State`. -Dieser Feature-State ist der erste Zweig des zentralen Objekt-Baums. +Nun folgt der erste inhaltliche Schritt auf dem Weg zum State Management: Wir müssen die Struktur des Feature-States für das Feature `book` definieren. Dazu befindet sich in der Datei `books/store/book.reducer.ts` ein Interface mit dem Namen `State`. Dieser Feature-State ist der erste Zweig des zentralen Objekt-Baums. -\footnote{State-Interface}Im Interface `State` legen wir fest, welche Zustände und Daten wir speichern möchten. -Es soll zunächst nur darum gehen, die Buchliste vom Server abzurufen, im Store zu speichern und schließlich darzustellen. -Wir benötigen also eine Liste von Büchern und integrieren außerdem einen Ladeindikator. +Im Interface `State` legen wir fest, welche Zustände und Daten wir speichern möchten. Es soll zunächst nur darum gehen, die Buchliste vom Server abzurufen, im Store zu speichern und schließlich darzustellen. Wir benötigen also eine Liste von Büchern und integrieren außerdem einen Ladeindikator. -\footnote{Initialzustand}Direkt darunter befindet sich die Variable `initialState`. -Damit das System weiß, welche Zustände direkt nach dem Start herrschen, müssen wir hier einen initialen Zustand definieren. -Für unsere Anwendung ist die Buchliste beim Start leer und der Ladeindikator steht auf `false`: +Direkt darunter befindet sich die Variable `initialState`. Damit das System weiß, welche Zustände direkt nach dem Start herrschen, müssen wir hier einen initialen Zustand definieren. Für unsere Anwendung ist die Buchliste beim Start leer und der Ladeindikator steht auf `false`: -\lstinputlisting[firstline=8,lastline=16,caption={Feature-State und initialer Zustand \textup{(\file{book.reducer.ts})}}, label=lst:ngrx:featstateinitial, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.reducer.ts} +```ts +// books/store/book.reducer.ts +export interface State { + books: Book[]; + loading: boolean; +} + +export const initialState: State = { + books: [], + loading: false +}; +``` -Damit haben wir die Struktur unseres Feature-States definiert. -Die restlichen Inhalte der Datei ignorieren wir zunächst, darum werden wir uns im übernächsten Schritt kümmern. -Unser *gesamter* State der Anwendung hat jetzt den folgenden Aufbau: +Damit haben wir die Struktur unseres Feature-States definiert. Die restlichen Inhalte der Datei ignorieren wir zunächst, darum werden wir uns im übernächsten Schritt kümmern. Unser *gesamter* State der Anwendung hat jetzt den folgenden Aufbau: -\pagebreakbeforelisting -\lstinputlisting[caption={Aufbau des gesamten State-Objekts}, label=lst:ngrx:statestructure, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/state-structure.ts} +```ts +{ + book: { + books: [], + loading: false + } +} +``` Der Name `book` wird durch den Feature-Key definiert, den wir an den Aufruf von `forFeature()` übergeben haben. ### Actions: Kommunikation mit dem Store +Alle relevanten Ereignisse in der Anwendung werden durch Actions repräsentiert, die in den Store gesendet werden. Das umfasst Aktionen, die direkt in der Oberfläche ausgeführt werden, und auch technische Ereignisse wie Antworten von der HTTP-Schnittstelle. + +Dabei beschreiben Actions idealerweise eine abstrakte Sicht auf das Geschehen. Actions sollten keine technischen Kommandos für das System darstellen, sondern die dahinterliegende Intention beschreiben. Für unser Beispiel raten wir etwa von der Action `Show Loading Spinner` ab, denn sie beschreibt ein technisches Implementierungsdetail und kein fachliches Ereignis. + +Actions bilden die Grundlage für die Kommunikation mit dem Store und können Änderungen am Anwendungszustand auslösen. Dieses Konzept entspricht den Nachrichten, die wir im einführenden Beispiel an den Service gesendet haben. Die folgende Auflistung zeigt einige Beispiele für Actions, die in einer Anwendung vorkommen könnten: + +- Load Books +- Load Books Success +- Load Books Failure +- Session Expired +- Router Navigation +- Window Resize +- User Login +- User Login Success +- User Login Failure +- Add item to cart +- Remove item from cart +- Create book +- Update book +- Set language +- Increment counter + +Technisch ist eine solche Action immer ein Objekt mit einer bestimmten vorgegebenen Struktur. Verpflichtend ist die Eigenschaft `type`, die den Namen der Nachricht angibt und die Nachricht unverwechselbar macht. Jeder Type muss eindeutig sein und er darf immer nur einmal vergeben werden! Zusätzlich können weitere optionale Eigenschaften definiert werden, um Daten in der Action zu transportieren: der sogenannte Payload, der im folgenden Beispiel `data` genannt wird. + +```ts +{ + type: 'Load Books Success', + data: { /* ... */ } +} +``` -Alle relevanten Ereignisse in der Anwendung werden durch Actions repräsentiert, die in den Store gesendet werden. -Das umfasst Aktionen, die direkt in der Oberfläche ausgeführt werden, und auch technische Ereignisse wie Antworten von der HTTP-Schnittstelle. - -\footnote{Actions beschreiben fachliche Ereignisse.}Dabei beschreiben Actions idealerweise eine abstrakte Sicht auf das Geschehen. -Actions sollten keine technischen Kommandos für das System darstellen, sondern die dahinterliegende Intention beschreiben. -Für unser Beispiel raten wir etwa von der Action `Show Loading Spinner` ab, denn sie beschreibt ein technisches Implementierungsdetail und kein fachliches Ereignis. - -Actions bilden die Grundlage für die Kommunikation mit dem \mbox{Store} und können Änderungen am Anwendungszustand auslösen. -Dieses Konzept entspricht den Nachrichten, die wir im einführenden Beispiel an den Service gesendet haben. -Die folgende Auflistung zeigt einige Beispiele für Actions, die in einer Anwendung vorkommen könnten: - -\vskip-4mm\begin{tabular}[t]{@{}p{.4\textwidth}p{.4\textwidth}@{}} - \begin{itemize} - \item Load Books - \item Load Books Success - \item Load Books Failure - \item Session Expired - \item Router Navigation - \item Window Resize - \item User Login - \item User Login Success - \end{itemize} - & - \begin{itemize} - \item User Login Failure - \item Add item to cart - \item Remove item from cart - \item Create book - \item Update book - \item Set language - \item Increment counter - \end{itemize} -\end{tabular} - -\vskip-2mm\noindent -Technisch ist eine solche Action immer ein Objekt mit einer bestimmten vorgegebenen Struktur. -Verpflichtend ist die Eigenschaft `type`, die den Namen der Nachricht angibt und die Nachricht unverwechselbar macht. -Jeder Type muss eindeutig sein und er darf immer nur einmal vergeben werden! -Zusätzlich können weitere optionale Eigenschaften definiert werden, um Daten in der Action zu transportieren: der sogenannte Payload, der im folgenden Beispiel `data` genannt wird. - -\pagebreakbeforelisting -\lstinputlisting[caption={Grundaufbau einer Action}, label=lst:ngrx:actionstructure, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-structure.ts} - -\footnote{Action Creator}Um eine starke Typisierung zu ermöglichen und Tippfehler zu vermeiden, notieren wir die Objekte jedoch nicht direkt im Code. -Stattdessen nutzen wir einen sogenannten *Action Creator* – eine Funktion, die das Objekt mit der richtigen Struktur erzeugt. - -Dafür stellt NgRx die Funktion `createAction()` zur Verfügung. -Als erstes Argument geben wir hier immer den Namen der Action an. -Dieser `type` muss in der gesamten Anwendung eindeutig sein. -Um die Nachvollziehbarkeit zu erhöhen und mögliche Kollisionen zu verhindern, wird üblicherweise die Quelle der Action in eckigen Klammern im Namen notiert. -Damit erzeugen wir eine Art Namespace für den Action-Typ – es handelt sich dabei aber nur um eine Konvention. - -\lstinputlisting[caption={Action ohne Payload}, label=lst:ngrx:actionwithoutpayload, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-withoutpayload.ts} - -Diese Action besitzt noch keinen Payload. -Wollen wir weitere Daten in der Action verpacken, können wir die Struktur des Payloads im zweiten Argument von `createAction()` festlegen: - -\lstinputlisting[caption={Action mit Payload}, label=lst:ngrx:actionwithpayload, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-withpayload.ts} - -Der generische Typparameter der Funktion `props()` gibt an, welche zusätzlichen Eigenschaften die Action enthalten soll. -Wir haben hier den generischen Namen `data` gewählt, Sie können die Payload-Propertys allerdings nach Belieben benennen. -Das erzeugte Action-Objekt hat den folgenden Aufbau: - -\lstinputlisting[caption={Struktur der Action}, label=lst:ngrx:actionstructurelbs, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-structure-lbs.ts} - -Zur Abfrage einer HTTP-Schnittstelle benötigt man normalerweise drei zusammengehörige Actions, die diesem Muster folgen: -\begin{itemize} - \item `Load XXX`: Daten anfragen - \item `Load XXX Success`: Daten sind erfolgreich vom Server eingetroffen. - \item `Load XXX Failure`: Das Laden der Daten ist fehlgeschlagen. -\end{itemize} - -Die Actions werden in einer oder mehreren Dateien gesammelt. -Beim Anlegen des Features mit `ng generate feature` wurde eine solche Datei bereits erstellt: -\file{books/store/book.actions.ts}. -Da wir das Feature mit der Option `-{`-api} angelegt haben, sind in dieser Datei bereits die ersten drei Actions vorbereitet. -Wir können dieses Grundgerüst nutzen und die Signaturen der Actions für unseren Anwendungsfall anpassen. - -Die Success-Action erhält als Payload eine Buchliste `Book[]`, die Failure-Action transportiert einen Fehler vom Typ `string`.\footnote*{In der Praxis ist es sinnvoll, hier ein eigenes Fehlerobjekt zu verwenden, das mehr Informationen beinhaltet als nur den Fehlertext.} -Die Ac\-tion `loadBooks` benötigt keine weiteren Daten, denn die Intention wird schon durch den Action-Typ vollständig ausgedrückt. - -\lstinputlisting[caption={Actions für das Feature `book` \textup{(\file{book.actions.ts})}}, label=lst:ngrx:bookactions, language=js,breakatwhitespace=true,belowskip=0pt]{\currentBm/src/app/books/store/book.actions.ts} - -### Dispatch: Actions in den Store senden - - -Um mit dem Store zu kommunizieren und Zustandsänderungen anzustoßen, müssen die Actions von den Komponenten in den Store gesendet werden. -Der Store kann dazu als Service in die Komponente injiziert werden. - -Der Store verfügt über eine Methode `dispatch()`, mit der wir eine Action in den Store dispatchen können. -Beim Aufruf der `BookList\-Component` soll das Laden der Buchliste angestoßen werden. -Deshalb lösen wir dort gleich im Konstruktor die Action `loadBooks` aus. - -Wichtig ist, dass das exportierte `loadBooks` aus der Datei \file{book"".\mbox{actions}"".ts} selbst noch keine Action ist, sondern ein Action Creator, der ein Action-Objekt erzeugen kann. -Dazu muss die Funktion aufgerufen werden. -Hat die Action einen Payload, so wird er als Argument an den Action Creator übergeben: - -\lstinputlisting[caption={Action Creator verwenden}, label=lst:ngrx:actioncreatoruse, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/action-creator-use.ts} - -Wir müssen die Funktion `loadBooks` also aufrufen, um ein Action-Objekt zu erhalten, das wir dispatchen können: - -\lstinputlisting[caption={Action dispatchen \textup{(\file{book-list"".component.ts})}}, label=lst:ngrx:dispatchloadbooksblc, language=js,breakatwhitespace=true]{\currentBm/src/app/books/book-list/book-list.component.ts.snippet} - -\footnote{Redux DevTools}Wenn Sie bereits die Redux DevTools installiert haben, so können Sie nun überprüfen, ob die ausgelöste Action tatsächlich im Store eingetroffen ist. -Wir betrachten die DevTools separat in \autoref{sec:redux-developer-tools} ab \autopageref{sec:redux-developer-tools}. - -### Reducers: den State aktualisieren - - -Nachdem wir die erste Action in den Store dispatcht haben, ist es nun an der Zeit, einen Reducer zu entwickeln, um den State zu verändern. -Ein Reducer im Kontext von Redux ist eine Funktion mit zwei Eingabewerten: der aktuelle Zustand und die neu eintreffende Action. -Die Aufgabe des Reducers ist es, anhand der Action und des Zustands einen neuen Zustand zu berechnen und zurückzugeben: - -\pagebreakbeforelisting -\lstinputlisting[caption={Signatur eines Reducers}, label=lst:ngrx:reducersignature, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/reducer-signature.ts} - -Ein Reducer ist dabei immer für einen Teilbaum des States zuständig. -Das Feature `book` besitzt einen eigenen Reducer, der ausschließlich diesen State verarbeitet. -\footnote{State-Slice}Ein solcher Feature-State wird auch *Slice* genannt. Wir verwenden die beiden Begriffe synonym. - -In unserem einführenden Beispiel haben wir zur Unterscheidung der Nachrichten ein *switch/case*-Statement verwendet. -Traditionell wird in Redux auch genau dieser Ansatz genutzt. -Der Einsatz von *switch/case* ist jedoch etwas gewöhnungsbedürftig, und auch die Menge an erforderlichem Code ist recht hoch. -Daher stellt NgRx die Funktion `create\-Reducer()` zur Verfügung, um Reducers sehr kompakt und typsicher zu implementieren. -Die grundsätzliche Idee ist jedoch dieselbe: -Der Reducer unterscheidet nach dem Action-Typ und nimmt für verschiedene Actions verschiedene Anpassungen am State vor. -Wir entwickeln also für jede Action eine kleine Reducer-Funktion. - -\footnote{Reducers sind Pure Functions.}Die wichtigste Eigenschaft der Reducer-Funktionen ist ihre Reinheit : Reducers sind Pure Functions. -Dieses Konzept ist von drei wesentlichen Einschränkungen geprägt: -\begin{itemize} - \item **deterministisch:** Die Funktion liefert für gleiche Eingabewerte stets die gleiche Ausgabe. Es dürfen also keine Werte verarbeitet werden, die nicht zweifelsfrei aus den Eingaben ableitbar sind, z.\,B. Zufallswerte oder die Uhrzeit. - \item **keine äußeren Zustände:** Es werden nur die Daten verarbeitet, die als Argumente an die Funktion übergeben werden (also hier: State und Action). Es darf nicht auf andere Variablen zugegriffen werden, die außerhalb der Funktion liegen. Eine Ausnahme bilden ausgelagerte Hilfsfunktionen, die allerdings dieselben Anforderungen an eine Pure Function erfüllen müssen. - \item **keine Seiteneffekte:** Die Funktion darf keine Aktionen ausführen, die einen Effekt außerhalb ihres Gültigkeitsbereichs haben. HTTP-Requests, Logging, Authentifizierung oder Dispatchen von Actions sind also in den Reducers *nicht* erlaubt. Zu Seiteneffekten zählt auch, das State-Objekt direkt zu manipulieren! -\end{itemize} - -Die Einhaltung dieser Einschränkungen ist besonders wichtig, um eine hohe Stabilität des Systems sicherzustellen. -Nur wenn wir den strikten Regeln von Redux folgen, kann der Anwendungszustand zuverlässig kontrolliert werden. - -Ein Reducer darf deshalb ausschließlich den aktuellen State und die eintreffende Action verarbeiten. -Alle notwendigen Informationen, um den neuen State zu erzeugen, müssen in State oder Action vorliegen. -\footnote{Kopie erzeugen}Außerdem muss der Reducer bei Änderungen stets eine *Kopie* des States zurückgeben, die die gewünschten Änderungen beinhaltet. -Es dürfen niemals Änderungen direkt auf dem Objekt ausgeführt werden. -Diese Eigenschaft der Immutability haben wir bereits in der Einleitung besprochen. -Wir setzen mit NgRx in der Regel nicht auf echte Unveränderlichkeit, sondern wenden Disziplin an und behandeln die Objekte lediglich als unveränderlich – auch wenn sie prinzipiell veränderlich sind. -Während der Entwicklung sind außerdem sogenannte *Runtime Checks*\shortlinkFootnote{link-ngrx-runtimechecks} aktiv, die den State auf Unveränderlichkeit und Serialisierbarkeit prüfen. -Jede versehentliche Änderung am State führt dann direkt zu einer Exception. - -Um das State-Objekt vor der Verwendung und Änderung zu klonen, können wir den Spread-Operator einsetzen. -Bitte beachten Sie, dass dieses Werkzeug stets nur eine flache Kopie (Shallow Copy) erzeugt. -Wollen wir Änderungen an tiefer verzweigten Teilen des States vornehmen, müssen wir explizit eine tiefe Kopie (Deep Copy) des Objekts erzeugen.\footnote*{Ein verschachteltes Objekt kann mit dem Spread-Operator geklont werden, indem wir jeden Zweig des Objekts einzeln kopieren. Wird das zu komplex, empfehlen wir Ihnen, ein Hilfsmittel zu verwenden wie die native Funktion `structuredClone()`. Wir haben die verschiedenen Möglichkeiten in einem Blogartikel zusammengefasst: \shortlinkInline{link-schule-immutable}.} - -\footnote{Reducers für die Anwendung}Wir wollen nun auch für den BookMonkey passende Reducers entwickeln. -Dazu überlegen wir zunächst, welche Zustandsänderungen von den Actions ausgelöst werden: -\begin{itemize} - \item `loadBooks`: Ladeindikator auf `true` setzen - \item `loadBooksSuccess`: Buchliste einfügen und Ladeindikator auf `false` setzen - \item `loadBooksFailure`: Ladeindikator auf `false` setzen -\end{itemize} - -Die Implementierung bringen wir in der Datei \file{books/""store/""book"".\mbox{reducers}.ts} unter, in der das Grundgerüst der Reducer-Funktion bereits vorbereitet ist. -Statt *switch/""case* wird hier die Funktion `create\-Reducer()` genutzt. -Für jede Fallunterscheidung existiert ein Block, der in ein~`on()` gekapselt ist. -Als erstes Argument geben wir hier immer die Action an, die behandelt werden soll. -Im zweiten Argument ist die Reducer-Funktion notiert, die schließlich anhand der eingehenden Action den neuen State generiert. -Für die einzelnen Reducer-Funktionen sollten wir den Body in geschweiften Klammern definieren und von dort das State-Objekt direkt returnen. -Außerdem sollte der Rückgabetyp der Funktion immer explizit mit `State` definiert sein.{\tolerance5000\par} - -\pagebreakbeforelisting -\lstinputlisting[firstline=18,lastline=36,caption={Reducers für die Anwendung \textup{(\file{book.reducer.ts})}}, label=lst:ngrx:bookreducer, language=js,breakatwhitespace=true,belowskip=0pt]{\currentBm/src/app/books/store/book.reducer.ts} - -\definition{reducermeherereactions}{Reducers für mehrere Actions}{ -Übrigens wird jeder Reducer immer für jede Action durchlaufen. -Das bedeutet, dass Sie in einem Reducer auch auf mehrere Actions oder sogar auf Actions aus einem anderen Bereich der Anwendung reagieren können. -Dazu können Sie im `on()` mehrere Actions nacheinander als einzelne Argumente angeben. -Es gibt jedoch keine direkte Möglichkeit, in einem Reducer auf einen anderen Slice des States zuzugreifen.\footnotemark}\footnotetext{Um in einem Reducer einen anderen State-Slice zu lesen, müsste zunächst die Art und Weise geändert werden, wie NgRx die verschiedenen Reducers intern zusammenfasst. Die Funktion `combineReducers()` bietet dafür einen Ansatz.} - -Haben Sie die Implementierung abgeschlossen, so können Sie die State-Änderung in den Redux DevTools nachvollziehen. -Für die dispatchte Action `loadBooks` ändert sich das `loading`-Flag im State von \mbox{`false`} auf \mbox{`true`}. - -Die Action `loadBooksSuccess` wird aktuell niemals ausgelöst; das werden wir im übernächsten Schritt mit einem Effect lösen. -Wir haben trotzdem bereits definiert, was in diesem Fall mit dem State passieren soll. -Dasselbe gilt für `loadBooksFailure`. - -Für alle unbekannten Actions liefert der Reducer automatisch den aktuellen State unverändert zurück. -Wenn es also für eine Action keinen passenden Reducer gibt, führt das nicht zu einem Fehler. -Die Bestandteile der Architektur sind so stark entkoppelt, dass sie unabhängig voneinander entwickelt werden können. - -### Selektoren: Daten aus dem State lesen - -Lassen Sie uns kurz zusammenfassen, wie weit wir bisher gekommen sind: -Wir haben Action Creators definiert und die Action `loadBooks` von der `BookListComponent` aus in den Store dispatcht. -Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. - -Um den Kreislauf des Datenflusses zu schließen, wollen wir die Daten aus dem State nun auslesen und in der Komponente darstellen. -Dazu benötigen wir demnächst etwas Theorie und einen Blick hinter die Kulissen: -Der Kernbestandteil des Stores ist ein `BehaviorSubject` in Kombination mit dem Operator `scan()`. -Alle eingehenden Actions werden reduziert ( aufsummiert), indem die Reducer-Funktionen angewendet werden. -Auf diese Weise wird der jeweils aktuelle Zustand erzeugt und über das Observable ausgegeben. - -\footnote{Datenstrom von States}Der Store ist selbst ein Observable, das wir verwenden können, um in den Komponenten auf State-Änderungen zu reagieren. -Der Datenstrom gibt allerdings stets den vollständigen State aus. -Um einzelne Teile daraus zu selektieren, benötigen wir eine Projektion, die sich z.\,B. mit dem Operator `map()` realisieren lässt. -Das folgende Beispiel erstellt ein Observable, das nur das `loading`-Flag liefert: - -\lstinputlisting[caption={State auswählen mit `map()`}, label=lst:ngrx:selectmap, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/select-map.ts} - -\footnote{Datenfluss optimieren}Obwohl diese Herangehensweise theoretisch funktioniert, bringt sie ein konzeptionelles Problem mit sich: -Der Store gibt bei *jeder* Änderung den gesamten State aus. -Das Observable `loading\$` emittiert also auch dann einen neuen Wert, wenn sich das `loading`-Flag gar nicht geändert hat. -Werden die Anwendung und der State komplexer, führt das dazu, dass bei jeder noch so trivialen State-Änderung alle Observables in den Komponenten feuern und in der Folge die Change Detection von Angular getriggert wird. - -Das gilt es zu verhindern! -Wir wollen nur dann einen Wert ausgeben, wenn wirklich eine relevante Änderung an den Daten vorliegt. -Dazu könnten wir den Operator `distinctUntilChanged()` verwenden. -Das Framework NgRx bringt auf seinem `Store` allerdings eine eigene Methode mit, die alle nötigen Funktionalitäten bereits kombiniert: `\mbox{select`()}. - -\lstinputlisting[caption={Die Methode `select()` verwenden}, label=lst:ngrx:selectselect, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/select-select.ts} - -Das resultierende Observable gibt das Ergebnis nur aus, wenn der Wert sich tatsächlich verändert hat, wie die \autoref{fig:redux-selectorsgap} zeigt. - -\begin{fig} - {fig:redux-selectorsgap} - {Selektor mit `store.select()` verwenden} - {Selektor mit `store.select()` verwenden} - {10pt} - \vskip-1pt - \includegraphics[width=\textwidth]{images/statemanagement/selectors-gap} -\end{fig} - -\suppressindent -\footnote{Typisierung}Wenn wir das Beispiel so implementieren, stoßen wir allerdings auf ein weiteres Problem: -Die Struktur des States wird dynamisch verändert, wenn einzelne Feature-Module mithilfe von `forFeature()` einen Teilbaum des States registrieren. -Durch eine statische Typanalyse allein ist es also nicht möglich, die vollständige Struktur des States zu ermitteln: Der TypeScript-Compiler weiß nicht, dass das Property `book` im State existiert. - -\footnote{Komplexe Lesezugriffe}Dazu kommt, dass wir an dieser Stelle lediglich einen einfachen Teil des States selektiert haben. -In der Praxis werden aber die Lesezugriffe auf den State komplexer. -Mitunter wollen wir Daten nicht nur einfach auslesen, sondern Projektionen über verschiedene Teile des States ausführen. -Stellen Sie sich vor, Sie besitzen eine Liste von Autoren und Autorinnen und eine Liste von Büchern – wollen aber nun nur die Bücher ausgeben, die von einer bestimmten Person verfasst wurden. -Dazu ist zusätzliche Logik nötig, die nicht in die Komponenten gehört. -Stattdessen soll diese Logik in separate Funktionen ausgelagert werden, die unabhängig von den Komponenten sind. -Sie können eine solche Funktion mit einer Datenbankabfrage vergleichen: Die Query wird einmal definiert und kann beliebig komplex sein. -Verschiedene Teile der Anwendung können diese Query nutzen und die Daten genau im benötigten Format erhalten. -Die Funktionen zur Abfrage von Daten aus dem Store werden *Selektoren* genannt. - -Selektoren werden ebenfalls in eigenen Dateien untergebracht, die unabhängig von den Komponenten sind. -Dazu wurde bereits die Datei \file{books/store/book.selectors.ts} erstellt. -Zur Definition von Selektoren bringt NgRx eigene Hilfsfunktionen mit: `createFeatureSelector()` und `createSelector()`. -Sie sorgen unter anderem dafür, dass die selektierten Daten korrekt typisiert sind, auch wenn der State zur Laufzeit dynamisch erweitert wird. - -\footnote{Memoization}In einem solchen Selektor versteckt sich außerdem ein wichtiges Konzept, das sich *Memoization* nennt. -Damit aufwendige Projektionen nicht bei jeder State-Änderung neu ausgeführt werden, speichert jeder Selektor seine zuletzt verarbeiteten Eingabewerte. -Haben sich diese Werte nicht geändert, so wird auch die Projektion nicht neu ausgeführt, sondern das zuletzt berechnete Ergebnis wird erneut ausgegeben. -Das Prinzip der Memoization ist in die Selektoren bereits eingebaut, wenn wir die mitgelieferten Funktionen verwenden. -Damit das allerdings funktioniert, müssen wir Selektoren als Pure Functions definieren: Sie dürfen nur die Werte verarbeiten, die sie als Argumente erhalten, und es dürfen keine Seiteneffekte ausgeführt werden. - -\footnote{Feature-Selektor}Ausgehend davon, dass der Store immer den gesamten State liefert, müssen wir zunächst einen bestimmten Feature-State aus dem großen Objekt selektieren. -Dazu nutzen wir die Funktion `create\-Feature\-Selector()`. -Sie erstellt einen Selektor, der ein Feature anhand seines Namens auswählt. -Dabei verwenden wir den Feature-Key, den wir bereits bei `forFeature()` im `BooksModule` genutzt haben. -Damit diese Selektion typsicher funktioniert, müssen wir außerdem das Interface für den Feature-State angeben. -Um den State-Slice für das Feature `book` auszuwählen, benötigen wir also den folgenden Feature-Selektor, der bereits automatisch vorbereitet wurde:{\tolerance1000\par} - -\lstinputlisting[firstline=1,lastline=6,caption={Feature-Selektor erstellen \textup{(\file{book"".selectors.ts})}}, label=lst:ngrx:bookfeatselector, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.selectors.ts} - -Anschließend können wir weitere Selektoren mithilfe der Funktion `create\-Selector()` bauen. -Diese Funktion erhält als Argumente mehrere andere Selektoren. -Das Kernstück des Selektors ist schließlich die Projektionsfunktion, die im letzten Argument übergeben wird. -Sie erhält die Daten von allen zuvor angegebenen Selektoren als Argumente und liefert den daraus berechneten Wert zurück:{\tolerance1000\par} - -\lstinputlisting[caption={Schematischer Aufbau eines Selektors}, label=lst:ngrx:selectorstructure, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/selector-structure.ts} - -Wir können auf diese Weise ein komplexes Gerüst von Selektoren definieren. -Die Selektoren bauen aufeinander auf und kombinieren die Daten aus dem State so, wie sie in den Komponenten benötigt werden. -Jeder einzelne Selektor nutzt die Memoization, um die berechneten Werte zu cachen. -Die Berechnung in der Projektionsfunktion wird nur neu ausgeführt, wenn sich einer der Eingabewerte tatsächlich ändert. -So sind auch komplexe Projektionen in großen Anwendungen ohne Nachteile in der Performance möglich. - -Für den BookMonkey wollen wir zwei einfache Selektoren entwickeln, die uns Zugriff auf das `loading`-Flag und auf die Buchliste geben: - -\lstinputlisting[firstline=8,lastline=16,caption={Selektoren für die Anwendung \textup{(\file{book.selectors.ts})}}, label=lst:ngrx:bookselectors, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.selectors.ts} - -\footnote{Selektoren verwenden}In der `BookListComponent` verwenden wir nun diese beiden Funktionen, um Observables zu erstellen, die uns die benötigten Daten liefern. -Die Methode `store.select()` erhält dazu als Argument einen der eben definierten Selektoren. -Wir legen alle Observables direkt in der Komponentenklasse ab, denn wir wollen die Subscription im Template mit der Async\-Pipe erledigen. -Wenn Sie Ihre Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. -Sie müssen also nur selten in der Komponentenklasse direkt auf ein Observable aus dem Store sub\-scriben. - -Da wir das Observable `books\$` nun aus dem Store beziehen, benötigen wir den `BookStoreService` nicht mehr. -Wir können das Argument des Konstruktors also entfernen. - -\lstinputlisting[caption={Selektoren verwenden \textup{(\file{book-list"".component.ts})}}, label=lst:ngrx:useselectorsblc, language=js,breakatwhitespace=true]{\currentBm/src/app/books/book-list/book-list.component.ts.2.snippet} - -Im Template der Komponente nutzen wir die Observables, um die Daten darzustellen. -Das Observable `books\$` wird bereits korrekt verarbeitet. -Für den Ladeindikator können wir ein neues Element erstellen, das wir direkt mit `ngIf` ein- und ausblenden. -Unser globales Stylesheet bietet dafür die passende \acs{CSS}-Klasse `loader` an. - -\lstinputlisting[caption={Daten anzeigen \textup{(\file{book-list"".component.html})}}, label=lst:ngrx:blchtml, language=js,breakatwhitespace=true]{\currentBm/src/app/books/book-list/book-list.component.html.snippet} - -Wenn Sie die Anwendung nun aufrufen, sehen Sie den Ladeindikator. -Die Komponente ist nun deutlich loser gekoppelt als vorher, denn sie braucht keine eigene spezifische Logik mehr, um den Indikator anzuzeigen oder Bücher zu laden. -Sie bezieht lediglich Daten über Observables aus dem Store und löst Actions aus. - -### Effects: Seiteneffekte ausführen - -Wir haben nun alle Bausteine von Redux betrachtet und den Weg der Daten von der Action bis zum Selektor verfolgt. -Jeder Baustein hat klare Zuständigkeiten, und es gibt strikte Regeln dazu, welche Aufgaben in welchen Teil der Anwendung gehören. - -\footnote{Kommunikation mit der Außenwelt}Was unserer Anwendung allerdings bis hierhin noch fehlt, ist die Kommunikation mit der Außenwelt: -Wird die Action `loadBooks` ausgelöst, so soll die Buchliste per HTTP vom Server geladen werden. -Bei genauer Betrachtung fällt jedoch auf, dass Reducers und Selektoren nicht die richtigen Orte für diese Aufgabe sind: Beide sind durch Pure Functions definiert, die keine Seiteneffekte ausführen dürfen. - -Die grundsätzliche Aufgabe lässt sich wie folgt zusammenfassen: -Wir wollen auf die Action `loadBooks` reagieren und einen HTTP-Request auslösen, um die Buchliste vom Server zu laden. -Ist die HTTP-Antwort eingetroffen, so soll die Buchliste mit einer Action `loadBooksSuccess` in den Store gesendet werden. -Schlägt die HTTP-Kommunikation fehl, so soll die Action `loadBooksFailure` ausgelöst werden. - -Um diese Aufgabe anzugehen, bietet NgRx ein ausgereiftes Modell zur Ausführung von Seiteneffekten: -Mit `@ngrx/effects` können wir asynchrone Ereignisse und andere Seiteneffekte in der Anwendung koordinieren. -In einem Effect ist alles\ erlaubt: HTTP-Kommunikation, Logging, Authentifizierung, LocalStorage, Routing, Anzeige von Meldungen usw. -Effects sind kein Bestandteil der eigentlichen Redux-Architektur, sondern agieren außerhalb des Stores. - -%\vspace{3\baselineskip}\suppressindent -\footnote{Effect: reaktiver Datenstrom von Actions}Mit Effects kommmen wir in den Genuss des vollen Erlebnisses von reaktiver Programmierung. -Ein Effect ist ein reaktiver Datenstrom, der -\begin{itemize} - \item auf Actions und andere Ereignisse reagiert, - \item Seiteneffekte ausführen kann und - \item neue Actions generiert. -\end{itemize} - -Technisch ist ein Effect also immer ein `Observable`. -Alle so erzeugten Actions werden automatisch in den Store dispatcht – darum kümmert sich das Framework. -Effects werden in einer eigenen Klasse untergebracht, die wie ein Service aufgebaut ist: Sie trägt den Decorator `@Injectable()` und kann über ihren Konstruktor Abhängigkeiten anfordern. -Damit die Effects funktionieren, muss die Klasse allerdings explizit registriert werden. -Dazu muss jede Effects-Klasse in einem Modul mithilfe des `EffectsModule` eingebunden werden. -Für das `BooksModule` wurde dieser Schritt bereits bei der Einrichtung erledigt: -\begin{widefigtop} - {fig:redux:flowfull} - {Datenfluss in NgRx mit Effects} - \includegraphics[width=\totalwidth]{images/statemanagement/ngrx-flow-full} -\end{widefigtop} - -\vspace*{4\baselineskip} -\lstinputlisting[caption={Effect in der Anwendung registrieren \textup{(\file{books.module.ts})}}, label=lst:ngrx:booksmoduleeffects, language=js,breakatwhitespace=true]{\currentBm/src/app/books/books.module.ts.2.snippet} - -Die Klasse `BookEffects` befindet sich in der Datei \file{books/""store/""book""\mbox{.effects}.ts}. -Ein Effect wird als ein Property in dieser Klasse definiert. -Der Name des Propertys spielt keine Rolle, sollte aber passend zur Aufgabe benannt werden. -Das Ziel ist es, hier ein Observable zu entwickeln, das Actions ausgibt. -Jeder Effect wird mit der Funktion `create\-Effect()` gekapselt und wird so automatisch in den Lebenszyklus von NgRx integriert. -Ein erster Effect zum Laden von Daten wurde hier bereits automatisch generiert, weil wir beim Anlegen die Einstellung \mbox{`-{`-api}} verwendet haben. -Dieses Grundgerüst wollen wir vervollständigen, um die Buchliste per HTTP zu laden.{\tolerance1000\par} - -Zunächst benötigen wir eine Instanz des `BookStoreService`, der die HTTP-Kommunikation kapselt: - -\pagebreakbeforelisting -\lstinputlisting[caption={Service in den Effect injizieren \textup{(\file{book.effects.ts})}}, label=lst:ngrx:bookeffectsbss, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.ts.snippet} - -\footnote{Datenstrom von Actions}In die Klasse wurde bereits der Service `Actions` injiziert. -Damit erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. -Dieser Datenstrom ist meist die Grundlage für unsere Effects.\footnote*{Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: \shortlinkInline{link-schule-effects}.} - -Der Effect `loadBooks\$` soll auf die Action `loadBooks` reagieren, den passenden HTTP-Request auslösen und Actions zurück in den Store leiten (Success und Failure). -Wir zeigen zunächst den vollständigen Code und gehen die Implementierung anschließend Schritt für Schritt durch: - -\lstinputlisting[caption={Effect zum Laden der Buchliste \textup{(\file{book.effects.ts})}}, label=lst:ngrx:bookeffectsfull, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.ts} - -Der Datenfluss beginnt beim Strom aller Actions aus der gesamten Anwendung: `this.actions\$`. -\footnote{Actions filtern}Wir interessieren uns hier allerdings nur für Actions mit dem bestimmten Typ `loadBooks`. -Um den Datenstrom zu filtern, eignet sich der Operator `filter()`. -Das führt allerdings zu sehr technischem Code, erschwert die korrekte Typisierung und sagt wenig über die tatsächliche Semantik aus. -NgRx bringt deshalb einen eigenen Operator mit, mit dem wir den Datenstrom nach dem Action-Typ filtern können: `ofType()`. -Als Argument übergeben wir hier einen Action Creator. -Anschließend liegt ein Datenstrom vor, der nur \mbox{Actions} vom Typ `loadBooks` ausgibt. - -\footnote{Flattening-Operator auswählen}Für jede dieser Actions wollen wir nun einen HTTP-Request ausführen und die Ergebnisse verarbeiten. -Damit betreten wir erneut das Terrain der Higher-Order Observables. -Wir müssen uns sorgfältig für einen der vier Flattening-Operatoren entscheiden. -Unsere Wahl fällt hier auf `switchMap()`: Wird die Buchliste noch geladen und während\-dessen erneut angefragt, soll nur die zuletzt gesendete Anfrage be\-arbei\-tet werden. -Verwenden Sie bitte nicht immer `switchMap()` in Ihren Effects, sondern wägen Sie sorgfältig ab, welcher Flattening-Operator am besten zu Ihrem jeweiligen Problem passt. - -\footnote{HTTP-Request}Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookStoreService` mit der Methode `getAll()` einsetzen, die ein Observable zurückgibt. -Unser Effect ist nun ein Observable, das ein Array von Büchern liefert. -Damit die Buchliste im Store verarbeitet werden kann, müssen wir sie in eine Action verpacken. -Wir nutzen dazu den Operator `map()` und wandeln damit die Buchliste um in eine Action `loadBooksSuccess`, die die Liste enthält. - -\footnote{Fehlerbehandlung}Ähnlich gehen wir für den Fehlerfall vor: Hier nutzen wir den Operator `catchError()`, um einen Fehler in der Ausführung abzufangen. -Wir werfen den Fehler allerdings an der Stelle nicht weiter, sondern kapseln ihn in eine Action `loadBooksFailure`, die wir als reguläres Element des Datenstroms weitergeben. -Dabei verwenden wir das Property `message` auf dem Fehlerobjekt, um die Meldung auszulesen. - -Auf diese Weise erzeugen wir einen Datenstrom, der nacheinander verschiedene Actions ausgeben kann. -Die Funktion `createEffect()` sorgt dafür, dass dieser Datenstrom automatisch abonniert wird. -Die resultierenden Actions werden in den Store dispatcht und durchlaufen dort die Reducers. -In einem Effect sollten wir also niemals selbst die Methode `store.dispatch()` aufrufen. - -Bei der Fehlerbehandlung ist es wichtig, den Fehler so dicht wie möglich dort abzufangen, wo er entsteht. -Wir setzen `catchError()` deshalb nicht im Hauptdatenstrom ein, sondern hängen `map()` und `catch\-Error()` direkt an den Serviceaufruf an. -Das `switchMap()` verarbeitet also ein Observable, das in jedem Fall eine Action liefert – so wird der Hauptdatenstrom nicht durch Fehler gefährdet. -Wird ein Effect durch einen Fehler im Hauptdatenstrom gestört, so wird das zugrunde liegende Observable beendet, und die Anwendung kann nicht mehr auf eine weitere Nachricht durch den beendeten Effect reagieren. - -Probieren Sie die Anwendung nun aus und beobachten Sie auch die Actions in den Redux DevTools! -Nachdem die Buchliste geladen wurde, wird `loadBooksSuccess` dispatcht, und der Reducer fügt die Buchliste in den State ein. -Die Selektoren in der Komponente reagieren darauf, und die Bücher werden dargestellt. - -\footnote{Loop of Death}Bitte beachten Sie, dass Sie mit einem ungünstig programmierten Effect schnell eine Endlosschleife erzeugen können, die den Browser zum Glühen\ bringt, wenn parallel `ng serve` läuft und die Anwendung sofort aktualisiert. -Der folgende Effect empfängt alle Actions und leitet sie direkt in den Store zurück – es entsteht eine Endlosschleife: - -\lstinputlisting[caption={\mbox{Effect mit Endlosschleife}}, label=lst:ngrx:effectloopofdeath, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/effect-loopofdeath.ts} +Um eine starke Typisierung zu ermöglichen und Tippfehler zu vermeiden, notieren wir die Objekte jedoch nicht direkt im Code. Stattdessen nutzen wir einen sogenannten *Action Creator* – eine Funktion, die das Objekt mit der richtigen Struktur erzeugt. -Wir können übrigens einen Effect so konfigurieren, dass er die ausgegebenen Elemente des Datenstroms nicht als Actions dispatcht. -Das ist immer dann nützlich, wenn ein Effect lediglich Seiten\-effekte ausführt, aber keine weiteren Aktionen zur Folge hat. -Denken Sie z.\,B. daran, dass wir nach dem erfolgreichen Anlegen eines Buchs zur Detailseite navigieren möchten – nach diesem Schritt sollen keine weiteren Actions getriggert werden. -Dazu können wir im zweiten Argument von `create\-Effect()` die Einstellung `\{ dispatch: false \`} setzen. +Dafür stellt NgRx die Funktion `createAction()` zur Verfügung. Als erstes Argument geben wir hier immer den Namen der Action an. Dieser `type` muss in der gesamten Anwendung eindeutig sein. Um die Nachvollziehbarkeit zu erhöhen und mögliche Kollisionen zu verhindern, wird üblicherweise die Quelle der Action in eckigen Klammern im Namen notiert. Damit erzeugen wir eine Art Namespace für den Action-Typ – es handelt sich dabei aber nur um eine Konvention. -\lstinputlisting[caption={Dispatchen von Actions deaktivieren}, label=lst:ngrx:effectdispatchfalse, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/effect-dispatchfalse.ts} +```ts +import { createAction } from '@ngrx/store'; -\noindent -Ein Tipp aus der Praxis: Nutzen Sie diese Einstellung während der Entwicklung, um einen *Loop of Death* zu verhindern. -Sie können einen komplexen Effect dann in Ruhe debuggen, ohne dass die Actions in den Store geleitet werden. -Erst wenn die Implementierung fertig ist, aktivieren Sie den Effect, indem Sie das `\{ dispatch: false \`} wieder entfernen. +export const loadBooks = createAction( + '[Book] Load Books' +); +``` -\subsubsection*{Achtung: Proprietäres Verhalten von TypeScript} +Diese Action besitzt noch keinen Payload. Wollen wir weitere Daten in der Action verpacken, können wir die Struktur des Payloads im zweiten Argument von `createAction()` festlegen: -Bei allen gezeigten Effects haben wir die Syntax mit dem alten proprietären Verhalten von TypeScript (`useDefineForClassFields: false`) verwendet. -Zum Zeitpunkt der Fertigstellung dieses Buchs wurde diese Schreibweise in der offiziellen Dokumentation, in der Demo-Anwen\-dung und in den Schematics von NgRx empfohlen. -Wir haben uns daher dazu entschlossen, möglichst wenig von den offiziellen Quellen abzuweichen, auch wenn es womöglich bei geänderten Standardeinstellungen zu Fehlern kommen kann. -In \autoref{sub:ww:useDefineForClassFields} ab \autopageref{sub:ww:useDefineForClassFields} haben wir beschrieben, unter welchen Umständen dieser Fehler entsteht. +```ts +import { createAction, props } from '@ngrx/store'; -Sollten die Beispiele in Zukunft nicht mehr lauffähig sein, so können Sie den Code leicht umstellen. -Das folgende Listing zeigt eine Variante, bei der wir die Klasse `Actions` und den `BookStoreService` mithilfe der Funktion `inject()` anfordern. +export const loadBooksSuccess = createAction( + '[Book] Load Books Success', + props<{ data: Book[] }>() +); +``` -\lstinputlisting[caption={Effect zum Laden der Buchliste mit `inject()` \textup{(\file{book.effects.ts})}}, label=lst:ngrx:bookeffectsfull-inject, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.inject.snippet} +Der generische Typparameter der Funktion `props()` gibt an, welche zusätzlichen Eigenschaften die Action enthalten soll. Wir haben hier den generischen Namen `data` gewählt, Sie können die Payload-Propertys allerdings nach Belieben benennen. Das erzeugte Action-Objekt hat den folgenden Aufbau: -### Geschafft! +```ts +{ + type: '[Book] Load Books Success'; + data: Book[]; +} +``` -Sobald der Effect aktiv ist, wird unsere Anwendung die Bücher über HTTP laden und die Liste mittels `loadBooksSuccess` an den Reducer übergeben. -Dieser wird einen neuen Zustand mit den geladenen Büchern berechnen. -Abschließend wird mithilfe eines Selektors die Oberfläche aktualisiert, und die neuen Bücher werden angezeigt. +Zur Abfrage einer HTTP-Schnittstelle benötigt man normalerweise drei zusammengehörige Actions, die diesem Muster folgen: -Und damit haben Sie die Rundreise durch alle Bausteine von Redux und NgRx geschafft. -Mithilfe eines zentralen Stores haben wir die Zustände der Anwendung zentralisiert und so die Buchliste vom Server abgerufen. -Die Komponenten beinhalten mit dieser Architektur nur noch wenig Logik – alles Nötige passiert im Store. +- `Load XXX`: Daten anfragen +- `Load XXX Success`: Daten sind erfolgreich vom Server eingetroffen. +- `Load XXX Failure`: Das Laden der Daten ist fehlgeschlagen. -Vielleicht haben Sie nun das Gefühl, dass wir für eine kleine Aufgabe unnötig viel Code erzeugt haben. -Betrachtet man das vorliegende Beispiel isoliert, so ist diese Empfindung richtig: Für kleine Anwendungen ist der Einsatz von Redux und NgRx nicht zwingend sinnvoll. -Steigt allerdings die Komplexität des Projekts, so kann ein zentrales State Management mit klaren Regeln und einer durchdachten Architektur eine sinnvolle Investition sein. -Prüfen Sie also für ein Projekt sorgfältig, ob sich der Einsatz von Redux wirklich lohnt. +Die Actions werden in einer oder mehreren Dateien gesammelt. Beim Anlegen des Features mit `ng generate feature` wurde eine solche Datei bereits erstellt: `books/store/book.actions.ts`. Da wir das Feature mit der Option `--api` angelegt haben, sind in dieser Datei bereits die ersten drei Actions vorbereitet. Wir können dieses Grundgerüst nutzen und die Signaturen der Actions für unseren Anwendungsfall anpassen. +Die Success-Action erhält als Payload eine Buchliste `Book[]`, die Failure-Action transportiert einen Fehler vom Typ `string`. (In der Praxis ist es sinnvoll, hier ein eigenes Fehlerobjekt zu verwenden, das mehr Informationen beinhaltet als nur den Fehlertext.) Die Action `loadBooks` benötigt keine weiteren Daten, denn die Intention wird schon durch den Action-Typ vollständig ausgedrückt. -## Debugging mit den Redux DevTools +```ts +// books/store/book.actions.ts +import { createAction, props } from '@ngrx/store'; +import { Book } from '../../shared/book'; +export const loadBooks = createAction( + '[Book] Load Books' +); -Wir möchten Ihnen zum Abschluss des Praxisteils ein wichtiges Hilfsmittel zeigen, das beim Debugging von Anwendungen mit NgRx hilfreich ist: die *Redux DevTools*. +export const loadBooksSuccess = createAction( + '[Book] Load Books Success', + props<{ data: Book[] }>() +); -Ein besonderer Vorteil der Redux-Architektur ist das sogenannte *Time Travel Debugging*. -Da stets durch eintreffende Actions ein neuer State im Store erzeugt wird, ist es möglich, die Historie der Ereignisse und Zustände nachzuvollziehen. -Die Redux DevTools unterstützen uns dabei mit einer grafischen Oberfläche, über die wir alle Actions und Zustandsänderungen kontrollieren und debuggen können. +export const loadBooksFailure = createAction( + '[Book] Load Books Failure', + props<{ error: string }>() +); +``` -### Die DevTools installieren -Die Redux DevTools sind eine Erweiterung für Google Chrome. -\footnote{Erweiterung für Google~Chrome}Um sie zu verwenden, besuchen wir den Webstore und installieren die Extension im Browser.\shortlinkFootnote{link-redux-devtools} -\begin{widefig} - {fig:dev-tools:redux} - {Redux DevTools installieren} - % {Die Redux DevTools installieren} - % {10pt} - \includegraphics[width=\totalwidth]{images/statemanagement/webstore-devtools.png} -\end{widefig} - -### Die DevTools in der Anwendung registrieren - -Um Informationen aus unserem Store mit dem Entwicklungswerkzeug zu teilen, müssen wir in der Anwendung die passende Schnittstelle schaffen. -Dazu bringt NgRx das Modul `@ngrx/store-devtools` mit, das diese Schnittstelle zur Kommunikation implementiert. +### Dispatch: Actions in den Store senden -Der einfachste Weg zur Einrichtung ist es, das Modul mithilfe von `ng~add` in das Projekt einzufügen. -Als wir im Praxisteil auf \autopageref{lst:ngrx:storeeinrichten} die Pakete für NgRx installiert haben, haben wir diesen Schritt bereits erledigt. +Um mit dem Store zu kommunizieren und Zustandsänderungen anzustoßen, müssen die Actions von den Komponenten in den Store gesendet werden. Der Store kann dazu als Service in die Komponente injiziert werden. -\begin{clilisting}[caption={NgRx DevTools installieren}] -ng add @ngrx/store-devtools -\end{clilisting} +Der Store verfügt über eine Methode `dispatch()`, mit der wir eine Action in den Store dispatchen können. Beim Aufruf der `BookListComponent` soll das Laden der Buchliste angestoßen werden. Deshalb lösen wir dort gleich im Konstruktor die Action `loadBooks` aus. -Damit werden die benötigten Abhängigkeiten installiert, und das zugehörige `StoreDevtoolsModule` wird in das `AppModule` der Anwendung eingebunden. +Wichtig ist, dass das exportierte `loadBooks` aus der Datei `book.actions.ts` selbst noch keine Action ist, sondern ein Action Creator, der ein Action-Objekt erzeugen kann. Dazu muss die Funktion aufgerufen werden. Hat die Action einen Payload, so wird er als Argument an den Action Creator übergeben: -### Die DevTools nutzen +```ts +const myAction = loadBooks(); +const mySuccessAction = loadBooksSuccess({ data: [/* ... */] }); +``` -Die *Redux DevTools* integrieren sich in die Chrome DevTools, und wir finden nach der Installation hier einen neuen Reiter *Redux*. +Wir müssen die Funktion `loadBooks` also aufrufen, um ein Action-Objekt zu erhalten, das wir dispatchen können: -\begin{widefig} - {fig:dev-tools:redux:actions} - {Actions in den DevTools anzeigen} - \includegraphics[width=\totalwidth]{images/statemanagement/developer-tool-action.png} -\end{widefig} - -\footnote{Historie aller Actions}In der \autoref{fig:dev-tools:redux:actions} sind fünf Actions in der Historie auf der linken Seite zu sehen. -Die ersten drei Einträge mit dem Präfix `@ngrx` stammen vom System und werden automatisch bei der Initialisierung dispatcht. -Danach folgen zwei Aktionen, die wir selbst in der Anwendung ausgelöst haben. -Anhand dieser Liste kann genau nachvollzogen werden, was bisher geschehen ist: -Es wurde die Aufforderung zum Abrufen der Buchliste in den Store geschickt, und die Liste kam schließlich vom Server zurück. - -\suppressindent -Wählen wir eine Action aus, werden auf der rechten Seite die Änderungen zum vorhergehenden Zustand visualisiert. -Nachdem die Buchliste geladen wurde, wurde das `loading`-Flag von `true` auf `false` geändert und die Buchliste wurde in das Property `books` eingefügt. -Mit den Reitern *Action* und *State* können wir das Action-Objekt und den State betrachten, wie er nach der Verarbeitung dieser Action aussah. - -\footnote{Time Travel Debugging}Mit der Historie der Actions können wir außerdem das Time \mbox{Travel} Debugging durchführen. -Dazu besitzt jeder Eintrag in der Liste zwei Buttons: *Jump* und *Skip*. -Damit können wir einzelne Actions überspringen oder den Zustand zu einem bestimmten Zeitpunkt rekonstruieren. -Die Anwendung reagiert live auf die Zustandsänderungen, sodass wir das Ergebnis direkt im Browser betrachten können. -Am unteren Rand befindet sich außerdem ein Slider, mit dem wir schrittweise durch die Zustände der Anwendung navigieren können. - -In \autoref{fig:dev-tools:skip-acktion} wurde die Action `[Book] Load Books Success` mit der Schaltfläche *Skip* übersprungen. -Nun ist links in der Anwendung der Ladeindikator zu sehen. -\begin{widefig} - {fig:dev-tools:skip-acktion} - {Time Travel Debugging} - \includegraphics[width=\totalwidth]{images/statemanagement/developer-tool-skip-action.png} -\end{widefig} - -\suppressindent -\footnote{States exportieren}Weitere interessante Features verstecken sich hinter den Buttons am unteren Rand: -Damit können wir unter anderem alle Actions und Zustände als JSON-Datei exportieren und später wieder importieren. -Ein solcher Export kann z.\,B. an ein anderes Teammitglied übermittelt werden, das damit den gleichen Anwendungsstatus erzeugen kann wie wir. - -\vspace{\baselineskip}\noindent -Mit diesem Debugging-Werkzeug haben wir also an einem Ort einen zentralen Überblick über alle Ereignisse der Anwendung. -Wir können den Zustand des Systems überwachen und so auch Fehler bei der Entwicklung einfacher aufspüren. +```ts +// books/book-list/book-list.component.ts +import { Store } from '@ngrx/store'; +import { loadBooks } from '../store/book.actions'; + +@Component({ /* ... */ }) +export class BookListComponent { + // ... + constructor( + private service: BookStoreService, + private store: Store + ) { + this.store.dispatch(loadBooks()); + } +} +``` +Wenn Sie bereits die Redux DevTools installiert haben, so können Sie nun überprüfen, ob die ausgelöste Action tatsächlich im Store eingetroffen ist. Wir betrachten die DevTools separat im Abschnitt zu den Redux DevTools weiter unten. -## Redux und NgRx: Wie geht's weiter? +### Reducers: den State aktualisieren -Wir haben in diesem Kapitel die Grundlagen der Redux-Architektur und des Frameworks NgRx kennengelernt. -Tatsächlich geht die Reise aber noch viel weiter! -Wir möchten Ihnen in diesem Abschnitt einen Ausblick geben, welche Tools und Konzepte Ihnen dabei noch begegnen werden. +Nachdem wir die erste Action in den Store dispatcht haben, ist es nun an der Zeit, einen Reducer zu entwickeln, um den State zu verändern. Ein Reducer im Kontext von Redux ist eine Funktion mit zwei Eingabewerten: der aktuelle Zustand und die neu eintreffende Action. Die Aufgabe des Reducers ist es, anhand der Action und des Zustands einen neuen Zustand zu berechnen und zurückzugeben: +```ts +function reducer(state: State, action: Action): State {} +``` -### Actions gruppieren mit `createActionGroup()` +Ein Reducer ist dabei immer für einen Teilbaum des States zuständig. Das Feature `book` besitzt einen eigenen Reducer, der ausschließlich diesen State verarbeitet. Ein solcher Feature-State wird auch *Slice* genannt. Wir verwenden die beiden Begriffe synonym. +In unserem einführenden Beispiel haben wir zur Unterscheidung der Nachrichten ein *switch/case*-Statement verwendet. Traditionell wird in Redux auch genau dieser Ansatz genutzt. Der Einsatz von *switch/case* ist jedoch etwas gewöhnungsbedürftig, und auch die Menge an erforderlichem Code ist recht hoch. Daher stellt NgRx die Funktion `createReducer()` zur Verfügung, um Reducers sehr kompakt und typsicher zu implementieren. Die grundsätzliche Idee ist jedoch dieselbe: Der Reducer unterscheidet nach dem Action-Typ und nimmt für verschiedene Actions verschiedene Anpassungen am State vor. Wir entwickeln also für jede Action eine kleine Reducer-Funktion. -Wir haben die Funktion `createAction()` kennengelernt, um einzelne Actions zu erstellen. -Zum Laden der Buchliste haben wir auf diese Weise drei einzelne Actions definiert, siehe \autoref{lst:ngrx:bookactions} auf \autopageref{lst:ngrx:bookactions}. -Dafür war viel repetitiver Code notwendig: Wir mussten den Variablennamen definieren, das Präfix `[Book]` setzen und den Type der Action als String angeben. +Die wichtigste Eigenschaft der Reducer-Funktionen ist ihre "Reinheit": Reducers sind Pure Functions. Dieses Konzept ist von drei wesentlichen Einschränkungen geprägt: -Um die Erzeugung der Actions zu vereinfachen, wurde mit NgRx~13.2 die Funktion `createActionGroup()` eingeführt. -Damit können wir eine Gruppe von Actions mit einem gemeinsamen Präfix anlegen. -Die drei Actions aus dem vorherigen Beispiel lassen sich also wie folgt definieren:{\tolerance2000\par} +- **deterministisch:** Die Funktion liefert für gleiche Eingabewerte stets die gleiche Ausgabe. Es dürfen also keine Werte verarbeitet werden, die nicht zweifelsfrei aus den Eingaben ableitbar sind, z. B. Zufallswerte oder die Uhrzeit. +- **keine äußeren Zustände:** Es werden nur die Daten verarbeitet, die als Argumente an die Funktion übergeben werden (also hier: State und Action). Es darf nicht auf andere Variablen zugegriffen werden, die außerhalb der Funktion liegen. Eine Ausnahme bilden ausgelagerte Hilfsfunktionen, die allerdings dieselben Anforderungen an eine Pure Function erfüllen müssen. +- **keine Seiteneffekte:** Die Funktion darf keine Aktionen ausführen, die einen Effekt außerhalb ihres Gültigkeitsbereichs haben. HTTP-Requests, Logging, Authentifizierung oder Dispatchen von Actions sind also in den Reducers *nicht* erlaubt. Zu Seiteneffekten zählt auch, das State-Objekt direkt zu manipulieren! -\lstinputlisting[caption={Actions als Gruppe definieren \textup{(\file{book.actions.ts})}}, label=lst:ngrx:bookactionsgroup, language=js,breakatwhitespace=true,belowskip=0pt]{\snippets/reduxngrx/ngrx/action-group.ts} +Die Einhaltung dieser Einschränkungen ist besonders wichtig, um eine hohe Stabilität des Systems sicherzustellen. Nur wenn wir den strikten Regeln von Redux folgen, kann der Anwendungszustand zuverlässig kontrolliert werden. -Die Besonderheit an diesem Ansatz ist, dass wir die Variablennamen nicht mehr von Hand notieren müssen. -Wir können die Actions als z.\,B. `BookActions.loadBooks()` verwenden – der Name des Propertys wird automatisch aus dem Action Type ermittelt. -Auf diese Weise sparen wir uns viel Tipparbeit. +Ein Reducer darf deshalb ausschließlich den aktuellen State und die eintreffende Action verarbeiten. Alle notwendigen Informationen, um den neuen State zu erzeugen, müssen in State oder Action vorliegen. Außerdem muss der Reducer bei Änderungen stets eine *Kopie* des States zurückgeben, die die gewünschten Änderungen beinhaltet. Es dürfen niemals Änderungen direkt auf dem Objekt ausgeführt werden. Diese Eigenschaft der Immutability haben wir bereits in der Einleitung besprochen. Wir setzen mit NgRx in der Regel nicht auf "echte Unveränderlichkeit", sondern wenden Disziplin an und behandeln die Objekte lediglich als unveränderlich – auch wenn sie prinzipiell veränderlich sind. Während der Entwicklung sind außerdem sogenannte [Runtime Checks](https://ngrx.io/guide/store/configuration/runtime-checks) aktiv, die den State auf Unveränderlichkeit und Serialisierbarkeit prüfen. Jede versehentliche Änderung am State führt dann direkt zu einer Exception. -Bitte beachten Sie: Um die Actions zu verwenden, müssen wir nun immer das gesamte Objekt `BookActions` importieren, nicht die einzelnen Actions. -In den Komponenten, Reducers und Effects sind also auch Anpassungen nötig, um diesen Ansatz zu nutzen. +Um das State-Objekt vor der Verwendung und Änderung zu klonen, können wir den Spread-Operator einsetzen. Bitte beachten Sie, dass dieses Werkzeug stets nur eine flache Kopie (Shallow Copy) erzeugt. Wollen wir Änderungen an tiefer verzweigten Teilen des States vornehmen, müssen wir explizit eine tiefe Kopie (Deep Copy) des Objekts erzeugen. +> **Hinweis:** Ein verschachteltes Objekt kann mit dem Spread-Operator geklont werden, indem wir jeden Zweig des Objekts einzeln kopieren. Wird das zu komplex, empfehlen wir Ihnen, ein Hilfsmittel zu verwenden wie die native Funktion `structuredClone()`. Wir haben die verschiedenen Möglichkeiten in einem Blogartikel zusammengefasst: [10 useful operations for pure & immutable data structures](https://angular.schule/blog/2018-03-pure-immutable-operations). -### Routing +Wir wollen nun auch für den BookMonkey passende Reducers entwickeln. Dazu überlegen wir zunächst, welche Zustandsänderungen von den Actions ausgelöst werden: +- `loadBooks`: Ladeindikator auf `true` setzen +- `loadBooksSuccess`: Buchliste einfügen und Ladeindikator auf `false` setzen +- `loadBooksFailure`: Ladeindikator auf `false` setzen +Die Implementierung bringen wir in der Datei `books/store/book.reducer.ts` unter, in der das Grundgerüst der Reducer-Funktion bereits vorbereitet ist. Statt *switch/case* wird hier die Funktion `createReducer()` genutzt. Für jede Fallunterscheidung existiert ein Block, der in ein `on()` gekapselt ist. Als erstes Argument geben wir hier immer die Action an, die behandelt werden soll. Im zweiten Argument ist die Reducer-Funktion notiert, die schließlich anhand der eingehenden Action den neuen State generiert. Für die einzelnen Reducer-Funktionen sollten wir den Body in geschweiften Klammern definieren und von dort das State-Objekt direkt returnen. Außerdem sollte der Rückgabetyp der Funktion immer explizit mit `State` definiert sein. +```ts +// books/store/book.reducer.ts +export const reducer = createReducer( + initialState, -Der Anwendungszustand setzt sich nicht nur aus geladenen Daten und Einstellungen zusammen – die aktuell geladene Route und ihre Parameter gehören ebenfalls in den State. -Beachtet man dies nicht, so sieht man gegebenenfalls zum richtigen State die falsche Komponente auf der Oberfläche. -Deshalb sollten Informationen zum Routing auch im NgRx-Store abgelegt werden. -Den passenden Adapter zwischen Router und Store erhalten wir mit dem Modul `@ngrx/router-store`: -\begin{clilisting}[caption={Router-Store installieren}] -ng add @ngrx/router-store -\end{clilisting} + on(BookActions.loadBooks, (state): State => { + return { ...state, loading: true }; + }), -Das Modul verfügt über eigene Actions und Reducers und kommuniziert direkt mit dem Router. -Dadurch werden alle Aktivitäten des Routers mit Actions abgebildet. -Es werden unter anderem die folgenden Actions ausgelöst: + on(BookActions.loadBooksSuccess, (state, action): State => { + return { + ...state, + books: action.data, + loading: false + }; + }), -\begin{itemize} - \item `routerRequestAction` - \item `routerNavigationAction` - \item `routerNavigatedAction` -\end{itemize} + on(BookActions.loadBooksFailure, (state, action): State => { + return { ...state, loading: false }; + }), +); +``` -Die passenden Reducers fügen anschließend die Routeninformationen in den State ein, sodass wir z.\,B. die Routenparameter in den Komponenten, Reducers und Effects auslesen können. +> **Reducers für mehrere Actions:** Übrigens wird jeder Reducer immer für jede Action durchlaufen. Das bedeutet, dass Sie in einem Reducer auch auf mehrere Actions oder sogar auf Actions aus einem anderen Bereich der Anwendung reagieren können. Dazu können Sie im `on()` mehrere Actions nacheinander als einzelne Argumente angeben. Es gibt jedoch keine direkte Möglichkeit, in einem Reducer auf einen anderen Slice des States zuzugreifen. (Um in einem Reducer einen anderen State-Slice zu lesen, müsste zunächst die Art und Weise geändert werden, wie NgRx die verschiedenen Reducers intern zusammenfasst. Die Funktion `combineReducers()` bietet dafür einen Ansatz.) -### Entity Management +Haben Sie die Implementierung abgeschlossen, so können Sie die State-Änderung in den Redux DevTools nachvollziehen. Für die dispatchte Action `loadBooks` ändert sich das `loading`-Flag im State von `false` auf `true`. +Die Action `loadBooksSuccess` wird aktuell niemals ausgelöst; das werden wir im übernächsten Schritt mit einem Effect lösen. Wir haben trotzdem bereits definiert, was in diesem Fall mit dem State passieren soll. Dasselbe gilt für `loadBooksFailure`. +Für alle unbekannten Actions liefert der Reducer automatisch den aktuellen State unverändert zurück. Wenn es also für eine Action keinen passenden Reducer gibt, führt das nicht zu einem Fehler. Die Bestandteile der Architektur sind so stark entkoppelt, dass sie unabhängig voneinander entwickelt werden können. -Verwalten wir Entitäten in unserem State (z.\,B. Bücher), so müssen wir in den Reducers für jede Entität wiederkehrende Routinen implementieren: -Wir müssen eine Liste in den State schreiben und die Liste pflegen, indem wir Objekte einfügen, aktualisieren oder löschen. -Bei der Suche nach einem bestimmten Objekt müssen wir stets durch die Liste laufen und dabei z.\,B. `Array.find()` verwenden. +### Selektoren: Daten aus dem State lesen -Diese wiederkehrenden Aufgaben sind für fast alle Entitäten notwendig und verleiten zu redundantem Code. -Um diese Zugriffe auf den State zu vereinfachen, existiert das Paket `@ngrx/entity`: -\begin{clilisting}[caption={Entity-Verwaltung installieren}] -ng add @ngrx/entity -\end{clilisting} +Lassen Sie uns kurz zusammenfassen, wie weit wir bisher gekommen sind: Wir haben Action Creators definiert und die Action `loadBooks` von der `BookListComponent` aus in den Store dispatcht. Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. -Anstatt die Entitäten selbst im State zu deklarieren, können wir die vorgegebene Struktur des Entity-Adapters nutzen. -Jedes State-Interface, in dem Entitäten verwaltet werden, muss dazu das Interface `Entity\-State` erweitern: +Um den Kreislauf des Datenflusses zu schließen, wollen wir die Daten aus dem State nun auslesen und in der Komponente darstellen. Dazu benötigen wir demnächst etwas Theorie und einen Blick hinter die Kulissen: Der Kernbestandteil des Stores ist ein `BehaviorSubject` in Kombination mit dem Operator `scan()`. Alle eingehenden Actions werden reduziert ("aufsummiert"), indem die Reducer-Funktionen angewendet werden. Auf diese Weise wird der jeweils aktuelle Zustand erzeugt und über das Observable ausgegeben. -\lstinputlisting[caption={`EntityState` implementieren}, label=lst:ngrx:entitystate, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/state.ts} +Der Store ist selbst ein Observable, das wir verwenden können, um in den Komponenten auf State-Änderungen zu reagieren. Der Datenstrom gibt allerdings stets den vollständigen State aus. Um einzelne Teile daraus zu selektieren, benötigen wir eine Projektion, die sich z. B. mit dem Operator `map()` realisieren lässt. Das folgende Beispiel erstellt ein Observable, das nur das `loading`-Flag liefert: -Der `EntityState` gibt eine Datenstruktur vor, in der die Entitäten in einem Objekt abgelegt werden; als Schlüssel werden die IDs der Entitäten verwendet. -Ein Objekt vereinfacht die Suche nach einer Entität mit einer bestimmten~ID. -Die geordnete Reihenfolge einer Liste geht allerdings bei einem Objekt verloren. -Deshalb werden zusätzlich die~IDs in einem Array im State abgelegt, um daraus die Reihenfolge jederzeit ablesen zu können. +```ts +@Component({ /* ... */ }) +export class MyComponent { + loading$: Observable; -Durch den `EntityState` wird die folgende Struktur vorgegeben. -Der Typ `Dictionary` stammt von NgRx und beschreibt das Objekt, in dem die Entitäten gespeichert sind. + constructor(private store: Store) { + this.loading$ = this.store.pipe( + map(state => state.book.loading) + ); + } +} +``` -\lstinputlisting[caption={Struktur des `EntityState`}, label=lst:ngrx:entitystatestructure, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/state-structure.ts} +Obwohl diese Herangehensweise theoretisch funktioniert, bringt sie ein konzeptionelles Problem mit sich: Der Store gibt bei *jeder* Änderung den gesamten State aus. Das Observable `loading$` emittiert also auch dann einen neuen Wert, wenn sich das `loading`-Flag gar nicht geändert hat. Werden die Anwendung und der State komplexer, führt das dazu, dass bei jeder noch so trivialen State-Änderung alle Observables in den Komponenten feuern und in der Folge die Change Detection von Angular getriggert wird. -Damit wir die Daten in dieser Struktur nicht eigenhändig pflegen müssen, stellt das Modul einen Adapter zur Verfügung, den wir zusätzlich initialisieren müssen. -\footnote{Primärschlüssel festlegen}Der Adapter nutzt automatisch das Property~`id` als Primärschlüssel der Entität. -Ist die ID allerdings in einem anderen Property enthalten, z.\,B. bei einem Buch die ISBN, so müssen wir eine Funktion `selectId` angeben, die die richtige ID auswählt. +Das gilt es zu verhindern! Wir wollen nur dann einen Wert ausgeben, wenn wirklich eine relevante Änderung an den Daten vorliegt. Dazu könnten wir den Operator `distinctUntilChanged()` verwenden. Das Framework NgRx bringt auf seinem `Store` allerdings eine eigene Methode mit, die alle nötigen Funktionalitäten bereits kombiniert: `select()`. -\lstinputlisting[caption={`EntityAdapter` erzeugen}, label=lst:ngrx:entitycreateadapter, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/create-adapter.ts} +```ts +@Component({ /* ... */ }) +export class MyComponent { + loading$: Observable; -Mithilfe des Adapters können wir zunächst den initialen Zustand erzeugen. -Beinhaltet unser State noch weitere Eigenschaften, so geben wir als Argument ein Objekt an: + constructor(private store: Store) { + this.loading$ = this.store + .select(state => state.book.loading); + } +} +``` -\lstinputlisting[caption={Initialzustand erzeugen}, label=lst:ngrx:entitygetinitialstate, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/get-initialstate.ts} +Das resultierende Observable gibt das Ergebnis nur aus, wenn der Wert sich tatsächlich verändert hat. -Die Mächtigkeit von `@ngrx/entity` entfaltet sich schließlich, wenn es um die Implementierung der Reducers geht. -Dazu stellt der Adapter eine Reihe von Methoden zur Verfügung, mit denen wir die Kollektion von Entitäten im State verwalten können: -\begin{flushleft} - \tabcolsep4pt - \vskip-4mm - \begin{longtable}{@{}lp{89mm}@{}} - `addOne` & eine Entität hinzufügen \\ - `addMany` & mehrere Entitäten hinzufügen \\ - `setAll` & Kollektion vollständig ersetzen \\ - `setOne` & eine Entität hinzufügen oder ersetzen \\ - `setMany` & mehrere Entitäten hinzufügen oder ersetzen \\ - `removeOne` & eine Entität aus der Kollektion entfernen \\ - `removeMany` & mehrere Entitäten aus der Kollektion entfernen\newline (nach ID oder Prädikatsfunktion) \\ - `removeAll` & Kollektion leeren \\ - `updateOne` & eine Entität aktualisieren \\ - `updateMany` & mehrere Entitäten aktualisieren \\ - `upsertOne` & eine Entität hinzufügen oder aktualisieren\newline (mit partiellen Änderungen) \\ - `upsertMany` & mehrere Entitäten hinzufügen oder aktualisieren \\ - `map` & Kollektion aktualisieren mithilfe einer Projektionsfunktion - \end{longtable} -\vskip-9mm -\end{flushleft} + -Alle Funktionen erhalten den aktuellen State und die zu verarbeitenden Werte als Argument und erzeugen einen neuen State. -Wir können den so erzeugten State also direkt aus dem Reducer zurückgeben oder verwenden, um einen komplexeren State mit weiteren Änderungen zu erzeugen: +Wenn wir das Beispiel so implementieren, stoßen wir allerdings auf ein weiteres Problem: Die Struktur des States wird dynamisch verändert, wenn einzelne Feature-Module mithilfe von `forFeature()` einen Teilbaum des States registrieren. Durch eine statische Typanalyse allein ist es also nicht möglich, die vollständige Struktur des States zu ermitteln: Der TypeScript-Compiler weiß nicht, dass das Property `book` im State existiert. -\lstinputlisting[caption={Reducer mit Adapter-Funktionen}, label=lst:ngrx:entityreducer, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/reducer.ts} +Dazu kommt, dass wir an dieser Stelle lediglich einen einfachen Teil des States selektiert haben. In der Praxis werden aber die Lesezugriffe auf den State komplexer. Mitunter wollen wir Daten nicht nur einfach auslesen, sondern Projektionen über verschiedene Teile des States ausführen. Stellen Sie sich vor, Sie besitzen eine Liste von Autoren und Autorinnen und eine Liste von Büchern – wollen aber nun nur die Bücher ausgeben, die von einer bestimmten Person verfasst wurden. Dazu ist zusätzliche Logik nötig, die nicht in die Komponenten gehört. Stattdessen soll diese Logik in separate Funktionen ausgelagert werden, die unabhängig von den Komponenten sind. Sie können eine solche Funktion mit einer Datenbankabfrage vergleichen: Die Query wird einmal definiert und kann beliebig komplex sein. Verschiedene Teile der Anwendung können diese Query nutzen und die Daten genau im benötigten Format erhalten. Die Funktionen zur Abfrage von Daten aus dem Store werden *Selektoren* genannt. -Der EntityAdapter stellt außerdem eine Reihe von Selektoren zur Verfügung, die wir nutzen können, um die Kollektion auszulesen. -Die Selektoren werden mithilfe von `adapter.getSelectors()` erzeugt, müssen dann allerdings noch in einzelne Selektoren verpackt werden, die den jeweiligen Feature-State als Grundlage nutzen: +Selektoren werden ebenfalls in eigenen Dateien untergebracht, die unabhängig von den Komponenten sind. Dazu wurde bereits die Datei `books/store/book.selectors.ts` erstellt. Zur Definition von Selektoren bringt NgRx eigene Hilfsfunktionen mit: `createFeatureSelector()` und `createSelector()`. Sie sorgen unter anderem dafür, dass die selektierten Daten korrekt typisiert sind, auch wenn der State zur Laufzeit dynamisch erweitert wird. -\lstinputlisting[caption={Selektoren mit `getSelectors()`}, label=lst:ngrx:entityselectors, language=js,breakatwhitespace=true]{\snippets/reduxngrx/entity/selectors.ts} +In einem solchen Selektor versteckt sich außerdem ein wichtiges Konzept, das sich *Memoization* nennt. Damit aufwendige Projektionen nicht bei jeder State-Änderung neu ausgeführt werden, speichert jeder Selektor seine zuletzt verarbeiteten Eingabewerte. Haben sich diese Werte nicht geändert, so wird auch die Projektion nicht neu ausgeführt, sondern das zuletzt berechnete Ergebnis wird erneut ausgegeben. Das Prinzip der Memoization ist in die Selektoren bereits eingebaut, wenn wir die mitgelieferten Funktionen verwenden. Damit das allerdings funktioniert, müssen wir Selektoren als Pure Functions definieren: Sie dürfen nur die Werte verarbeiten, die sie als Argumente erhalten, und es dürfen keine Seiteneffekte ausgeführt werden. -Auf diese Weise können wir die Verwaltung von Entitäten im State sehr effizient durchführen. +Ausgehend davon, dass der Store immer den gesamten State liefert, müssen wir zunächst einen bestimmten Feature-State aus dem großen Objekt selektieren. Dazu nutzen wir die Funktion `createFeatureSelector()`. Sie erstellt einen Selektor, der ein Feature anhand seines Namens auswählt. Dabei verwenden wir den Feature-Key, den wir bereits bei `forFeature()` im `BooksModule` genutzt haben. Damit diese Selektion typsicher funktioniert, müssen wir außerdem das Interface für den Feature-State angeben. Um den State-Slice für das Feature `book` auszuwählen, benötigen wir also den folgenden Feature-Selektor, der bereits automatisch vorbereitet wurde: -### Testing +```ts +// books/store/book.selectors.ts +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import * as fromBook from './book.reducer'; +export const selectBookState = createFeatureSelector( + fromBook.bookFeatureKey +); +``` -Alle auf Grundlage von NgRx entwickelten Bausteine sollten auch getestet werden. -Dafür möchten wir Ihnen in diesem Abschnitt einige Hinweise geben. -Grundsätzlich werden bei der Initialisierung mit den Schematics von NgRx bereits Grundgerüste für die Unit-Tests angelegt – Sie können also direkt loslegen. +Anschließend können wir weitere Selektoren mithilfe der Funktion `createSelector()` bauen. Diese Funktion erhält als Argumente mehrere andere Selektoren. Das Kernstück des Selektors ist schließlich die Projektionsfunktion, die im letzten Argument übergeben wird. Sie erhält die Daten von allen zuvor angegebenen Selektoren als Argumente und liefert den daraus berechneten Wert zurück: -\subsubsection*{Actions} -Die Action Creators beinhalten keine spezifische Logik, die getestet werden muss. -In manchen Blogartikeln wird vorgeschlagen, die korrekte Zusammensetzung der Objekte zu prüfen, damit eine Action stets den richtigen `type` besitzt. -Wir sind der Meinung, dass solche trivialen Tests keinen Mehrwert liefern und deshalb nicht notwendig sind. +```ts +export const selectMyData = createSelector( + selectBookState, + selectAdminState, + selectUserState, + (bookState, adminState, userState) => { + // Berechnung ... + return output; + } +); +``` -\subsubsection*{Reducers} +Wir können auf diese Weise ein komplexes Gerüst von Selektoren definieren. Die Selektoren bauen aufeinander auf und kombinieren die Daten aus dem State so, wie sie in den Komponenten benötigt werden. Jeder einzelne Selektor nutzt die Memoization, um die berechneten Werte zu cachen. Die Berechnung in der Projektionsfunktion wird nur neu ausgeführt, wenn sich einer der Eingabewerte tatsächlich ändert. So sind auch komplexe Projektionen in großen Anwendungen ohne Nachteile in der Performance möglich. +Für den BookMonkey wollen wir zwei einfache Selektoren entwickeln, die uns Zugriff auf das `loading`-Flag und auf die Buchliste geben: -Ein Reducer ist eine Pure Function, liefert also für die gleiche Eingabe immer die gleiche klar vorhersehbare Ausgabe. -Diese Eigenschaft kommt uns beim Testing zugute, denn wir können Reducers isoliert testen, ohne dass Angular dafür benötigt wird. -Dazu rufen wir die Reducer-Funktion mit einem Startzustand und einer Action auf und prüfen den erzeugten neuen Zustand. +```ts +// books/store/book.selectors.ts +export const selectBooksLoading = createSelector( + selectBookState, + state => state.loading +); -\lstinputlisting[caption={Reducer testen \textup{(\file{book.reducer.spec.ts})}}, label=lst:ngrx:testingreducer, language=js,breakatwhitespace=true,belowskip=0pt]{\currentBm/src/app/books/store/book.reducer.spec.ts} +export const selectAllBooks = createSelector( + selectBookState, + state => state.books +); +``` -\subsubsection*{Selektoren} -Zum Testen von Selektoren gibt es verschiedene Herangehensweisen. -Auch wenn ein Selektor mithilfe von `createSelector()` erstellt wurde, so ist er weiterhin nur eine Funktion, die den State als Argument erhält und Berechnungen über die Daten ausführt. -Wir können also auch Selektoren isoliert testen, ohne dass wir einen Store oder das Angular-Framework benötigen. -Dazu erstellen wir im Test ein State-Objekt, das alle benötigten Daten enthält. -Wir wenden den Selektor darauf an und prüfen das Ergebnis. -Wichtig ist, dass wir in diesem Stub stets die Struktur des Root-States abbilden, denn der ist ja auch der Ausgangspunkt eines jeden Selektors. +In der `BookListComponent` verwenden wir nun diese beiden Funktionen, um Observables zu erstellen, die uns die benötigten Daten liefern. Die Methode `store.select()` erhält dazu als Argument einen der eben definierten Selektoren. Wir legen alle Observables direkt in der Komponentenklasse ab, denn wir wollen die Subscription im Template mit der AsyncPipe erledigen. Wenn Sie Ihre Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. Sie müssen also nur selten in der Komponentenklasse direkt auf ein Observable aus dem Store subscriben. -Die Hilfsfunktion `book()` haben wir selbst definiert. Sie generiert einfache Buch-Objekte mit Beispieldaten, sodass wir schnell Testdaten erzeugen können. +Da wir das Observable `books$` nun aus dem Store beziehen, benötigen wir den `BookStoreService` nicht mehr. Wir können das Argument des Konstruktors also entfernen. -\pagebreakbeforelisting -\lstinputlisting[caption={Einfache Selektoren testen \textup{(\file{book.selectors.spec.ts})}}, label=lst:ngrx:testingselector, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.selectors.spec.ts} +```ts +// books/book-list/book-list.component.ts +import { selectAllBooks, selectBooksLoading } from '../store/book.selectors'; -Bei komplexeren Selektoren, die mehrere andere Selektoren als Grundlage verwenden, liegt die kritische Logik in der Projektionsfunktion, die im letzten Argument von `createSelector()` angegeben wird. -Es ist deshalb in den meisten Fällen ausreichend, nur diese Projektion zu testen. -Zugriff auf die Funktion erhalten wir mit `selector.projector`: +@Component({ /* ... */ }) +export class BookListComponent { + books$: Observable; + loading$: Observable; -\lstinputlisting[caption={Projektionsfunktion testen \textup{(\file{book.selectors.spec.ts})}}, label=lst:ngrx:testingselectorprojector, language=js,breakatwhitespace=true,firstline=4,lastline=12,belowskip=0pt]{\currentBm/src/app/books/store/book.selectors.1.spec.ts} + constructor(private store: Store) { + this.books$ = this.store.select(selectAllBooks); + this.loading$ = this.store.select(selectBooksLoading); -\subsubsection*{Effects} + this.store.dispatch(loadBooks()); + } +} +``` -Effects sind nur schwierig isoliert zu testen, denn sie greifen auf verschiedene Abhängigkeiten aus der Anwendung zu. -Dazu ist nicht nur das Modul `@ngrx/effects` nötig, sondern auch alle verwendeten HTTP-Services. -Außerdem muss das Observable `actions\$: Actions` mit einem Strom von Actions versorgt werden, und wir wollen keinen vollständigen Store aufsetzen. +Im Template der Komponente nutzen wir die Observables, um die Daten darzustellen. Das Observable `books$` wird bereits korrekt verarbeitet. Für den Ladeindikator können wir ein neues Element erstellen, das wir direkt mit `ngIf` ein- und ausblenden. Unser globales Stylesheet bietet dafür die passende CSS-Klasse `loader` an. -\footnote{`provideMockActions()`}NgRx bietet dafür die Funktion `provideMockActions()`, die einen gemockten Strom von Actions bereitstellt. -Tests für Effects müssen mithilfe von `TestBed` definiert werden, sodass wir die Dependency Injection von Angular nutzen können. -Alle verwendeten Services müssen selbstverständlich auch durch Mocks oder Stubs ersetzt werden, damit keine echten HTTP-Requests ausgeführt werden. +```html + +

Books

+
    + +
-\pagebreakbeforelisting -\lstinputlisting[caption={Grundgerüst zum Testen von Effects \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingeffect, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.spec.ts.1.snippet} +
Loading ...
+``` -Für die konkreten Tests müssen wir ein Observable von Actions bereitstellen und das ausgegebene Observable sowie ggf. die ausgeführten Seiteneffekte prüfen. -Das \autoref{lst:ngrx:testingeffectFull} zeigt einen vollständigen Testaufbau. -Hier stellen wir sicher, dass der Effect die gewünschte Action ausgibt und die Daten wie gewünscht über den `BookStoreService` bezieht.%blank line down +Wenn Sie die Anwendung nun aufrufen, sehen Sie den Ladeindikator. Die Komponente ist nun deutlich loser gekoppelt als vorher, denn sie braucht keine eigene spezifische Logik mehr, um den Indikator anzuzeigen oder Bücher zu laden. Sie bezieht lediglich Daten über Observables aus dem Store und löst Actions aus. -\lstinputlisting[caption={Vollständiger Test für einen Effect \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingeffectFull, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.spec.ts.2.snippet} +### Effects: Seiteneffekte ausführen -\footnote{Marble Testing}Der Aufbau lässt sich vereinfachen, wenn wir nur die Datenströme miteinander vergleichen. -Dazu eignet sich das Konzept des *Marble Testing*: -Anstatt ein Observable wie üblich zu erzeugen und dann darauf zu subscriben, notieren wir den geplanten Datenstrom als Marble-Diagramm direkt im Test und definieren damit die Eingabe und die erwartete Ausgabe. -Die technische Grundlage dafür bietet das Paket \mbox{`jasmine-marbles`}.\shortlinkFootnote{link-jasmine-marbles} -Das Projekt stellt auch den Matcher `toBeObserv\-able()` zur Verfügung, mit dem wir in der Expectation direkt gegen das erzeugte Observable prüfen können: +Wir haben nun alle Bausteine von Redux betrachtet und den Weg der Daten von der Action bis zum Selektor verfolgt. Jeder Baustein hat klare Zuständigkeiten, und es gibt strikte Regeln dazu, welche Aufgaben in welchen Teil der Anwendung gehören. -\lstinputlisting[caption={Effects testen mit Marbles \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingmarble, language=js,breakatwhitespace=true,belowskip=0pt]{\currentBm/src/app/books/store/book.effects.marble.spec.ts.snippet} +Was unserer Anwendung allerdings bis hierhin noch fehlt, ist die Kommunikation mit der Außenwelt: Wird die Action `loadBooks` ausgelöst, so soll die Buchliste per HTTP vom Server geladen werden. Bei genauer Betrachtung fällt jedoch auf, dass Reducers und Selektoren nicht die richtigen Orte für diese Aufgabe sind: Beide sind durch Pure Functions definiert, die keine Seiteneffekte ausführen dürfen. -\subsubsection*{Store} +Die grundsätzliche Aufgabe lässt sich wie folgt zusammenfassen: Wir wollen auf die Action `loadBooks` reagieren und einen HTTP-Request auslösen, um die Buchliste vom Server zu laden. Ist die HTTP-Antwort eingetroffen, so soll die Buchliste mit einer Action `loadBooksSuccess` in den Store gesendet werden. Schlägt die HTTP-Kommunikation fehl, so soll die Action `loadBooksFailure` ausgelöst werden. -Komponenten und Effects greifen häufig auf den Store zu. -Unter anderem werden Actions in den Store gesendet (dispatcht), und der aktuelle Zustand wird mithilfe von Selektoren ermittelt. -Wenn man für solche Komponenten oder Effects einen Unit-Test definieren will, benötigt man einen Ersatz für den echten Store. -\footnote{`provideMockStore()`}NgRx bietet uns hierfür die Funktion `provideMockStore()` an: -Wir können damit einen gemockten Store registrieren, der einen von uns definierten Zustand besitzt. -Dieser Zustand bleibt so lange unverändert, bis wir mittels `store.setState()` einen anderen Zustand setzen. -Als Beispiel wollen wir den bekannten Effect zum Laden der Bücher so erweitern, dass er nur dann Bücher lädt, wenn die Buchliste im State noch leer ist. -Hat das Array bereits Einträge, soll nichts passieren. +Um diese Aufgabe anzugehen, bietet NgRx ein ausgereiftes Modell zur Ausführung von Seiteneffekten: Mit `@ngrx/effects` können wir asynchrone Ereignisse und andere Seiteneffekte in der Anwendung koordinieren. In einem Effect ist "alles" erlaubt: HTTP-Kommunikation, Logging, Authentifizierung, LocalStorage, Routing, Anzeige von Meldungen usw. Effects sind kein Bestandteil der eigentlichen Redux-Architektur, sondern agieren außerhalb des Stores. -Es ist für dieses Szenario notwendig, dass wir den bestehenden State im Effect berücksichtigen. -Dies können wir mit dem Operator `concat\-Latest\-From()` aus dem Paket `@ngrx/effects` realisieren: -Wir übergeben ein anderes Observable als Argument, und der Operator reichert den Hauptdatenstrom mit dem jeweils letzten Element aus diesem Observ\-able an. -Das bedeutet also, dass uns in den Effects neben dem Payload aus den Actions auch zusätzlich Daten aus dem Store zur Verfügung stehen. -Wir verwenden hier direkt unseren Selektor `selectAllBooks`, um die aktuelle Buchliste aus dem Store zu erhalten. -Anhand der Buchliste können wir dann mit dem Operator `filter()` entscheiden, ob die Bücher neu heruntergeladen werden sollen oder nicht. +Mit Effects kommen wir in den Genuss des vollen Erlebnisses von reaktiver Programmierung. Ein Effect ist ein reaktiver Datenstrom, der -\pagebreakbeforelisting -\lstinputlisting[caption={Effect zum Laden der Buchliste mit Filter \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingeffectwithFilter, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.alternative.ts.snippet} +- auf Actions und andere Ereignisse reagiert, +- Seiteneffekte ausführen kann und +- neue Actions generiert. -Für diesen Effect sollten wir sicherstellen, dass bei einem leeren Array auch tatsächlich die Action `loadBooksSuccess` gefeuert wird – und dass dies bei einem gefüllten Array eben gerade nicht passiert. -Mittels `provide\-MockStore()` stellen wir vor jeder Spezifikation zunächst den vorgetäuschten\ Store bereit. -Für die zweite Spezifikation ändern wir den State anschließend noch einmal mittels `store.setState()` ab:%blank line down +Technisch ist ein Effect also immer ein `Observable`. Alle so erzeugten Actions werden automatisch in den Store dispatcht – darum kümmert sich das Framework. Effects werden in einer eigenen Klasse untergebracht, die wie ein Service aufgebaut ist: Sie trägt den Decorator `@Injectable()` und kann über ihren Konstruktor Abhängigkeiten anfordern. Damit die Effects funktionieren, muss die Klasse allerdings explizit registriert werden. Dazu muss jede Effects-Klasse in einem Modul mithilfe des `EffectsModule` eingebunden werden. Für das `BooksModule` wurde dieser Schritt bereits bei der Einrichtung erledigt: -\lstinputlisting[caption={Effects testen mit dem `MockStore` \textup{(\file{book.effects.spec.ts})}}, label=lst:ngrx:testingMockStore1, language=js,breakatwhitespace=true]{\currentBm/src/app/books/store/book.effects.alternative.spec.ts.snippet} + -Ebenso können wir mit dem `MockStore` die Rückgabewerte von Selektoren überschreiben. -Unser letztes Beispiel zum Thema zeigt, wie wir sicherstellen können, dass die `BookListComponent` tatsächlich den Text * Loading~...* anzeigt, wenn im State das Loading-Flag entsprechend gesetzt ist (siehe \autoref{lst:ngrx:useselectorsblc} und \autoref{lst:ngrx:blchtml} ab \autopageref{lst:ngrx:useselectorsblc}). -Es ist dabei nicht notwendig, den State im `MockStore` zu setzen, da wir auf diesen nicht direkt zugreifen. -Die Komponente bezieht ihre Daten über die beiden Selektoren `select\-All\-Books` und `select\-Books\-Loading`, die wir entsprechend "ausmocken": +```ts +// books/books.module.ts +import { EffectsModule } from '@ngrx/effects'; +import { BookEffects } from './store/book.effects'; -\lstinputlisting[caption={Selektoren mocken mit dem `MockStore` \textup{(\file{\mbox{book-list}"".component.spec.ts})}}, label=lst:ngrx:ngrx:testingMockStore2, language=js,breakatwhitespace=true]{\currentBm/src/app/books/book-list/book-list.component.spec.ts.snippet} +@NgModule({ + // ... + imports: [ + // ... + EffectsModule.forFeature([BookEffects]) + ] +}) +export class BooksModule { } +``` -Wie Sie sehen, stellt NgRx einige Hilfen und Konzepte bereit, um die einzelnen Bausteine effektiv zu testen. -Vor allem die Funktionen `provide\-Mock\-Actions()` und `provide\-Mock\-Store()` erleichtern uns das Testing für komplexe Szenarien in NgRx. +Die Klasse `BookEffects` befindet sich in der Datei `books/store/book.effects.ts`. Ein Effect wird als ein Property in dieser Klasse definiert. Der Name des Propertys spielt keine Rolle, sollte aber passend zur Aufgabe benannt werden. Das Ziel ist es, hier ein Observable zu entwickeln, das Actions ausgibt. Jeder Effect wird mit der Funktion `createEffect()` gekapselt und wird so automatisch in den Lebenszyklus von NgRx integriert. Ein erster Effect zum Laden von Daten wurde hier bereits automatisch generiert, weil wir beim Anlegen die Einstellung `--api` verwendet haben. Dieses Grundgerüst wollen wir vervollständigen, um die Buchliste per HTTP zu laden. -### Hilfsmittel für Komponenten: `@ngrx/component` +Zunächst benötigen wir eine Instanz des `BookStoreService`, der die HTTP-Kommunikation kapselt: +```ts +// books/store/book.effects.ts +import { BookStoreService } from '../../shared/book-store.service'; + +@Injectable() +export class BookEffects { + // ... + constructor( + private actions$: Actions, + private service: BookStoreService + ) {} +} +``` + +In die Klasse wurde bereits der Service `Actions` injiziert. Damit erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. Dieser Datenstrom ist meist die Grundlage für unsere Effects. (Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: [5 useful NgRx effects that don't rely on actions](https://angular.schule/blog/2018-06-5-useful-effects-without-actions).) + +Der Effect `loadBooks$` soll auf die Action `loadBooks` reagieren, den passenden HTTP-Request auslösen und Actions zurück in den Store leiten (Success und Failure). Wir zeigen zunächst den vollständigen Code und gehen die Implementierung anschließend Schritt für Schritt durch: + +```ts +// books/store/book.effects.ts +import { Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; + +import * as BookActions from './book.actions'; +import { BookStoreService } from '../../shared/book-store.service'; + +@Injectable() +export class BookEffects { + + loadBooks$ = createEffect(() => { + return this.actions$.pipe( + ofType(BookActions.loadBooks), + switchMap(() => + this.service.getAll().pipe( + map(data => BookActions.loadBooksSuccess({ data })), + catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) + ) + ) + ); + }); + + constructor( + private actions$: Actions, + private service: BookStoreService + ) {} +} +``` +Der Datenfluss beginnt beim Strom aller Actions aus der gesamten Anwendung: `this.actions$`. Wir interessieren uns hier allerdings nur für Actions mit dem bestimmten Typ `loadBooks`. Um den Datenstrom zu filtern, eignet sich der Operator `filter()`. Das führt allerdings zu sehr technischem Code, erschwert die korrekte Typisierung und sagt wenig über die tatsächliche Semantik aus. NgRx bringt deshalb einen eigenen Operator mit, mit dem wir den Datenstrom nach dem Action-Typ filtern können: `ofType()`. Als Argument übergeben wir hier einen Action Creator. Anschließend liegt ein Datenstrom vor, der nur Actions vom Typ `loadBooks` ausgibt. -Das Paket `@ngrx/component` stellt eine Sammlung von nützlichen Helfern zur Verfügung, um mit reaktiven Datenströmen in Komponenten zu arbeiten. -Die Werkzeuge sind dabei nicht auf den Einsatz mit NgRx beschränkt, sondern können auch unabhängig davon eingesetzt werden. -Das Paket beinhaltet zwei elementare Bausteine: die Let-Direktive und die PushPipe. +Für jede dieser Actions wollen wir nun einen HTTP-Request ausführen und die Ergebnisse verarbeiten. Damit betreten wir erneut das Terrain der Higher-Order Observables. Wir müssen uns sorgfältig für einen der vier Flattening-Operatoren entscheiden. Unsere Wahl fällt hier auf `switchMap()`: Wird die Buchliste noch geladen und währenddessen erneut angefragt, soll nur die zuletzt gesendete Anfrage bearbeitet werden. Verwenden Sie bitte nicht immer `switchMap()` in Ihren Effects, sondern wägen Sie sorgfältig ab, welcher Flattening-Operator am besten zu Ihrem jeweiligen Problem passt. -\footnote{AsyncPipe}Üblicherweise verwenden wir die AsyncPipe von Angular, um Observables direkt im Template einer Komponente aufzulösen. -Das ist der Weg, den wir vor allem beim Einsatz von NgRx wählen, denn die Daten kommen bereits vollständig aufbereitet durch Observables in die Komponente. -Die AsyncPipe kann in jedem Template-Ausdruck eingesetzt werden, also in der Interpolation, in Property Bindings und in Direktiven. -Gemeinsam mit `ngIf` und der `as`-Syntax können wir das Ergebnis eines Observables so in einem DOM-Container verfügbar machen. +Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookStoreService` mit der Methode `getAll()` einsetzen, die ein Observable zurückgibt. Unser Effect ist nun ein Observable, das ein Array von Büchern liefert. Damit die Buchliste im Store verarbeitet werden kann, müssen wir sie in eine Action verpacken. Wir nutzen dazu den Operator `map()` und wandeln damit die Buchliste um in eine Action `loadBooksSuccess`, die die Liste enthält. -\lstinputlisting[caption={AsyncPipe verwenden}, label=lst:ngrx:compasncpipe, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrxcomponent/asyncpipe.html} +Ähnlich gehen wir für den Fehlerfall vor: Hier nutzen wir den Operator `catchError()`, um einen Fehler in der Ausführung abzufangen. Wir werfen den Fehler allerdings an der Stelle nicht weiter, sondern kapseln ihn in eine Action `loadBooksFailure`, die wir als reguläres Element des Datenstroms weitergeben. Dabei verwenden wir das Property `message` auf dem Fehlerobjekt, um die Meldung auszulesen. -So praktisch die AsyncPipe allerdings auch ist – sie birgt einige praktische Probleme. +Auf diese Weise erzeugen wir einen Datenstrom, der nacheinander verschiedene Actions ausgeben kann. Die Funktion `createEffect()` sorgt dafür, dass dieser Datenstrom automatisch abonniert wird. Die resultierenden Actions werden in den Store dispatcht und durchlaufen dort die Reducers. In einem Effect sollten wir also niemals selbst die Methode `store.dispatch()` aufrufen. -\subsubsection*{ngrxLet: Observables auflösen mit falsy Werten} +Bei der Fehlerbehandlung ist es wichtig, den Fehler so dicht wie möglich dort abzufangen, wo er entsteht. Wir setzen `catchError()` deshalb nicht im Hauptdatenstrom ein, sondern hängen `map()` und `catchError()` direkt an den Serviceaufruf an. Das `switchMap()` verarbeitet also ein Observable, das in jedem Fall eine Action liefert – so wird der Hauptdatenstrom nicht durch Fehler gefährdet. Wird ein Effect durch einen Fehler im Hauptdatenstrom gestört, so wird das zugrunde liegende Observable beendet, und die Anwendung kann nicht mehr auf eine weitere Nachricht durch den beendeten Effect reagieren. +Probieren Sie die Anwendung nun aus und beobachten Sie auch die Actions in den Redux DevTools! Nachdem die Buchliste geladen wurde, wird `loadBooksSuccess` dispatcht, und der Reducer fügt die Buchliste in den State ein. Die Selektoren in der Komponente reagieren darauf, und die Bücher werden dargestellt. -Die oben beschriebene Kombination von AsyncPipe und `ngIf` ist hilfreich, um die Daten aus dem Observable `numbers\$` im Template verfügbar zu machen. -Liefert das Observable allerdings einen *falsy* Wert – also `false`, `0`, `null`, `undefined`, `NaN` oder einen leeren String – , so wird der jeweilige Container durch `ngIf` gar nicht angezeigt. +Bitte beachten Sie, dass Sie mit einem ungünstig programmierten Effect schnell eine Endlosschleife erzeugen können, die den Browser "zum Glühen" bringt, wenn parallel `ng serve` läuft und die Anwendung sofort aktualisiert. Der folgende Effect empfängt alle Actions und leitet sie direkt in den Store zurück – es entsteht eine Endlosschleife: -Um dieses Problem zu umgehen, kann die Direktive `ngrxLet` eingesetzt werden. -Sie löst ein Observable im Template auf und stellt die empfangenen Daten in einer lokalen Variable zur Verfügung. -Im Gegensatz zu `ngIf` bleibt das DOM-Element dabei stets sichtbar und wird nicht ausgeblendet. -Liefert `numbers\$` also eine `0`, so wird der Container im folgenden Beispiel trotzdem angezeigt. -Darüber hinaus erhält man mit `ngrxLet` bei Bedarf auch Zugriff auf mögliche Error- und Complete-Ereignisse des Observables. +```ts +loopOfDeath$ = createEffect(() => this.actions$); +``` -\lstinputlisting[caption={Direktive `ngrxLet` verwenden}, label=lst:ngrx:completdirective, language=js,breakatwhitespace=true,belowskip=0pt]{\snippets/reduxngrx/ngrxcomponent/letdirective.html} +Wir können übrigens einen Effect so konfigurieren, dass er die ausgegebenen Elemente des Datenstroms nicht als Actions dispatcht. Das ist immer dann nützlich, wenn ein Effect lediglich Seiteneffekte ausführt, aber keine weiteren Aktionen zur Folge hat. Denken Sie z. B. daran, dass wir nach dem erfolgreichen Anlegen eines Buchs zur Detailseite navigieren möchten – nach diesem Schritt sollen keine weiteren Actions getriggert werden. Dazu können wir im zweiten Argument von `createEffect()` die Einstellung `{ dispatch: false }` setzen. -\subsubsection*{PushPipe: Observables auflösen ohne Zone.js} +```ts +loopOfDeath$ = createEffect( + () => this.actions$, + { dispatch: false } +); +``` +Ein Tipp aus der Praxis: Nutzen Sie diese Einstellung während der Entwicklung, um einen *Loop of Death* zu verhindern. Sie können einen komplexen Effect dann in Ruhe debuggen, ohne dass die Actions in den Store geleitet werden. Erst wenn die Implementierung fertig ist, aktivieren Sie den Effect, indem Sie das `{ dispatch: false }` wieder entfernen. -Die AsyncPipe von Angular hat den Vorteil, dass sie automatisch die betroffene Komponente als *dirty* markiert, sobald ein neuer Wert im Observable ausgegeben wird. -Daraufhin wird diese Komponente beim nächsten Durchlauf der Change Detection geprüft, sodass die View aktualisiert wird. -Durch diesen Mechanismus ist es möglich, in allen Komponenten die Strategie `OnPush` für die Change Detection zu aktivieren. -Die AsyncPipe ist allerdings stets abhängig von der Bibliothek Zone.js, die für bestimmte Ereignisse in der Anwendung automatisch die Change Detection triggert. +#### Achtung: Proprietäres Verhalten von TypeScript -Im Abschnitt zur Change Detection ab \autopageref{sub:cd:zoneless} beschreiben wir einen Weg, um Angular ohne Zone.js zu verwenden. -Entwickelt man die Anwendung durchweg mit den Prinzipien der Reaktiven Programmierung, so kann es sinnvoll sein, auf die automatische Change Detection zu verzichten. -Hier setzt die Pipe `ngrxPush` von NgRx an, die als Alternative zur AsyncPipe genutzt werden kann. -\footnote{Zoneless Angular}Der elementare Unterschied: Die PushPipe triggert die Change Detection direkt, anstatt die Komponente nur für den nächsten Durchlauf als *dirty* zu markieren. -Die PushPipe ist damit unabhängig von Zone.js und kann in einer *zoneless* Umgebung eingesetzt werden. -Das kann sich positiv auf die Performance der Anwendung auswirken. +Bei allen gezeigten Effects haben wir die Syntax mit dem alten proprietären Verhalten von TypeScript (`useDefineForClassFields: false`) verwendet. Zum Zeitpunkt der Fertigstellung dieses Buchs wurde diese Schreibweise in der offiziellen Dokumentation, in der Demo-Anwendung und in den Schematics von NgRx empfohlen. Wir haben uns daher dazu entschlossen, möglichst wenig von den offiziellen Quellen abzuweichen, auch wenn es womöglich bei geänderten Standardeinstellungen zu Fehlern kommen kann. Im Buch beschreiben wir im Teil "Wissenswertes", unter welchen Umständen dieser Fehler entsteht. -Übrigens ermittelt die PushPipe automatisch, ob Zone.js in der Anwendung aktiviert ist, und kann dadurch auch das Verhalten der Async\-Pipe annehmen. -Auch die zuvor beschriebene Direktive `ngrxLet` baut auf derselben Grundlage auf und kann ohne Zone.js eingesetzt werden. +Sollten die Beispiele in Zukunft nicht mehr lauffähig sein, so können Sie den Code leicht umstellen. Das folgende Listing zeigt eine Variante, bei der wir die Klasse `Actions` und den `BookStoreService` mithilfe der Funktion `inject()` anfordern. -\pagebreakbeforelisting -\lstinputlisting[caption={Pipe `ngrxPush` verwenden}, label=lst:ngrx:comppushpipe, language=js,breakatwhitespace=true,belowskip=0pt]{\snippets/reduxngrx/ngrxcomponent/pushpipe.html} +```ts +// books/store/book.effects.ts +import { inject } from '@angular/core'; -### Facades: Zustandsverwaltung abstrahieren +@Injectable() +export class BookEffects { -Um den Anwendungszustand mit NgRx zu verwalten, kommunizieren die Komponenten mit dem zentralen Store: Sie senden Actions in das System und lesen Daten mithilfe von Observables aus. -Dafür müssen wir die Klasse `Store` als direkte Abhängigkeit in den Komponenten verwenden. -Außerdem müssen wir die Actions und Selektoren importieren. + private service = inject(BookStoreService); -Damit sind die Komponenten stets abhängig von den Bausteinen des Frameworks NgRx. -Das erschwert insbesondere das Testing und die Wiederverwendbarkeit. -Außerdem müssen wir uns für das State Management auf eine spezifische Lösung festlegen. -Jeglicher State wird global im Store abgelegt, und ein Mischbetrieb mit anderen Ansätzen führt potenziell zu unsauberem Code in der Komponente. + loadBooks$ = createEffect(() => { + return inject(Actions).pipe( + ofType(BookActions.loadBooks), + switchMap(() => + this.service.getAll().pipe( + map(data => BookActions.loadBooksSuccess({ data })), + catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) + ) + ) + ); + }); +} +``` -\footnote{Facade: Schnittstelle für die Komponenten}Um diese Unschönheiten zu lösen, können wir eine *Facade* verwenden: -Dafür erstellen wir einen Service, der eine Schnittstelle zwischen der Komponente und dem Store bildet. -Die Komponente kommuniziert über Methoden und Propertys mit der Facade. -Das Framework NgRx bleibt dabei verborgen, und wir halten die Komponente frei von frameworkspezifischem Code. +### Geschafft! -\begin{fig} - {fig:redux-facade} - {NgRx Flow mit Facade} - {NgRx Flow mit Facade} - {10pt} - \includegraphics[width=\textwidth]{images/statemanagement/facade} -\end{fig} +Sobald der Effect aktiv ist, wird unsere Anwendung die Bücher über HTTP laden und die Liste mittels `loadBooksSuccess` an den Reducer übergeben. Dieser wird einen neuen Zustand mit den geladenen Büchern berechnen. Abschließend wird mithilfe eines Selektors die Oberfläche aktualisiert, und die neuen Bücher werden angezeigt. -\suppressindent -Zur Implementierung nutzen wir einen einfachen Service, der mit dem Store kommuniziert. -Als öffentliche Schnittstelle werden gut lesbare Methoden und Propertys angeboten. +Und damit haben Sie die Rundreise durch alle Bausteine von Redux und NgRx geschafft. Mithilfe eines zentralen Stores haben wir die Zustände der Anwendung zentralisiert und so die Buchliste vom Server abgerufen. Die Komponenten beinhalten mit dieser Architektur nur noch wenig Logik – alles Nötige passiert im Store. -\pagebreakbeforelisting -\lstinputlisting[caption={Beispiel für eine Facade}, label=lst:ngrx:facade, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/books.facade.ts} +Vielleicht haben Sie nun das Gefühl, dass wir für eine kleine Aufgabe unnötig viel Code erzeugt haben. Betrachtet man das vorliegende Beispiel isoliert, so ist diese Empfindung richtig: Für kleine Anwendungen ist der Einsatz von Redux und NgRx nicht zwingend sinnvoll. Steigt allerdings die Komplexität des Projekts, so kann ein zentrales State Management mit klaren Regeln und einer durchdachten Architektur eine sinnvolle Investition sein. Prüfen Sie also für ein Projekt sorgfältig, ob sich der Einsatz von Redux wirklich lohnt. -In der Komponente nutzen wir die Facade, um die Daten aus den Propertys zu lesen und Aktionen mithilfe der angebotenen Methoden auszulösen. +## Debugging mit den Redux DevTools -\lstinputlisting[caption={Facade in der Komponente verwenden}, label=lst:ngrx:facadeusage, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/facade-usage.ts} +Wir möchten Ihnen zum Abschluss des Praxisteils ein wichtiges Hilfsmittel zeigen, das beim Debugging von Anwendungen mit NgRx hilfreich ist: die *Redux DevTools*. -Im Unit-Test können wir einen solchen Service leicht durch einen Stub ersetzen. -Die Propertys und Methoden können dabei in einem einfachen Objekt nachgebildet werden, ohne die Schnittstelle des Stores ausmocken zu müssen. -Im Kapitel zum Testing haben wir ab \autopageref{sec:testing:replacestub} auf diese Weise einen Stub für den `BookStoreService` bereitgestellt. +Ein besonderer Vorteil der Redux-Architektur ist das sogenannte *Time Travel Debugging*. Da stets durch eintreffende Actions ein neuer State im Store erzeugt wird, ist es möglich, die Historie der Ereignisse und Zustände nachzuvollziehen. Die Redux DevTools unterstützen uns dabei mit einer grafischen Oberfläche, über die wir alle Actions und Zustandsänderungen kontrollieren und debuggen können. -\lstinputlisting[caption={Unit-Testing: Stub für die Facade}, label=lst:ngrx:facadestub, language=js,breakatwhitespace=true]{\snippets/reduxngrx/ngrx/facade-stub.ts} +### Die DevTools installieren -Ein weiterer großer Vorteil dieser Idee liegt darin, dass wir die Implementierung für das State Management jederzeit austauschen oder variieren können. -So ist es zum Beispiel möglich, bestimmte Teile des States rein lokal in der Facade zu verarbeiten und gar nicht an den zentralen NgRx Store zu übermitteln. -Einen Ansatz für die Verwaltung von lokalem State bietet das Projekt `@ngrx/component-store`, das wir am Ende dieses Kapitels kurz vorstellen. +Die Redux DevTools sind eine Erweiterung für Google Chrome. Um sie zu verwenden, besuchen wir den Webstore und installieren die [Extension im Browser](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd). +![Die Redux DevTools im Chrome Web Store](./webstore-devtools.png "Die Redux DevTools als Erweiterung für Google Chrome installieren") -\footnote{Manueller Provider}Für eine Facade empfehlen wir übrigens, keinen Tree-Shakable Provider zu verwenden. -Stattdessen tragen wir die Facade unter `providers` in dem Modul ein, das auch den Feature-State bereitstellt. -Auf diese Weise ist garantiert, dass der Feature-State korrekt registriert ist, wenn wir die Facade verwenden. +### Die DevTools in der Anwendung registrieren +Um Informationen aus unserem Store mit dem Entwicklungswerkzeug zu teilen, müssen wir in der Anwendung die passende Schnittstelle schaffen. Dazu bringt NgRx das Modul `@ngrx/store-devtools` mit, das diese Schnittstelle zur Kommunikation implementiert. -\section*{Fazit} +Der einfachste Weg zur Einrichtung ist es, das Modul mithilfe von `ng add` in das Projekt einzufügen. Als wir im Praxisteil die Pakete für NgRx installiert haben, haben wir diesen Schritt bereits erledigt. -Mithilfe von Redux und NgRx können wir die Zustandsverwaltung in der Anwendung zentralisieren. -Redux setzt auf ein unveränderliches Zustandsobjekt, das mithilfe von Pure Functions im Store verwaltet wird. -Alle Ereignisse der Anwendung werden durch Actions signalisiert, die Änderungen am Zustand auslösen können. -Dabei erzeugen die Reducers einen neuen Zustand, der über ein Observable an alle Interessierten ausgegeben wird. -Jeder Baustein von NgRx hat eine klar definierte Aufgabe, was eine konsistente Struktur in das Projekt bringt. -Die Teile der Architektur sind stark entkoppelt, sodass sie unabhängig voneinander entwickelt und gewartet werden können. +```bash +ng add @ngrx/store-devtools +``` -Mit Redux lassen sich komplexe Strukturen in Projekten harmonisieren. -Das bedeutet allerdings nicht, dass Redux für jede Anwendung die passende Architektur ist. -Bewerten Sie deshalb für ein Projekt zunächst, ob sich der Einsatz von Redux wirklich lohnt. -Die Thematik ist nicht trivial – um eine solche zentrale Zustandsverwaltung sicher und effektiv einzusetzen, braucht es Zeit und Übung. -Die offizielle Dokumentation von NgRx\shortlinkFootnote{link-ngrxio} ist ein guter Ausgangspunkt, um alle Funktionen und Konzepte weiter zu studieren. +Damit werden die benötigten Abhängigkeiten installiert, und das zugehörige `StoreDevtoolsModule` wird in das `AppModule` der Anwendung eingebunden. -Zusätzlich haben wir das Beispiel aus diesem Kapitel auf GitHub zur Verfügung gestellt, sodass Sie den Code vollständig nachvollziehen können. +### Die DevTools nutzen -\qrdemo{bm5-ngrx} +Die *Redux DevTools* integrieren sich in die Chrome DevTools, und wir finden nach der Installation hier einen neuen Reiter *Redux*. +![Die Redux DevTools zeigen die Historie aller dispatchten Actions auf der linken Seite und die Zustandsänderungen auf der rechten Seite.](./developer-tool-action.png "Actions in den Redux DevTools anzeigen") -\input{chapter/redux/componentstore} +In der Abbildung sind fünf Actions in der Historie auf der linken Seite zu sehen. Die ersten drei Einträge mit dem Präfix `@ngrx` stammen vom System und werden automatisch bei der Initialisierung dispatcht. Danach folgen zwei Aktionen, die wir selbst in der Anwendung ausgelöst haben. Anhand dieser Liste kann genau nachvollzogen werden, was bisher geschehen ist: Es wurde die Aufforderung zum Abrufen der Buchliste in den Store geschickt, und die Liste kam schließlich vom Server zurück. -%%% Local Variables: -%%% TeX-PDF-mode: t -%%% mode: latex -%%% TeX-master: "../../angularBuch" -%%% End: +Wählen wir eine Action aus, werden auf der rechten Seite die Änderungen zum vorhergehenden Zustand visualisiert. Nachdem die Buchliste geladen wurde, wurde das `loading`-Flag von `true` auf `false` geändert und die Buchliste wurde in das Property `books` eingefügt. Mit den Reitern *Action* und *State* können wir das Action-Objekt und den State betrachten, wie er nach der Verarbeitung dieser Action aussah. +Mit der Historie der Actions können wir außerdem das Time Travel Debugging durchführen. Dazu besitzt jeder Eintrag in der Liste zwei Buttons: *Jump* und *Skip*. Damit können wir einzelne Actions überspringen oder den Zustand zu einem bestimmten Zeitpunkt rekonstruieren. Die Anwendung reagiert live auf die Zustandsänderungen, sodass wir das Ergebnis direkt im Browser betrachten können. Am unteren Rand befindet sich außerdem ein Slider, mit dem wir schrittweise durch die Zustände der Anwendung navigieren können. -================================================ -File: /tex/chapter/tipps/prettier.tex -================================================ -\chapter[Powertipp: Codeformatierung mit Prettier]{Powertipp:\newline Codeformatierung mit Prettier} +In der folgenden Abbildung wurde die Action `[Book] Load Books Success` mit der Schaltfläche *Skip* übersprungen. Nun ist links in der Anwendung der Ladeindikator zu sehen. +![Time Travel Debugging: Die Action zum erfolgreichen Laden der Bücher wurde übersprungen, daher zeigt die Anwendung wieder den Ladeindikator.](./developer-tool-skip-action.png "Time Travel Debugging mit den Redux DevTools") +Weitere interessante Features verstecken sich hinter den Buttons am unteren Rand: Damit können wir unter anderem alle Actions und Zustände als JSON-Datei exportieren und später wieder importieren. Ein solcher Export kann z. B. an ein anderes Teammitglied übermittelt werden, das damit den gleichen Anwendungsstatus erzeugen kann wie wir. -Wir haben bei der Projekteinrichtung ab \autopageref{sec:eslint} bereits das Projekt ESLint kennengelernt und eingerichtet. -ESLint führt eine syntaktische und semantische Prüfung des Codes durch, die sich nach dem Styleguide sowie gängigen Best Practices für JavaScript und TypeScript richtet. +Mit diesem Debugging-Werkzeug haben wir also an einem Ort einen zentralen Überblick über alle Ereignisse der Anwendung. Wir können den Zustand des Systems überwachen und so auch Fehler bei der Entwicklung einfacher aufspüren. -\footnote{Einheitliche Formatierung mit Prettier}ESLint kümmert sich allerdings nicht um die reine Formatierung des Codes: Wie werden Klammern gesetzt? Wie werden Blöcke eingerückt? Werden optionale Klammern bei Arrow Functions gesetzt? Für solche Fragen müssen wir im Entwicklungsteam einen Konsens finden und die Formatierung idealerweise automatisieren. -Im Web-Umfeld hat sich in den letzten Jahren das Projekt Prettier\shortlinkFootnote{link-prettier} durchgesetzt, um Quellcode automatisch und einheitlich zu formatieren. +## Redux und NgRx: Wie geht's weiter? -Prettier ist auf die reine Formatierung spezialisiert, während ESLint vor allem inhaltliche Prüfungen durchführt. -Trotzdem ist auch ESLint in der Lage, bestimmte Formatierungsregeln zu prüfen und zu korrigieren. -Damit beide Tools nicht gegeneinander arbeiten, sondern miteinander und unabhängig von der Reihenfolge der Ausführung immer zum gleichen Ergebnis führen, müssen wir bei der Installation und Konfiguration von Prettier einige Punkte beachten. -Die folgenden Schritte setzen voraus, dass ESLint bereits im Projekt eingerichtet ist. +Wir haben in diesem Kapitel die Grundlagen der Redux-Architektur und des Frameworks NgRx kennengelernt. Tatsächlich geht die Reise aber noch viel weiter! Wir möchten Ihnen in diesem Abschnitt einen Ausblick geben, welche Tools und Konzepte Ihnen dabei noch begegnen werden. -\footnote{Installation}Zunächst müssen wir Prettier mit den zugehörigen Plug-ins im Projekt installieren: +### Actions gruppieren mit `createActionGroup()` -\begin{clilisting}[style=nocaption] -npm install prettier prettier-eslint eslint-config-prettier eslint-plugin-prettier --save-dev -\end{clilisting} +Wir haben die Funktion `createAction()` kennengelernt, um einzelne Actions zu erstellen. Zum Laden der Buchliste haben wir auf diese Weise drei einzelne Actions definiert. Dafür war viel repetitiver Code notwendig: Wir mussten den Variablennamen definieren, das Präfix `[Book]` setzen und den Type der Action als String angeben. -\footnote{ESLint mit Prettier konfigurieren}Wir müssen als Nächstes einige Änderungen an der Datei \file{.\mbox{eslintrc}"".json} vornehmen. -Sie sind für ein harmonisches Zusammenspiel von Prettier und ESLint notwendig. -Details und Hintergründe dazu finden Sie in der Dokumentation zum Paket `angular-eslint`.\shortlinkFootnote{link-angular-eslint-prettier} +Um die Erzeugung der Actions zu vereinfachen, wurde mit NgRx 13.2 die Funktion `createActionGroup()` eingeführt. Damit können wir eine Gruppe von Actions mit einem gemeinsamen Präfix anlegen. Die drei Actions aus dem vorherigen Beispiel lassen sich also wie folgt definieren: -\pagebreakbeforelisting -\lstinputlisting[caption={ESLint-Konfiguration anpassen \textup{(\file{.eslintrc.json})}},label=lst:prettier:eslintrc]{\snippets/prettier/eslintrc.snippet.json} +```ts +// books/store/book.actions.ts +import { createActionGroup, emptyProps, props } from '@ngrx/store'; -Im Anschluss ist Prettier einsatzbereit und arbeitet Hand in Hand mit ESLint. -Wir können das Tool nun über die Kommandozeile aufrufen. -Damit der Befehl mit allen Argumenten nicht immer wieder per Hand eingegeben werden muss, empfehlen wir, ein NPM-Skript in der Datei \file{package.json} zu hinterlegen: +export const BookActions = createActionGroup({ + source: 'Book', + events: { + 'Load Books': emptyProps(), + 'Load Books Success': props<{ data: Book[] }>(), + 'Load Books Failure': props<{ error: string }>() + } +}); +``` -\pagebreakbeforelisting -\lstinputlisting[caption={NPM-Skript `format` \textup{(\file{package.json})}},label=lst:prettier:npmformat]{\snippets/prettier/package.snippet.json} +Die Besonderheit an diesem Ansatz ist, dass wir die Variablennamen nicht mehr von Hand notieren müssen. Wir können die Actions als z. B. `BookActions.loadBooks()` verwenden – der Name des Propertys wird automatisch aus dem Action Type ermittelt. Auf diese Weise sparen wir uns viel Tipparbeit. -Im Anschluss können wir Prettier wie folgt ausführen: +Bitte beachten Sie: Um die Actions zu verwenden, müssen wir nun immer das gesamte Objekt `BookActions` importieren, nicht die einzelnen Actions. In den Komponenten, Reducers und Effects sind also auch Anpassungen nötig, um diesen Ansatz zu nutzen. -\begin{clilisting}[style=nocaption] -npm run format -\end{clilisting} +### Routing -Um die Formatierung direkt im Editor Visual Studio Code durchzuführen, können wir die offizielle Extension für Prettier installieren: `esbenp.prettier-vscode`. -Diesen Namen können Sie in der Suche im Extension Browser von Visual Studio Code eingeben. +Der Anwendungszustand setzt sich nicht nur aus geladenen Daten und Einstellungen zusammen – die aktuell geladene Route und ihre Parameter gehören ebenfalls in den State. Beachtet man dies nicht, so sieht man gegebenenfalls zum richtigen State die falsche Komponente auf der Oberfläche. Deshalb sollten Informationen zum Routing auch im NgRx-Store abgelegt werden. Den passenden Adapter zwischen Router und Store erhalten wir mit dem Modul `@ngrx/router-store`: -Außerdem empfehlen wir, die folgenden Einstellungen in der Datei \file{.vscode/settings.json} im Projekt zu setzen: +```bash +ng add @ngrx/router-store +``` -\lstinputlisting[caption={Automatische Formatierung durch Visual Studio Code \textup{(\file{.vscode/settings.json})}},label=lst:prettier:vscsettings]{\snippets/prettier/settings.json} +Das Modul verfügt über eigene Actions und Reducers und kommuniziert direkt mit dem Router. Dadurch werden alle Aktivitäten des Routers mit Actions abgebildet. Es werden unter anderem die folgenden Actions ausgelöst: -Nun wird der Quellcode bei jedem Speichern der Datei mit Prettier formatiert. -Das ist während der Entwicklung besonders praktisch, weil Leerzeichen, Klammern und Einrückungen beim Tippen zunächst vernachlässigt werden können. Beim Speichern sorgt Prettier automatisch für die korrekte Formatierung. +- `routerRequestAction` +- `routerNavigationAction` +- `routerNavigatedAction` -\footnote{Prettier konfigurieren}Das Verhalten von Prettier kann konfiguriert werden, um den individuellen Stil des Teams zu berücksichtigen. -Dazu legen wir im Hauptverzeichnis des Projekts eine Datei mit dem Namen \file{.prettierrc} an. -Alle verfügbaren Optionen sind in der offiziellen Dokumentation zu finden.\shortlinkFootnote{link-prettier-config} +Die passenden Reducers fügen anschließend die Routeninformationen in den State ein, sodass wir z. B. die Routenparameter in den Komponenten, Reducers und Effects auslesen können. -%%% Local Variables: -%%% TeX-PDF-mode: t -%%% mode: latex -%%% TeX-master: "../../angularBuch" -%%% End: +### Entity Management +Verwalten wir Entitäten in unserem State (z. B. Bücher), so müssen wir in den Reducers für jede Entität wiederkehrende Routinen implementieren: Wir müssen eine Liste in den State schreiben und die Liste pflegen, indem wir Objekte einfügen, aktualisieren oder löschen. Bei der Suche nach einem bestimmten Objekt müssen wir stets durch die Liste laufen und dabei z. B. `Array.find()` verwenden. -================================================ -File: /tex/chapter/eap.tex -================================================ -\section*{EAP -- Early Access Programm} -Lieber \{\{ Leser \}\}, \\ -\\ -wir freuen uns sehr, dass du uns bei der Verbesserung unseres Buchs unterstützen möchtest. -Mit unserem Buch möchten wir der Community einen soliden Start mit Angular anbieten. -Wir legen großen Wert auf Verständlichkeit und Praxisnähe -- daher freuen wir uns über jedes Feedback von dir.\\ -\\ -Dies ist die Vorabversion der **dritten Auflage** des Buchs. -Wir haben viele Aktualisierungen vorgenommen und teilweise auch ganze Kapitel neu aufgerollt oder strukturiert. -Viele Inhalte befinden sich noch in Arbeit oder werden von uns überdacht. -Dieses Buch wird -- wie Angular -- stetig korrigiert, verbessert und erweitert, bis es seine endgültige Form erhalten hat. +Diese wiederkehrenden Aufgaben sind für fast alle Entitäten notwendig und verleiten zu redundantem Code. Um diese Zugriffe auf den State zu vereinfachen, existiert das Paket `@ngrx/entity`: -Die **Deadline** für die Abgabe beim Verlag ist der **12. Juni 2020**. -Bis dahin müssen wir alle Korrekturen eingepflegt haben. -Das Buch erscheint dann voraussichtlich im September im Buchhandel. +```bash +ng add @ngrx/entity +``` -### Einreichen von Feedback -Bei Ideen, Anregungen und oder Korrekturen sende bitte eine *E-Mail* an: +Anstatt die Entitäten selbst im State zu deklarieren, können wir die vorgegebene Struktur des Entity-Adapters nutzen. Jedes State-Interface, in dem Entitäten verwaltet werden, muss dazu das Interface `EntityState` erweitern: -\begin{center} - **\author{\href{mailto:trello@angular-buch.com**{trello@angular-buch.com}}}. -\end{center} +```ts +import { EntityState } from '@ngrx/entity'; -Am besten ist es, wenn Du Kommentare direkt in das PDF schreibst. -Du kannst aber gerne auch ein separates Textdokument anlegen und jeweils auf die Abschnittsnummer und die Seitenzahl verweisen. +export interface State extends EntityState { + loading: boolean +} +``` -\begin{center} - Viel Spaß beim Lesen und vielen Dank für deine Unterstützung! -\end{center} +Der `EntityState` gibt eine Datenstruktur vor, in der die Entitäten in einem Objekt abgelegt werden; als Schlüssel werden die IDs der Entitäten verwendet. Ein Objekt vereinfacht die Suche nach einer Entität mit einer bestimmten ID. Die geordnete Reihenfolge einer Liste geht allerdings bei einem Objekt verloren. Deshalb werden zusätzlich die IDs in einem Array im State abgelegt, um daraus die Reihenfolge jederzeit ablesen zu können. -### Bekannte Probleme +Durch den `EntityState` wird die folgende Struktur vorgegeben. Der Typ `Dictionary` stammt von NgRx und beschreibt das Objekt, in dem die Entitäten gespeichert sind. -\begin{itemize} -\item Die Nummerierung der Fußnoten ist teilweise fehlerhaft. -\item Es gibt zu große Abstände zu Überschriften, unschöne Seitenumbrüche oder leere Seiten. -\item Die automatische Silbentrennung erzeugt falsche Worttrennungen. -\item Tabellen und Abbildungen finden sich teilweise an unerwarteten Stellen wieder. -\end{itemize} +```ts +interface EntityState { + ids: string[] | number[]; + entities: Dictionary; +} +``` + +Damit wir die Daten in dieser Struktur nicht eigenhändig pflegen müssen, stellt das Modul einen Adapter zur Verfügung, den wir zusätzlich initialisieren müssen. Der Adapter nutzt automatisch das Property `id` als Primärschlüssel der Entität. Ist die ID allerdings in einem anderen Property enthalten, z. B. bei einem Buch die ISBN, so müssen wir eine Funktion `selectId` angeben, die die richtige ID auswählt. + +```ts +import { createEntityAdapter } from '@ngrx/entity'; + +export const adapter = createEntityAdapter({ + selectId: book => book.isbn // wenn ID nicht "id" ist +}); +``` + +Mithilfe des Adapters können wir zunächst den initialen Zustand erzeugen. Beinhaltet unser State noch weitere Eigenschaften, so geben wir als Argument ein Objekt an: + +```ts +export const initialState: State = + adapter.getInitialState({ + loading: false + }); +``` + +Die Mächtigkeit von `@ngrx/entity` entfaltet sich schließlich, wenn es um die Implementierung der Reducers geht. Dazu stellt der Adapter eine Reihe von Methoden zur Verfügung, mit denen wir die Kollektion von Entitäten im State verwalten können: + +| Methode | Beschreibung | +| --- | --- | +| `addOne` | eine Entität hinzufügen | +| `addMany` | mehrere Entitäten hinzufügen | +| `setAll` | Kollektion vollständig ersetzen | +| `setOne` | eine Entität hinzufügen oder ersetzen | +| `setMany` | mehrere Entitäten hinzufügen oder ersetzen | +| `removeOne` | eine Entität aus der Kollektion entfernen | +| `removeMany` | mehrere Entitäten aus der Kollektion entfernen (nach ID oder Prädikatsfunktion) | +| `removeAll` | Kollektion leeren | +| `updateOne` | eine Entität aktualisieren | +| `updateMany` | mehrere Entitäten aktualisieren | +| `upsertOne` | eine Entität hinzufügen oder aktualisieren (mit partiellen Änderungen) | +| `upsertMany` | mehrere Entitäten hinzufügen oder aktualisieren | +| `map` | Kollektion aktualisieren mithilfe einer Projektionsfunktion | + +Alle Funktionen erhalten den aktuellen State und die zu verarbeitenden Werte als Argument und erzeugen einen neuen State. Wir können den so erzeugten State also direkt aus dem Reducer zurückgeben oder verwenden, um einen komplexeren State mit weiteren Änderungen zu erzeugen: + +```ts +export const reducer = createReducer( + initialState, + + on(BookActions.createBook, (state, action) => { + return adapter.addOne(action.data, state); + }), + + on(BookActions.loadBooksSuccess, (state, action) => { + return { + ...adapter.setAll(action.data, state), + loading: false + }; + }) +); +``` + +Der EntityAdapter stellt außerdem eine Reihe von Selektoren zur Verfügung, die wir nutzen können, um die Kollektion auszulesen. Die Selektoren werden mithilfe von `adapter.getSelectors()` erzeugt, müssen dann allerdings noch in einzelne Selektoren verpackt werden, die den jeweiligen Feature-State als Grundlage nutzen: + +```ts +const { selectAll, selectEntities, + selectIds, selectTotal } = adapter.getSelectors(); + +export const selectAllBooks = createSelector( + selectBookState, selectAll +); + +export const selectBookEntities = createSelector( + selectBookState, selectEntities +); +``` -Fehler in der Ausrichtung und im Satz sind momentan gar nicht interessant -- es geht zunächst um den Inhalt! +Auf diese Weise können wir die Verwaltung von Entitäten im State sehr effizient durchführen. +### Testing +Alle auf Grundlage von NgRx entwickelten Bausteine sollten auch getestet werden. Dafür möchten wir Ihnen in diesem Abschnitt einige Hinweise geben. Grundsätzlich werden bei der Initialisierung mit den Schematics von NgRx bereits Grundgerüste für die Unit-Tests angelegt – Sie können also direkt loslegen. -%%% Local Variables: -%%% TeX-PDF-mode: t -%%% mode: latex -%%% TeX-master: "../angularBuch" -%%% End: +#### Actions +Die Action Creators beinhalten keine spezifische Logik, die getestet werden muss. In manchen Blogartikeln wird vorgeschlagen, die korrekte Zusammensetzung der Objekte zu prüfen, damit eine Action stets den richtigen `type` besitzt. Wir sind der Meinung, dass solche trivialen Tests keinen Mehrwert liefern und deshalb nicht notwendig sind. +#### Reducers +Ein Reducer ist eine Pure Function, liefert also für die gleiche Eingabe immer die gleiche klar vorhersehbare Ausgabe. Diese Eigenschaft kommt uns beim Testing zugute, denn wir können Reducers isoliert testen, ohne dass Angular dafür benötigt wird. Dazu rufen wir die Reducer-Funktion mit einem Startzustand und einer Action auf und prüfen den erzeugten neuen Zustand. -================================================ -File: /tex/chapter/vorwort/raetsel.tex -================================================ -\makeatletter -\@openrightfalse -\makeatother -\chapter*{Rätselzeit} % bitte nicht ins TOC +```ts +// books/store/book.reducer.spec.ts +import { loadBooks } from './book.actions'; +import { reducer } from './book.reducer'; -\vspace{-2cm} -\includegraphics[width=\textwidth]{images/raetsel.pdf} +describe('Book Reducer', () => { + it('should enable the loading flag for loadBooks', () => { + const state = { + books: [], + loading: false + }; + const action = loadBooks(); -\begin{flushleft} - \begin{tabular}{@{}rp{98mm}@{}} - **1** & Lifecycle-Methode, die bei Initialisierung der Komponente ausgeführt wird \\ - **2** & Teil der Template-Syntax, wird in runden Klammern notiert\\ - **3** & Programmiersprache von Angular\\ - **4** & Direktive zur Verlinkung zwischen Routen\\ - **5** & Sprachelement, mit dem z.\,B. eine Klasse als Komponente oder Injectable markiert wird, wird mit `@` notiert\\ - **6** & Baustein von Angular, um das Verhalten eines DOM-Elements zu ändern, besitzt aber kein eigenes Template\\ - **7** & Baustein von Reactive Forms, verwaltet mehrere Controls mit einer festen Reihenfolge\\ - **8** & Baustein aus RxJS, mit dem Multicasting realisiert werden kann, ist ein Hot Observable\\ - **9** & Konzept, um Bestandteile einer Anwendung zur Laufzeit nachzuladen, sobald sie benötigt werden\\ - **10** & Helfer im Template, um ein Datum zu formatieren\\ - \end{tabular} -\end{flushleft} + const newState = reducer(state, action); + expect(newState.loading).toBe(true); + }); +}); +``` -\suppressindent -Alle Begriffe müssen ohne Leerzeichen angegeben werden.\\**Viel Spaß beim Knobeln!** +#### Selektoren +Zum Testen von Selektoren gibt es verschiedene Herangehensweisen. Auch wenn ein Selektor mithilfe von `createSelector()` erstellt wurde, so ist er weiterhin nur eine Funktion, die den State als Argument erhält und Berechnungen über die Daten ausführt. Wir können also auch Selektoren isoliert testen, ohne dass wir einen Store oder das Angular-Framework benötigen. Dazu erstellen wir im Test ein State-Objekt, das alle benötigten Daten enthält. Wir wenden den Selektor darauf an und prüfen das Ergebnis. Wichtig ist, dass wir in diesem Stub stets die Struktur des Root-States abbilden, denn der ist ja auch der Ausgangspunkt eines jeden Selektors. -%%% Local Variables: -%%% TeX-PDF-mode: t -%%% mode: latex -%%% TeX-master: "../../angularBuch" -%%% End: +Die Hilfsfunktion `book()` haben wir selbst definiert. Sie generiert einfache Buch-Objekte mit Beispieldaten, sodass wir schnell Testdaten erzeugen können. +```ts +// books/store/book.selectors.spec.ts +import { selectAllBooks } from './book.selectors'; +import { book } from './test-helper'; + +describe('Book Selectors', () => { + it('should select all books', () => { + const books = [book(1), book(2), book(3)]; + const state = { + book: { books, loading: false } + }; + + const result = selectAllBooks(state); + expect(result).toEqual(books); + }); +}); +``` + +Bei komplexeren Selektoren, die mehrere andere Selektoren als Grundlage verwenden, liegt die kritische Logik in der Projektionsfunktion, die im letzten Argument von `createSelector()` angegeben wird. Es ist deshalb in den meisten Fällen ausreichend, nur diese Projektion zu testen. Zugriff auf die Funktion erhalten wir mit `selector.projector`: + +```ts +// books/store/book.selectors.spec.ts +describe('Book Selectors', () => { + it('should select all books', () => { + const books = [book(1), book(2), book(3)]; + const bookState = { books, loading: false }; + + const result = selectAllBooks.projector(bookState); + expect(result).toEqual(books); + }); +}); +``` + +#### Effects + +Effects sind nur schwierig isoliert zu testen, denn sie greifen auf verschiedene Abhängigkeiten aus der Anwendung zu. Dazu ist nicht nur das Modul `@ngrx/effects` nötig, sondern auch alle verwendeten HTTP-Services. Außerdem muss das Observable `actions$: Actions` mit einem Strom von Actions versorgt werden, und wir wollen keinen vollständigen Store aufsetzen. + +NgRx bietet dafür die Funktion `provideMockActions()`, die einen gemockten Strom von Actions bereitstellt. Tests für Effects müssen mithilfe von `TestBed` definiert werden, sodass wir die Dependency Injection von Angular nutzen können. Alle verwendeten Services müssen selbstverständlich auch durch Mocks oder Stubs ersetzt werden, damit keine echten HTTP-Requests ausgeführt werden. + +```ts +// books/store/book.effects.spec.ts +import { provideMockActions } from '@ngrx/effects/testing'; + +describe('BookEffects', () => { + let actions$: Observable; + let effects: BookEffects; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + BookEffects, + provideMockActions(() => actions$), + ] + }); + + effects = TestBed.inject(BookEffects); + }); +}); +``` + +Für die konkreten Tests müssen wir ein Observable von Actions bereitstellen und das ausgegebene Observable sowie ggf. die ausgeführten Seiteneffekte prüfen. Das folgende Listing zeigt einen vollständigen Testaufbau. Hier stellen wir sicher, dass der Effect die gewünschte Action ausgibt und die Daten wie gewünscht über den `BookStoreService` bezieht. + +```ts +// books/store/book.effects.spec.ts +describe('BookEffects', () => { + let actions$: Observable; + let effects: BookEffects; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + BookEffects, + provideMockActions(() => actions$), + { + provide: BookStoreService, + useValue: { getAll: () => of([]) } + } + ] + }); + + effects = TestBed.inject(BookEffects); + }); + + it('should fire loadBooksSuccess for loadBooks', () => { + const books = [book(1), book(2), book(3)]; + + // Implementierung von getAll() ersetzen + const bs = TestBed.inject(BookStoreService); + spyOn(bs, 'getAll').and.callFake(() => of(books)); + + // Action auslösen + actions$ = of(loadBooks()); + + // Actions aus Effect empfangen + let dispatchedAction: Action | undefined; + effects.loadBooks$.subscribe(action => { + dispatchedAction = action; + }); + + // Actions vergleichen + const expectedAction = loadBooksSuccess({ data: books }); + expect(dispatchedAction).toEqual(expectedAction); + + // Serviceaufruf prüfen + expect(bs.getAll).toHaveBeenCalled(); + }); +}); +``` + +Der Aufbau lässt sich vereinfachen, wenn wir nur die Datenströme miteinander vergleichen. Dazu eignet sich das Konzept des *Marble Testing*: Anstatt ein Observable wie üblich zu erzeugen und dann darauf zu subscriben, notieren wir den geplanten Datenstrom als Marble-Diagramm direkt im Test und definieren damit die Eingabe und die erwartete Ausgabe. Die technische Grundlage dafür bietet das Paket [jasmine-marbles](https://www.npmjs.com/package/jasmine-marbles). Das Projekt stellt auch den Matcher `toBeObservable()` zur Verfügung, mit dem wir in der Expectation direkt gegen das erzeugte Observable prüfen können: + +```ts +// books/store/book.effects.spec.ts +import { hot, cold } from 'jasmine-marbles'; + +describe('BookEffects', () => { + let actions$: Observable; + let effects: BookEffects; + + beforeEach(() => { /* ... */ }); + + it('should fire loadBooksSuccess for loadBooks', () => { + const books = [book(1), book(2), book(3)]; + const bs = TestBed.inject(BookStoreService); + spyOn(bs, 'getAll').and.callFake(() => of(books)); + + actions$ = hot('--a', { a: loadBooks() }); + const expected = cold('--b', { b: loadBooksSuccess({ data: books }) }); + + expect(effects.loadBooks$).toBeObservable(expected); + expect(bs.getAll).toHaveBeenCalled(); + }); +}); +``` + +#### Store + +Komponenten und Effects greifen häufig auf den Store zu. Unter anderem werden Actions in den Store gesendet (dispatcht), und der aktuelle Zustand wird mithilfe von Selektoren ermittelt. Wenn man für solche Komponenten oder Effects einen Unit-Test definieren will, benötigt man einen Ersatz für den echten Store. NgRx bietet uns hierfür die Funktion `provideMockStore()` an: Wir können damit einen gemockten Store registrieren, der einen von uns definierten Zustand besitzt. Dieser Zustand bleibt so lange unverändert, bis wir mittels `store.setState()` einen anderen Zustand setzen. Als Beispiel wollen wir den bekannten Effect zum Laden der Bücher so erweitern, dass er nur dann Bücher lädt, wenn die Buchliste im State noch leer ist. Hat das Array bereits Einträge, soll nichts passieren. + +Es ist für dieses Szenario notwendig, dass wir den bestehenden State im Effect berücksichtigen. Dies können wir mit dem Operator `concatLatestFrom()` aus dem Paket `@ngrx/effects` realisieren: Wir übergeben ein anderes Observable als Argument, und der Operator reichert den Hauptdatenstrom mit dem jeweils letzten Element aus diesem Observable an. Das bedeutet also, dass uns in den Effects neben dem Payload aus den Actions auch zusätzlich Daten aus dem Store zur Verfügung stehen. Wir verwenden hier direkt unseren Selektor `selectAllBooks`, um die aktuelle Buchliste aus dem Store zu erhalten. Anhand der Buchliste können wir dann mit dem Operator `filter()` entscheiden, ob die Bücher neu heruntergeladen werden sollen oder nicht. + +```ts +// books/store/book.effects.ts +import { Store } from '@ngrx/store'; +import { concatLatestFrom } from '@ngrx/effects'; +import { filter } from 'rxjs/operators'; + +@Injectable() +export class BookEffects { + + loadBooks$ = createEffect(() => { + return this.actions$.pipe( + ofType(BookActions.loadBooks), + concatLatestFrom(() => this.store.select(selectAllBooks)), + filter(([action, books]) => !books.length), + switchMap(() => + this.service.getAll().pipe( + map(data => BookActions.loadBooksSuccess({ data })), + catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) + ) + ) + ); + }); + + constructor( + private actions$: Actions, + private service: BookStoreService, + private store: Store + ) {} +} +``` + +Für diesen Effect sollten wir sicherstellen, dass bei einem leeren Array auch tatsächlich die Action `loadBooksSuccess` gefeuert wird – und dass dies bei einem gefüllten Array eben gerade nicht passiert. Mittels `provideMockStore()` stellen wir vor jeder Spezifikation zunächst den "vorgetäuschten" Store bereit. Für die zweite Spezifikation ändern wir den State anschließend noch einmal mittels `store.setState()` ab: + +```ts +// books/store/book.effects.spec.ts +import { provideMockStore, MockStore } from '@ngrx/store/testing'; +import { initialState } from './book.reducer'; + +describe('BookEffects', () => { + let actions$: Actions; + let effects: BookEffects; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + BookEffects, + provideMockActions(() => actions$), + provideMockStore({ + initialState: { book: initialState } + }), + { + provide: BookStoreService, + useValue: { + getAll: () => of([]) + } + } + ] + }); + + effects = TestBed.inject(BookEffects); + }); + + it('should fire loadBooksSuccess for loadBooks if store is empty', () => { + + actions$ = hot('--a', { a: loadBooks() }); + const expected = cold('--b', { b: loadBooksSuccess({ data: [] }) }); + + expect(effects.loadBooks$).toBeObservable(expected); + }); + + it('should do nothing if store is already filled', () => { + const store = TestBed.inject(MockStore); + store.setState({ + book: { + books: [book(1)], + loading: false + } + }); + + actions$ = hot('--a', { a: loadBooks() }); + const expected = cold('---'); + + expect(effects.loadBooks$).toBeObservable(expected); + }); +}); +``` + +Ebenso können wir mit dem `MockStore` die Rückgabewerte von Selektoren überschreiben. Unser letztes Beispiel zum Thema zeigt, wie wir sicherstellen können, dass die `BookListComponent` tatsächlich den Text *"Loading ..."* anzeigt, wenn im State das Loading-Flag entsprechend gesetzt ist. Es ist dabei nicht notwendig, den State im `MockStore` zu setzen, da wir auf diesen nicht direkt zugreifen. Die Komponente bezieht ihre Daten über die beiden Selektoren `selectAllBooks` und `selectBooksLoading`, die wir entsprechend "ausmocken": + +```ts +// books/book-list/book-list.component.spec.ts +import { provideMockStore } from '@ngrx/store/testing'; + +describe('BookListComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ BookListComponent ], + providers: [ + provideMockStore({ + selectors: [ + { selector: selectBooksLoading, value: true }, + { selector: selectAllBooks, value: [] }, + ] + }) + ] + }) + .compileComponents(); + }); + + it('should show a loading text', () => { + const fixture = TestBed.createComponent(BookListComponent); + fixture.detectChanges(); + const element = fixture.nativeElement; + expect(element.querySelector('.loader').textContent).toBe('Loading ...'); + }); +}); +``` + +Wie Sie sehen, stellt NgRx einige Hilfen und Konzepte bereit, um die einzelnen Bausteine effektiv zu testen. Vor allem die Funktionen `provideMockActions()` und `provideMockStore()` erleichtern uns das Testing für komplexe Szenarien in NgRx. -================================================ -File: /tex/chapter/vorwort/changelog-archiv.tex -================================================ +### Hilfsmittel für Komponenten: `@ngrx/component` -\section*{Aktualisierungen in der zweiten Auflage} +Das Paket `@ngrx/component` stellt eine Sammlung von nützlichen Helfern zur Verfügung, um mit reaktiven Datenströmen in Komponenten zu arbeiten. Die Werkzeuge sind dabei nicht auf den Einsatz mit NgRx beschränkt, sondern können auch unabhängig davon eingesetzt werden. Das Paket beinhaltet zwei elementare Bausteine: die Let-Direktive und die PushPipe. -### Neue Kapitel +Üblicherweise verwenden wir die AsyncPipe von Angular, um Observables direkt im Template einer Komponente aufzulösen. Das ist der Weg, den wir vor allem beim Einsatz von NgRx wählen, denn die Daten kommen bereits vollständig aufbereitet durch Observables in die Komponente. Die AsyncPipe kann in jedem Template-Ausdruck eingesetzt werden, also in der Interpolation, in Property Bindings und in Direktiven. Gemeinsam mit `ngIf` und der `as`-Syntax können wir das Ergebnis eines Observables so in einem DOM-Container verfügbar machen. -Folgende Kapitel und Abschnitte sind in der **zweiten Auflage** neu hinzugekommen: -\begin{itemize} - \item \changelogallref{ch:09interceptors} - \item \changelogallref{sec:universal} - \item \changelogallref{P_ww} \begin{itemize} - \item \changelogallref{sec:cli:schematics} - \item \changelogallref{sec:ww:containerpresentational} - \item \changelogallref{sec:ww:ngfortrackby} - \item \changelogallref{sec:ww:third-party} - \item \changelogallref{sub:ww:update} - \end{itemize} -\end{itemize} +```html +{{ title$ | async }} + -### Vollständig neu geschriebene Kapitel + + {{ myNumber }} + +``` -Einige bereits in der ersten Auflage existierende Kapitel wurden für die **zweite Auflage** vollständig neu aufgerollt: +So praktisch die AsyncPipe allerdings auch ist – sie birgt einige praktische Probleme. -\paragraph*{\changelogallref{sec:schnellstart}} -Der Schnellstart basierte in der ersten Auflage auf einer lokalen Lösung mit SystemJS und Paketen aus einem CDN. Der neue Schnellstart setzt auf die Online-Plattform StackBlitz zum schnellen Prototyping von Webanwendungen. +#### ngrxLet: Observables auflösen mit falsy Werten -\paragraph*{\changelogallref{ch:08rxjs}} Das Prinzip der reaktiven Programmierung und das Framework RxJS haben in den letzten Jahren weiter an Bedeutung gewonnen. Das alte Kapitel zu RxJS lieferte nur einen kurzen Überblick, ohne auf Details einzugehen. Mit dieser Neufassung finden Sie jetzt eine ausführliche Einführung in die Prinzipien von reaktiver Programmierung und Observables, und es werden alle wichtigen Konzepte anhand von Beispielen erklärt. Im Gegensatz zur ersten Auflage verwenden wir die neuen *Pipeable Operators*. +Die oben beschriebene Kombination von AsyncPipe und `ngIf` ist hilfreich, um die Daten aus dem Observable `numbers$` im Template verfügbar zu machen. Liefert das Observable allerdings einen *falsy* Wert – also `false`, `0`, `null`, `undefined`, `NaN` oder einen leeren String –, so wird der jeweilige Container durch `ngIf` gar nicht angezeigt. -\paragraph*{\changelogallref{subsection:formularverarbeitung}} In der ersten Auflage haben wir sowohl *Template-Driven Forms* als auch *Reactive Forms* gleichbedeutend vorgestellt. Wir empfehlen mittlerweile nicht mehr den Einsatz von Template-Driven Forms. Daher stellen wir zwar beide Ansätze weiterhin vor, legen aber im Kapitel zur Formularverarbeitung einen stärkeren Fokus auf Reactive Forms. Das Praxisbeispiel wurde neu entworfen, um eine saubere Trennung der Zuständigkeiten der Komponenten zu ermöglichen. Die Erläuterungen im Grundlagenteil wurden neu formuliert, um besser für die Anforderungen aus der Praxis geeignet zu sein. +Um dieses Problem zu umgehen, kann die Direktive `ngrxLet` eingesetzt werden. Sie löst ein Observable im Template auf und stellt die empfangenen Daten in einer lokalen Variable zur Verfügung. Im Gegensatz zu `ngIf` bleibt das DOM-Element dabei stets sichtbar und wird nicht ausgeblendet. Liefert `numbers$` also eine `0`, so wird der Container im folgenden Beispiel trotzdem angezeigt. Darüber hinaus erhält man mit `ngrxLet` bei Bedarf auch Zugriff auf mögliche Error- und Complete-Ereignisse des Observables. -\paragraph*{\changelogallref{sec:redux}} In den letzten zwei Jahren hat sich unserer Ansicht nach das Framework *NgRx* gegen weitere Frameworks wie *angular-redux* klar durchgesetzt. Während die erste Auflage in diesem Kapitel noch auf angular-redux setzte, arbeitet das Kapitel der zweiten Auflage durchgehend mit den *\foreignlanguage{english*Reactive Extensions for Angular (NgRx)}. Wir erarbeiten in der Einführung schrittweise ein Modell für zentrales State Management, um die Architektur von Redux zu erläutern, ohne eine konkrete Bibliothek zu nutzen. +```html + + {{ myNumber }} + -### Stark überarbeitete und erweiterte Kapitel + +

Fehler: {{ e }}

+
+``` -\paragraph*{\changelogallref{chapter:typescript}} Das Grundlagenkapitel zu TypeScript wurde neu strukturiert und behandelt zusätzlich auch neuere Features von ECMAScript/TypeScript, z.\,B. Destrukturierung, Spread-Operator und Rest-Syntax.{\tolerance2000\par} +#### PushPipe: Observables auflösen ohne Zone.js -\paragraph*{\changelogallref{ch:07http}} Das HTTP-Kapitel setzt durchgehend auf den `HttpClient`, der mit Angular 4.3 eingeführt wurde. -Dabei wird der Blick auch auf die erweiterten Features des Clients geworfen. Themen, die spezifisch für RxJS sind, wurden aus diesem Kapitel herausgelöst und werden nun im RxJS-Kapitel behandelt. +Die AsyncPipe von Angular hat den Vorteil, dass sie automatisch die betroffene Komponente als *dirty* markiert, sobald ein neuer Wert im Observable ausgegeben wird. Daraufhin wird diese Komponente beim nächsten Durchlauf der Change Detection geprüft, sodass die View aktualisiert wird. Durch diesen Mechanismus ist es möglich, in allen Komponenten die Strategie `OnPush` für die Change Detection zu aktivieren. Die AsyncPipe ist allerdings stets abhängig von der Bibliothek Zone.js, die für bestimmte Ereignisse in der Anwendung automatisch die Change Detection triggert. -\paragraph*{\changelogallref{sec:routing:resolver}} Resolver sind aus unserer Sicht nicht die beste Wahl, um reguläre Daten über HTTP nachzuladen. Der Iterationsschritt zu Resolvern wurde deshalb aus dem Beispielprojekt entfernt, und das Thema wird in dieser Auflage nur noch in der Theorie behandelt. +Im Buch beschreiben wir im Abschnitt zur Change Detection einen Weg, um Angular ohne Zone.js zu verwenden. Entwickelt man die Anwendung durchweg mit den Prinzipien der Reaktiven Programmierung, so kann es sinnvoll sein, auf die automatische Change Detection zu verzichten. Hier setzt die Pipe `ngrxPush` von NgRx an, die als Alternative zur AsyncPipe genutzt werden kann. Der elementare Unterschied: Die PushPipe triggert die Change Detection direkt, anstatt die Komponente nur für den nächsten Durchlauf als *dirty* zu markieren. Die PushPipe ist damit unabhängig von Zone.js und kann in einer *zoneless* Umgebung eingesetzt werden. Das kann sich positiv auf die Performance der Anwendung auswirken. -\paragraph*{\changelogallref{ch:i18n}} Die Möglichkeiten zur Konfiguration des Builds wurden mit Angular~6.0 stark vorangebracht. -Viele zuvor notwendige Kommando\-zeilen\-para\-meter sind nun nicht mehr notwendig, die Konfigurationsdatei \mbox{\file{angular.json}} löst diese ab. Dadurch konnten wir das Kapitel zur Internationalisierung (i18n) kürzen und verständlicher gestalten. Im Gegensatz zur ersten Auflage zeigen wir nicht mehr, wie man eine Anwendung im JIT-Modus internationalisiert, der hauptsächlich für die Entwicklung vorgesehen ist, aber nicht für produktive Anwendungen.{\tolerance5000\par} +Übrigens ermittelt die PushPipe automatisch, ob Zone.js in der Anwendung aktiviert ist, und kann dadurch auch das Verhalten der AsyncPipe annehmen. Auch die zuvor beschriebene Direktive `ngrxLet` baut auf derselben Grundlage auf und kann ohne Zone.js eingesetzt werden. -\paragraph*{\changelogallref{sec:testing}} Das Kapitel zum Testen von Angular-Anwendungen wurde stark erweitert. Neben den reinen Werkzeugen wird der Fokus besonders auf Philosophien, Patterns und Herangehensweisen gelegt. Zusätzlich werden die mitgelieferten Tools zum Testen von HTTP und Routing betrachtet. +```html +{{ title$ | ngrxPush }} -\paragraph*{\changelogallref{ww:change-detection}} Das Kapitel zur Change Detection wurde für besseres Verständnis neu strukturiert. Insbesondere wird auf Debugging und Strategien zur Optimierung eingegangen. + + + {{ myNumber }} + +``` -================================================ -File: /tex/chapter/vorwort/ueber.tex -================================================ -% {\parindent0pt -% \thispagestyle{empty}% -% \vspace*{90mm}% -% \hspace*{49mm}% -% \parbox{10em}{\centering{\itshape It's just \glq Angular\grq.}\par\nobreak\vskip1ex\relax Igor Minar}\par -% } +### Facades: Zustandsverwaltung abstrahieren -\Chapter*{Vorwort} -\ngcite{Angular is one of the most adopted frameworks on the planet.}{Brad Green}{ehem. Angular Engineering Director} +Um den Anwendungszustand mit NgRx zu verwalten, kommunizieren die Komponenten mit dem zentralen Store: Sie senden Actions in das System und lesen Daten mithilfe von Observables aus. Dafür müssen wir die Klasse `Store` als direkte Abhängigkeit in den Komponenten verwenden. Außerdem müssen wir die Actions und Selektoren importieren. -\noindent -Angular ist eines der populärsten Frameworks für die Entwicklung von Single-Page-Appli\-ka\-ti\-onen. -Das Framework wird weltweit von großen Unternehmen eingesetzt, um modulare, skalierbare und gut wart\-bare Applikationen zu entwickeln. -Mit Angular in Version~2.0.0 setzte Google im Jahr 2016 einen Meilenstein in der Welt der modernen Web\-entwick\-lung: -Das Framework nutzt die Programmiersprache TypeScript, bietet ein ausgereiftes Tooling und ermöglicht die komponentenbasierte Entwicklung von Single-Page-Anwendungen für den Browser und für Mobilgeräte. +Damit sind die Komponenten stets abhängig von den Bausteinen des Frameworks NgRx. Das erschwert insbesondere das Testing und die Wiederverwendbarkeit. Außerdem müssen wir uns für das State Management auf eine spezifische Lösung festlegen. Jeglicher State wird global im Store abgelegt, und ein Mischbetrieb mit anderen Ansätzen führt potenziell zu unsauberem Code in der Komponente. -In kurzer Zeit haben sich rund um Angular ein umfangreiches Ökosystem und eine vielfältige Community gebildet. -\footnote{React und Vue.js}Angular gilt neben \mbox{React.js} und \mbox{Vue.js} als eines der weltweit beliebtesten Web\-frame\-works. -Sie haben also die richtige Entscheidung getroffen, als Sie Angular für die Entwicklung Ihrer Projekte ins Auge gefasst haben. +Um diese Unschönheiten zu lösen, können wir eine *Facade* verwenden: Dafür erstellen wir einen Service, der eine Schnittstelle zwischen der Komponente und dem Store bildet. Die Komponente kommuniziert über Methoden und Propertys mit der Facade. Das Framework NgRx bleibt dabei verborgen, und wir halten die Komponente frei von frameworkspezifischem Code. -Der Einstieg in Angular ist umfangreich, aber die Konzepte sind durchdacht und konsequent. -\footnote{Opinionated Framework}Häufig verwendet man im Zusammenhang mit Angular das Attribut *opinionated*, das wir im Deutschen mit dem Begriff *meinungsstark* ausdrücken können: -Angular ist ein meinungsstarkes Framework, das viele klare Richtlinien zu Architektur, Codestruktur und Best Practices definiert. -Das kann zu Anfang umfangreich erscheinen, sorgt aber dafür, dass in der gesamten Community einheitliche Konventionen herrschen, Standardlösungen existieren und bestehende Bibliotheken vorausgewählt wurden. + -\footnote{Beispielanwendung}Sie werden in diesem Buch lernen, wie Sie mit Angular komponentenbasierte Single-Page-Applikationen erstellen. -Dazu entwickeln wir mit Ihnen gemeinsam eine Anwendung, anhand derer wir Ihnen die Konzepte und Features von Angular beibringen. -Wir führen Sie Schritt für Schritt durch das Framework – vom Pro\-jekt\-setup über Komponenten, Routing, Formulare und HTTP bis hin zum Testing und Deployment der Anwendung. -Auf dem Weg stellen wir Ihnen eine Reihe von Tools, Tipps und Best Practices vor, die wir in mehr als sechs Jahren Praxisalltag mit Angular sammeln konnten. -Die umfangreichen Theorie\-teile eignen sich auch später als Nachschlagewerk im Entwicklungs\-alltag. +Zur Implementierung nutzen wir einen einfachen Service, der mit dem Store kommuniziert. Als öffentliche Schnittstelle werden gut lesbare Methoden und Propertys angeboten. -Nach dem Lesen dieses Praxisbuchs sind Sie in der Lage, -\begin{itemize} - \item das Zusammenspiel der Funktionen von Angular sowie das Konzept hinter dem Framework zu verstehen, - \item modulare, strukturierte und wartbare Webanwendungen mithilfe von Angular zu entwickeln sowie - \item durch die Entwicklung von Tests qualitativ hochwertige Anwendungen zu erstellen. -\end{itemize} +```ts +@Injectable() +export class BooksFacade { + books$ = this.store.select(selectAllBooks); -Die Entwicklung mit Angular macht vor allem eines: *Spaß!* -Diesen Enthusiasmus für das Framework und für Webtechnologien möchten wir Ihnen in diesem Buch vermitteln – wir nehmen Sie mit auf die Reise in die Welt der modernen Webentwicklung! + constructor(private store: Store) {} -\section*{Versionen und Namenskonvention: Angular~vs.~AngularJS} + loadBooks() { + this.store.dispatch(loadBooks()); + } +} +``` +In der Komponente nutzen wir die Facade, um die Daten aus den Propertys zu lesen und Aktionen mithilfe der angebotenen Methoden auszulösen. -In diesem Buch dreht sich alles um das Framework Angular. -Die Geschichte dieses Projekts reicht zurück bis ins Jahr 2009 zur Vorgängerversion *AngularJS*. -Bis auf den ähnlichen Namen und einige Konzepte haben die beiden Frameworks aber nichts miteinander zu tun: -Angular ab Version~2 ist eine vollständige Neuentwicklung und ist nicht mit dem alten AngularJS kompatibel. +```ts +@Component({ /* ... */ }) +export class BookListComponent { + books$ = this.booksFacade.books$; -\footnote{It's just "`Angular"'.}Die offizielle Bezeichnung für das Framework ist *Angular*, ohne Angabe der Programmiersprache und ohne eine spezifische Versionsnummer. -Angular erschien im September 2016 in der Version~2.0.0 und hat viele neue Konzepte und Ideen in die Community gebracht. -Um Verwechslungen auszuschließen, gilt also die folgende Konvention: -\begin{itemize} - \item **Angular** – das Angular-Framework ab **Version~2 und höher** (dieses Buch ist durchgängig auf dem Stand von Angular~15) - \item **AngularJS** – das Angular-Framework in der **Version~1.x.x** -\end{itemize} - -AngularJS wird seit Januar 2022 offiziell nicht mehr weiterentwickelt und sollte nicht mehr verwendet werden.\shortlinkFootnote{link-version-support-status} -Sie haben also die richtige Entscheidung getroffen, Angular ab Version~2.0.0 einzusetzen. + constructor(private booksFacade: BooksFacade) { + this.booksFacade.loadBooks(); + } +} +``` -\footnote{Semantic Versioning}Die Versionsnummer~*x.y.z* basiert auf *Semantic Versioning*.\shortlinkFootnote{link-0} -Der Re\-lease-Zyklus von Angular ist kontinuierlich geplant: -Im Rhythmus von ungefähr sechs Monaten erscheint eine neue Major-Version~*x*. -Die Minor-Versionen *y* werden monatlich herausgegeben, nachdem eine Major-Version erschienen ist. -Jede Major-Version wird planmäßig für 1,5~Jahre unterstützt und weiterentwickelt (Long-Term Support). -\begingroup\renewcommand*{\thefigure}{\arabic{figure}} -\makeatletter\def\DP@pageref{1}\makeatother +Im Unit-Test können wir einen solchen Service leicht durch einen Stub ersetzen. Die Propertys und Methoden können dabei in einem einfachen Objekt nachgebildet werden, ohne die Schnittstelle des Stores ausmocken zu müssen. Im Buch haben wir im Kapitel zum Testing auf diese Weise einen Stub für den `BookStoreService` bereitgestellt. -\begin{widefig} - {fig:angular-timeline} - {Zeitleiste der Entwicklung von Angular} - \epsfig{width=\totalwidth, file=images/vorwort/timeline-angular} -\end{widefig} -\endgroup +```ts +const booksFacadeStub = { + books$: of([]), + loadBooks: () => {} +}; +``` -\vspace{-2\baselineskip} -\section*{Umgang mit Aktualisierungen} +Ein weiterer großer Vorteil dieser Idee liegt darin, dass wir die Implementierung für das State Management jederzeit austauschen oder variieren können. So ist es zum Beispiel möglich, bestimmte Teile des States rein lokal in der Facade zu verarbeiten und gar nicht an den zentralen NgRx Store zu übermitteln. Einen Ansatz für die Verwaltung von lokalem State bietet das Projekt `@ngrx/component-store`. Mit dem signal-basierten SignalStore stellen wir in [Teil 3 dieser Serie](/material/ngrx-signal-store) eine moderne Alternative dafür vor. -Das Release einer neuen Major-Version von Angular bedeutet keineswegs, dass alle Ideen verworfen werden und Ihre Software nach einem Update nicht mehr funktioniert. -Auch wenn Sie eine neuere Angular-Version verwenden, behalten die in diesem Buch beschriebenen Konzepte ihre Gültigkeit. -Die Grundideen von Angular sind seit Version~2.0.0 konsistent und auf Beständigkeit über einen langen Zeitraum ausgelegt. -Alle Updates zwischen den Major-Versionen waren in der Vergangenheit problemlos möglich, ohne dass Breaking Changes die gesamte Anwendung unbenutzbar machen. -Gibt es doch gravierende Änderungen, so werden stets ausführliche Informationen und Tools zur Migration angeboten. - -Auf der Website zu diesem Buch finden Sie die Codebeispiele für das Beispielprojekt und viele weiterführende Informationen. -Unter anderem veröffentlichen wir dort zu jeder Major-Version einen Artikel mit den wichtigsten Neuerungen in Angular. -Wir empfehlen Ihnen aus diesem Grund, unbedingt einen Blick auf die Begleitwebsite zu werfen, bevor Sie beginnen, sich mit den Inhalten des Buchs zu beschäftigen: - -\qrlink[Die Begleitwebsite zum~Buch]{https://angular-buch.com} - -\section*{An wen richtet sich das Buch?} - -\footnote{Erfahrung in Softwareentwicklung}Dieses Buch richtet sich an Menschen, die bereits grundlegende Kenntnisse in der Softwareentwicklung mitbringen. -Vorwissen zu JavaScript und HTML ist von Vorteil – es ist aber keine Voraussetzung, um mit diesem Buch Angular zu lernen. -Wenn Sie jedoch bereits mit der Webentwicklung vertraut sind, werden Sie mit diesem Buch schnell starten können. -Falls Sie gar keine Erfahrung in HTML und JavaScript mitbringen, empfehlen wir Ihnen, zunächst die grundlegenden Kenntnisse in diesen Bereichen zu festigen. - -\footnote{TypeScript}Für die Entwicklung mit Angular nutzen wir die populäre Programmiersprache TypeScript. -Doch keine Angst: TypeScript ist eine Erweiterung von JavaScript, und die Konzepte sind sehr eingängig und schnell gelernt. -Wenn Sie bereits eine stark typisierte Sprache wie Java oder C\# kennen, wird Ihnen der Einstieg in TypeScript nicht schwerfallen. - -Sie benötigen \footnote{Keine Angular-""Vorkenntnisse nötig!}*keinerlei* Vorkenntnisse im Umgang mit Angular bzw. AngularJS. -Ebenso müssen Sie sich nicht vorab mit benötigten Tools und Hilfsmitteln für die Entwicklung von Angular-Applikationen vertraut machen. -Das nötige Wissen darüber wird Ihnen in diesem Buch vermittelt. - -\footnote{Kein klassisches Nachschlagewerk}Wir erschließen uns die Welt von Angular praxisorientiert anhand eines Beispielprojekts. -Jedes Thema wird zunächst ausführlich in der Theorie behandelt, sodass Sie die Grundlagen auch losgelöst vom Beispielprojekt nachlesen können. -Wir wollen einen soliden Einstieg in Angular bieten, Best Practices zeigen und Schwerpunkte bei speziellen fortgeschrittenen Themen setzen. -Die meisten Aufgaben aus dem Entwicklungsalltag werden Sie mit den vielen praktischen Beispielen souverän meistern können. - -Wir hoffen, dass dieses Buch Ihre tägliche Begleitung bei der Arbeit mit Angular wird. -\footnote{Offizielle Angular-""Dokumentation}Für Details zu den einzelnen Framework-Funktionen empfehlen wir immer auch einen Blick in die offizielle Dokumentation.\shortlinkFootnote{link-1} - - - -\section*{Wie ist dieses Buch zu lesen?} - -\footnote{Einführung, Tools und Schnellstart}Im ersten Teil des Buchs lernen Sie die verwendeten Tools und die benötigten Werkzeuge kennen. -Im Schnellstart tauchen wir sofort in Angular ein und nehmen Sie mit zu einem einfachen Einstieg in das Framework und den Grundaufbau einer Anwendung. - -\footnote{Einführung in TypeScript}Der zweite Teil vermittelt Ihnen einen Einstieg in TypeScript. -Sie werden hier mit den Grundlagen dieser typisierten Skriptsprache vertraut gemacht. -Wenn Sie bereits Erfahrung im Umgang mit TypeScript haben, können Sie diesen Teil auch überspringen und bei Bedarf später einzelne Themen nachlesen. - -\footnote{Beispielanwendung}Der dritte Teil ist der Hauptteil des Buchs: -Hier werden wir mit Ihnen zusammen eine Beispielanwendung entwickeln. -Die Konzepte und Technologien von Angular wollen wir dabei direkt am Beispiel vermitteln. -\footnote{17~Praxiskapitel}Wir haben das Projekt in 17~einzelne Kapitel eingeteilt. -In jedem Teil setzen wir gemeinsam mit Ihnen neue Anforderungen und technische Aspekte im Beispielprojekt um. - -\begin{itemize}\raggedright - \item \nameref{ch:01comp} {\small (ab~S.~\pageref{ch:01comp})} - \item \nameref{ch:02prop} {\small (ab~S.~\pageref{ch:02prop})} - \item \nameref{ch:03eb} {\small (ab~S.~\pageref{ch:03eb})} - \item \nameref{ch:04modules} {\small (ab~S.~\pageref{ch:04modules})} - \item \nameref{ch:05di} {\small (ab~S.~\pageref{ch:05di})} - \item \nameref{ch:06routing} {\small (ab~S.~\pageref{ch:06routing})} - \item \nameref{ch:07http} {\small (ab~S.~\pageref{ch:07http})} - \item \nameref{ch:08rxjs} {\small (ab~S.~\pageref{ch:08rxjs})} - \item \nameref{ch:09interceptors} {\small (ab~S.~\pageref{ch:09interceptors})} - \item \nameref{ch:10tdf} {\small (ab~S.~\pageref{ch:10tdf})} - \item \nameref{ch:11rf} {\small (ab~S.~\pageref{ch:11rf})} - \item \nameref{ch:12val} {\small (ab~S.~\pageref{ch:12val})} - \item \nameref{ch:13pipes} {\small (ab~S.~\pageref{ch:13pipes})} - \item \nameref{ch:14directives} {\small (ab~S.~\pageref{ch:14directives})} - \item \nameref{ch:15lazy} {\small (ab~S.~\pageref{ch:15lazy})} - \item \nameref{ch:16guards} {\small (ab~S.~\pageref{ch:16guards})} - \item \nameref{ch:17standalone} {\small (ab~S.~\pageref{ch:17standalone})} -\end{itemize} - -Jedes dieser Kapitel besteht immer aus einem umfangreichen Theorieteil und der praktischen Implementierung im Beispielprojekt. -Neben fachlichen Themen führen wir Refactorings durch, die die Architektur oder den Codestil der Anwendung verbessern. -\footnote{Powertipps}In mehreren *Powertipps* zwischen den Kapiteln zeigen wir außerdem hilfreiche Werkzeuge, die uns bei der Entwicklung zur Seite stehen. - -\footnote{Projektübergreifende Themen}Nachdem alle Praxiskapitel erfolgreich absolviert wurden, widmen wir uns einer Auswahl von projektübergreifenden Themen: -Im Kapitel zu *Softwaretests* erfahren Sie, wie Sie Ihre Angular-Anwendung automatisiert testen und so die Softwarequalität sichern können. -Dieses Kapitel kann sowohl nach der Entwicklung des Beispielprojekts als auch parallel dazu bestritten werden. -Außerdem widmen wir uns ausführlich der *Barrierefreiheit*: In diesem Kapitel stellen wir Grundlagen und konkrete Maßnahmen vor, um die Anwendung für möglichst viele Menschen zugänglich zu machen. -Zum Schluss werfen wir einen differenzierten Blick auf die *Lokalisierung* und *Internationalisierung*, um die Anwendung für den mehrsprachigen Betrieb vorzubereiten. - - -\footnote{Deployment}Im fünften Teil des Buchs dreht sich alles um das Deployment einer Angular-Anwendung. -Dabei betrachten wir die Hintergründe und Konfiguration des Build-Prozesses und erläutern die Bereitstellung mithilfe von Docker. - -\footnote{Fortgeschrittene Themen: NgRx, SSR und~PWA}Im sechsten Teil möchten wir Ihnen einige Ansätze näherbringen, die über eine Standardanwendung hinausgehen. -Hier stellen wir zunächst das *Redux*-Pattern und das populäre Framework *NgRx* vor, mit dem wir den Anwendungszustand zentral und gut wartbar verwalten können. -Mit *Server-Side Rendering (SSR)* machen Sie Ihre Anwendung fit für Suchmaschinen und verbessern zusätzlich die wahrgenommene Geschwindigkeit beim initialen Start. -Außerdem werfen wir einen ausführlichen Blick auf *Progressive Web Apps (PWA)*, um eine Webanwendung elegant auf Mobilgeräten zu nutzen. -%Schließlich betrachten wir fortgeschrittene Aspekte der Angular CLI: Monorepos und Schematics. - -\footnote{Wissenswertes}Im letzten Teil Wissenswertes\ finden Sie weitere Informationen zu wissenswerten und begleitenden Themen. -Hier haben wir weiterführende Inhalte zusammengetragen, auf die wir im Beispielprojekt nicht ausführlich eingehen. +Für eine Facade empfehlen wir übrigens, keinen Tree-Shakable Provider zu verwenden. Stattdessen tragen wir die Facade unter `providers` in dem Modul ein, das auch den Feature-State bereitstellt. Auf diese Weise ist garantiert, dass der Feature-State korrekt registriert ist, wenn wir die Facade verwenden. +## Fazit -\section*{Selbst tippen statt Copy \& Paste} - -Der gesamte Code für das Beispielprojekt steht auf der Plattform GitHub zur Verfügung. -Wir wissen genau, wie groß die Versuchung ist, größere Teile des Codes von dort zu kopieren und so die Tipparbeit zu sparen. -\footnote{Abtippen heißt Lernen und Verstehen.}Aber: Kopieren und Einfügen ist nicht dasselbe wie *Lernen* und *Verstehen*. -Wenn Sie die Codebeispiele selbst *tippen*, werden Sie besser verstehen, wie Angular funktioniert, und werden die Software später erfolgreich in der Praxis einsetzen können. -Jeder einzelne Quelltext, den Sie abtippen, trainiert Ihre Hände, Ihr Gehirn und Ihre Sinne. -Wir möchten Sie deshalb ermutigen: Betrügen Sie sich nicht selbst. Der bereitgestellte Quelltext im Repository sollte lediglich der Überprüfung dienen. -Wir wissen, wie schwer das ist, aber vertrauen Sie uns: Es zahlt sich aus, denn Übung macht den Meister! +Mithilfe von Redux und NgRx können wir die Zustandsverwaltung in der Anwendung zentralisieren. Redux setzt auf ein unveränderliches Zustandsobjekt, das mithilfe von Pure Functions im Store verwaltet wird. Alle Ereignisse der Anwendung werden durch Actions signalisiert, die Änderungen am Zustand auslösen können. Dabei erzeugen die Reducers einen neuen Zustand, der über ein Observable an alle Interessierten ausgegeben wird. Jeder Baustein von NgRx hat eine klar definierte Aufgabe, was eine konsistente Struktur in das Projekt bringt. Die Teile der Architektur sind stark entkoppelt, sodass sie unabhängig voneinander entwickelt und gewartet werden können. -\section*{Angular.Schule: Workshops und Beratung} +Mit Redux lassen sich komplexe Strukturen in Projekten harmonisieren. Das bedeutet allerdings nicht, dass Redux für jede Anwendung die passende Architektur ist. Bewerten Sie deshalb für ein Projekt zunächst, ob sich der Einsatz von Redux wirklich lohnt. Die Thematik ist nicht trivial – um eine solche zentrale Zustandsverwaltung sicher und effektiv einzusetzen, braucht es Zeit und Übung. Die [offizielle Dokumentation von NgRx](https://ngrx.io/) ist ein guter Ausgangspunkt, um alle Funktionen und Konzepte weiter zu studieren. -Wir, die Autoren dieses Buchs, arbeiten seit Langem als Berater und Trainer für Angular. -Wir haben die Erfahrung gemacht, dass man Angular in kleinen Gruppen am effektivsten lernen kann. -In einem Workshop kann auf individuelle Fragen und Probleme direkt eingegangen werden – und es macht auch am meisten Spaß! +Zusätzlich haben wir das Beispiel aus diesem Kapitel [auf GitHub](https://github.com/book-monkey5/16e-ngrx) zur Verfügung gestellt, sodass Sie den Code vollständig nachvollziehen können. -Schauen Sie auf *\url{https://angular.schule*} vorbei. -Dort bieten wir Ihnen Angular-Schulungen in den Räumen Ihres Unternehmens, in offenen Gruppen oder als Online-Kurs an. -Das Angular-Buch verwenden wir dabei in unseren Kursen zur Nacharbeit. -Wir freuen uns auf Ihren Besuch! +--- +**Zurück zu Teil 1:** [Wie kommen wir zu zentralem State Management?](/material/ngrx-intro) · **Weiter zu Teil 3:** [SignalStore](/material/ngrx-signal-store) diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md new file mode 100644 index 00000000..4225901d --- /dev/null +++ b/material/ngrx-signal-store/README.md @@ -0,0 +1,22 @@ +--- +title: "State Management mit NgRx – Teil 3: SignalStore" +published: "2026-06-11" +lastModified: "2026-06-12" +hidden: true +--- + +**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (4. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** + +Dieser Artikel ist **Teil 3** einer dreiteiligen Serie zum Thema State Management mit NgRx. + +- Teil 1: Wie kommen wir zu zentralem State Management? → [zum Artikel](/material/ngrx-intro) +- Teil 2: Global Store mit NgRx → [zum Artikel](/material/ngrx-global-store) +- Teil 3: SignalStore (dieser Artikel) + +[[toc]] + +> **Hinweis:** Dieser Teil wird gerade geschrieben. Der SignalStore aus dem Paket `@ngrx/signals` stellt eine leichtgewichtige, signal-basierte Alternative zum Global Store vor. Wir zeigen ihn am selben Beispiel wie in Teil 2 (Buchliste laden mit Ladeindikator) und geben eine Entscheidungshilfe, wann sich welcher Ansatz lohnt. + +--- + +**Zurück zu Teil 2:** [Global Store mit NgRx](/material/ngrx-global-store) From 888a8d8df9614f48207c0d19f0790665cd3fb904 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Fri, 12 Jun 2026 17:11:33 +0200 Subject: [PATCH 05/24] material(ngrx): 4 Buch-Diagramme als SVG eingebunden - redux-flow-simple, selectors-gap, ngrx-flow-full, facade - aus den Buch-PDFs per 'pdftocairo -svg' nach SVG konvertiert (Vektor, kein Rasterbild), kein yEd noetig - TODO-Platzhalter durch echte Bild-Einbindungen mit beschreibendem Alt-Text ersetzt --- material/ngrx-global-store/README.md | 6 +- material/ngrx-global-store/facade.svg | 32 ++ material/ngrx-global-store/ngrx-flow-full.svg | 51 +++ material/ngrx-global-store/selectors-gap.svg | 431 ++++++++++++++++++ material/ngrx-intro/README.md | 2 +- material/ngrx-intro/redux-flow-simple.svg | 26 ++ 6 files changed, 544 insertions(+), 4 deletions(-) create mode 100644 material/ngrx-global-store/facade.svg create mode 100644 material/ngrx-global-store/ngrx-flow-full.svg create mode 100644 material/ngrx-global-store/selectors-gap.svg create mode 100644 material/ngrx-intro/redux-flow-simple.svg diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index d5a1aa20..1c7653ba 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -407,7 +407,7 @@ export class MyComponent { Das resultierende Observable gibt das Ergebnis nur aus, wenn der Wert sich tatsächlich verändert hat. - +![Diagramm: Der Store gibt bei jeder Änderung den gesamten State aus (loading wechselt false, true, true, false). Der Selektor store.select(state => state.book.loading) gibt darunter nur dann einen neuen Wert aus, wenn sich das loading-Flag tatsächlich ändert (false, true, false).](./selectors-gap.svg "Selektor mit store.select() verwenden") Wenn wir das Beispiel so implementieren, stoßen wir allerdings auf ein weiteres Problem: Die Struktur des States wird dynamisch verändert, wenn einzelne Feature-Module mithilfe von `forFeature()` einen Teilbaum des States registrieren. Durch eine statische Typanalyse allein ist es also nicht möglich, die vollständige Struktur des States zu ermitteln: Der TypeScript-Compiler weiß nicht, dass das Property `book` im State existiert. @@ -514,7 +514,7 @@ Mit Effects kommen wir in den Genuss des vollen Erlebnisses von reaktiver Progra Technisch ist ein Effect also immer ein `Observable`. Alle so erzeugten Actions werden automatisch in den Store dispatcht – darum kümmert sich das Framework. Effects werden in einer eigenen Klasse untergebracht, die wie ein Service aufgebaut ist: Sie trägt den Decorator `@Injectable()` und kann über ihren Konstruktor Abhängigkeiten anfordern. Damit die Effects funktionieren, muss die Klasse allerdings explizit registriert werden. Dazu muss jede Effects-Klasse in einem Modul mithilfe des `EffectsModule` eingebunden werden. Für das `BooksModule` wurde dieser Schritt bereits bei der Einrichtung erledigt: - +![Diagramm: Wie der einfache Redux-Fluss, aber erweitert um Effects. Eine Action triggert nicht nur den Reducer, sondern löst auch einen Effect aus. Der Effect ruft einen Service auf, der mit einer API kommuniziert, und löst seinerseits eine neue Action aus, die wieder in den Reducer fließt.](./ngrx-flow-full.svg "Datenfluss in NgRx mit Effects") ```ts // books/books.module.ts @@ -1203,7 +1203,7 @@ Damit sind die Komponenten stets abhängig von den Bausteinen des Frameworks NgR Um diese Unschönheiten zu lösen, können wir eine *Facade* verwenden: Dafür erstellen wir einen Service, der eine Schnittstelle zwischen der Komponente und dem Store bildet. Die Komponente kommuniziert über Methoden und Propertys mit der Facade. Das Framework NgRx bleibt dabei verborgen, und wir halten die Komponente frei von frameworkspezifischem Code. - +![Diagramm: Zwischen Component und Store liegt eine Facade. Die Component ruft Methoden der Facade auf (doThings()) und liest Daten über ein Observable (data$). Die Facade kommuniziert per dispatch() und select() mit dem Store, sodass die Component selbst keinen direkten Kontakt zu NgRx hat.](./facade.svg "NgRx Flow mit Facade") Zur Implementierung nutzen wir einen einfachen Service, der mit dem Store kommuniziert. Als öffentliche Schnittstelle werden gut lesbare Methoden und Propertys angeboten. diff --git a/material/ngrx-global-store/facade.svg b/material/ngrx-global-store/facade.svg new file mode 100644 index 00000000..54f47d90 --- /dev/null +++ b/material/ngrx-global-store/facade.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/material/ngrx-global-store/ngrx-flow-full.svg b/material/ngrx-global-store/ngrx-flow-full.svg new file mode 100644 index 00000000..36c14391 --- /dev/null +++ b/material/ngrx-global-store/ngrx-flow-full.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/material/ngrx-global-store/selectors-gap.svg b/material/ngrx-global-store/selectors-gap.svg new file mode 100644 index 00000000..b5164526 --- /dev/null +++ b/material/ngrx-global-store/selectors-gap.svg @@ -0,0 +1,431 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md index cc6e2cc1..4af9861f 100644 --- a/material/ngrx-intro/README.md +++ b/material/ngrx-intro/README.md @@ -323,7 +323,7 @@ Das State-Objekt im Store hat zwei elementare Eigenschaften: Es ist *immutable* Neben dem zentralen Store mit dem State-Objekt verwendet Redux zwei weitere wesentliche Bausteine: Alle fachlichen Ereignisse in der Anwendung werden mit Nachrichten abgebildet – im Kontext von Redux nennt man diese Nachrichten *Actions*. Eine Action wird von der Anwendung (z. B. von den Komponenten) in den Store gesendet (*dispatch*) und kann eine Zustandsänderung auslösen. Im Store werden die eingehenden Actions von *Reducers* verarbeitet. Diese Funktionen nehmen den aktuellen State und die neue Action als Grundlage und errechnen daraus den neuen State. Der Datenfluss in der Redux-Architektur ist in der folgenden Abbildung grafisch dargestellt. Hier ist gut erkennbar, dass die Daten stets in eine Richtung fließen und dass Lesen und Schreiben klar voneinander getrennt sind. - +![Diagramm: Die Component löst eine Action aus, die in den Store fließt. Dort triggert die Action einen Reducer, der einen neuen State erzeugt. Der State wird zurück an die Component gepusht. Die Daten fließen so im Kreis in eine Richtung.](./redux-flow-simple.svg "Datenfluss in Redux") Bringt man diese Bausteine in den Kontext des einführenden Beispiels, so entspricht der zentrale Service dem Store von Redux. Die gesendeten Nachrichten entsprechen den Actions. Die Funktion `calculateState()`, die wir zur Veranschaulichung verwendet haben, ist genauso aufgebaut wie die Reducers von Redux. Der Operator `scan()` ist tatsächlich auch die technische Grundlage des Frameworks NgRx, das wir in diesem Kapitel für das State Management nutzen werden. diff --git a/material/ngrx-intro/redux-flow-simple.svg b/material/ngrx-intro/redux-flow-simple.svg new file mode 100644 index 00000000..4dea6d76 --- /dev/null +++ b/material/ngrx-intro/redux-flow-simple.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + From c9e685e33110a3ca6e59c3f3e236d2df6a66b037 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Fri, 12 Jun 2026 19:02:53 +0200 Subject: [PATCH 06/24] material(ngrx): Diagramm-Beschriftungen auf selectSignal() modernisiert - selectors-gap, ngrx-flow-full und facade: Label 'select()' bzw. 'store.select(...)' durch 'selectSignal()' ersetzt - alte (als Vektor-Pfad vorliegende) Beschriftung in Hintergrundfarbe ueberdeckt und durch echten, editierbaren ersetzt - Bild-Alt-Texte/Titel entsprechend angepasst Hinweis: Der Fliesstext von Teil 2 nennt weiterhin store.select() (buchtreuer Port) - die Angleichung an selectSignal() erfolgt in der Modernisierung (Phase 2). --- material/ngrx-global-store/README.md | 4 ++-- material/ngrx-global-store/facade.svg | 1 + material/ngrx-global-store/ngrx-flow-full.svg | 1 + material/ngrx-global-store/selectors-gap.svg | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index 1c7653ba..92597824 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -407,7 +407,7 @@ export class MyComponent { Das resultierende Observable gibt das Ergebnis nur aus, wenn der Wert sich tatsächlich verändert hat. -![Diagramm: Der Store gibt bei jeder Änderung den gesamten State aus (loading wechselt false, true, true, false). Der Selektor store.select(state => state.book.loading) gibt darunter nur dann einen neuen Wert aus, wenn sich das loading-Flag tatsächlich ändert (false, true, false).](./selectors-gap.svg "Selektor mit store.select() verwenden") +![Diagramm: Der Store gibt bei jeder Änderung den gesamten State aus (loading wechselt false, true, true, false). Der Selektor store.selectSignal(state => state.book.loading) gibt darunter nur dann einen neuen Wert aus, wenn sich das loading-Flag tatsächlich ändert (false, true, false).](./selectors-gap.svg "Selektor mit store.selectSignal() verwenden") Wenn wir das Beispiel so implementieren, stoßen wir allerdings auf ein weiteres Problem: Die Struktur des States wird dynamisch verändert, wenn einzelne Feature-Module mithilfe von `forFeature()` einen Teilbaum des States registrieren. Durch eine statische Typanalyse allein ist es also nicht möglich, die vollständige Struktur des States zu ermitteln: Der TypeScript-Compiler weiß nicht, dass das Property `book` im State existiert. @@ -1203,7 +1203,7 @@ Damit sind die Komponenten stets abhängig von den Bausteinen des Frameworks NgR Um diese Unschönheiten zu lösen, können wir eine *Facade* verwenden: Dafür erstellen wir einen Service, der eine Schnittstelle zwischen der Komponente und dem Store bildet. Die Komponente kommuniziert über Methoden und Propertys mit der Facade. Das Framework NgRx bleibt dabei verborgen, und wir halten die Komponente frei von frameworkspezifischem Code. -![Diagramm: Zwischen Component und Store liegt eine Facade. Die Component ruft Methoden der Facade auf (doThings()) und liest Daten über ein Observable (data$). Die Facade kommuniziert per dispatch() und select() mit dem Store, sodass die Component selbst keinen direkten Kontakt zu NgRx hat.](./facade.svg "NgRx Flow mit Facade") +![Diagramm: Zwischen Component und Store liegt eine Facade. Die Component ruft Methoden der Facade auf (doThings()) und liest Daten über ein Observable (data$). Die Facade kommuniziert per dispatch() und selectSignal() mit dem Store, sodass die Component selbst keinen direkten Kontakt zu NgRx hat.](./facade.svg "NgRx Flow mit Facade") Zur Implementierung nutzen wir einen einfachen Service, der mit dem Store kommuniziert. Als öffentliche Schnittstelle werden gut lesbare Methoden und Propertys angeboten. diff --git a/material/ngrx-global-store/facade.svg b/material/ngrx-global-store/facade.svg index 54f47d90..3c4cb157 100644 --- a/material/ngrx-global-store/facade.svg +++ b/material/ngrx-global-store/facade.svg @@ -29,4 +29,5 @@ +selectSignal() diff --git a/material/ngrx-global-store/ngrx-flow-full.svg b/material/ngrx-global-store/ngrx-flow-full.svg index 36c14391..dd6dabec 100644 --- a/material/ngrx-global-store/ngrx-flow-full.svg +++ b/material/ngrx-global-store/ngrx-flow-full.svg @@ -48,4 +48,5 @@ +selectSignal() diff --git a/material/ngrx-global-store/selectors-gap.svg b/material/ngrx-global-store/selectors-gap.svg index b5164526..66ab0eb8 100644 --- a/material/ngrx-global-store/selectors-gap.svg +++ b/material/ngrx-global-store/selectors-gap.svg @@ -428,4 +428,5 @@ +store.selectSignal(state => state.book.loading) From cb17f468d71117e1e0cf45c1937e701b37c7d7d6 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Fri, 12 Jun 2026 19:07:01 +0200 Subject: [PATCH 07/24] material(ngrx): Teil 1 - Fussnote zur Nachrichten-Historie gerettet + TypeScript-Querverweis - A1: Hinweis-Box ergaenzt (Historie der Nachrichten wird in der Praxis nicht gespeichert) - war im Buch eine Fussnote, ging beim Port verloren - C: Verweis auf Spread-/Rest-Syntax als Link auf /material/typescript gesetzt; uebrige rein buchinterne Querverweise bleiben bewusst weg - lastModified aktualisiert --- material/ngrx-intro/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md index 4af9861f..09bf894d 100644 --- a/material/ngrx-intro/README.md +++ b/material/ngrx-intro/README.md @@ -1,7 +1,7 @@ --- title: "State Management mit NgRx – Teil 1: Wie kommen wir zu zentralem State Management?" published: "2026-06-11" -lastModified: "2026-06-11" +lastModified: "2026-06-12" hidden: true --- @@ -144,7 +144,7 @@ Das wollen wir ändern, indem wir das Objekt *unveränderlich* (engl. *immutable Dadurch ändert sich ein grundlegender Aspekt: Da Änderungen nicht mehr direkt am bisherigen Objekt möglich sind, werden wir gezwungen, das Objekt auszutauschen. Wir erzeugen hierfür bei jeder Änderung eine Kopie des vorherigen Objekts mit einer Ausnahme: dem zu ändernden Wert. Eine Änderung festzustellen ist nun sehr einfach: Wir müssen lediglich Referenzen vergleichen. Das ist kein Problem, da wir durch die Unveränderlichkeit sicher sein können, dass keine Änderung durch direkte Manipulation des Objekts möglich sein kann. Versehentliche Änderungen sind damit ebenfalls ausgeschlossen. -Für die meisten Anwendungsfälle benötigen wir allerdings gar keine echte Unveränderlichkeit! Es reicht im Prinzip schon aus, nur so zu tun, als wäre das Objekt unveränderlich, und dies konsequent beim Programmieren einzuhalten. Wir können hierfür den Spread-Operator nutzen und damit alle Eigenschaften kopieren. +Für die meisten Anwendungsfälle benötigen wir allerdings gar keine echte Unveränderlichkeit! Es reicht im Prinzip schon aus, nur so zu tun, als wäre das Objekt unveränderlich, und dies konsequent beim Programmieren einzuhalten. Wir können hierfür den Spread-Operator nutzen und damit alle Eigenschaften kopieren. Den Spread-Operator und die Rest-Syntax erklären wir ausführlich im Artikel [Einführung in TypeScript](/material/typescript). Im folgenden Listing demonstrieren wir die Verwendung. Die Methode `incrementCounter()` nutzt den Spread-Operator, um eine Kopie des vorherigen Objekts und damit eine neue Referenz zu erzeugen. Im selben Schritt schreiben wir den neuen Wert des Zählers in die Eigenschaft `counter`. @@ -301,6 +301,8 @@ Das Ergebnis ist das Observable `state$`, das für jede eintreffende Nachricht d Erneut haben wir unsere Zustandsverwaltung verbessert. Der Zustand ist nun aus den gesendeten Nachrichten abgeleitet. Ist die Historie aller Nachrichten bekannt, so kann man theoretisch jeden bisherigen Zustand jederzeit wieder reproduzieren, sofern unsere Reducer-Funktionen deterministisch sind. Diese Eigenschaften sorgen für ein sehr einfaches und gleichzeitig robustes System. Da die Funktionen sehr simpel sind, sind sie auch sehr einfach zu testen. +> **Hinweis:** Um jeden gewünschten Zustand wieder reproduzieren zu können, müsste man die Historie aller Nachrichten speichern. Das tun wir in diesem Beispiel nicht, und auch in der praktischen Anwendung von Redux wird das Protokoll der Nachrichten nicht gespeichert. + ### Zusammenfassung aller Konzepte Wir wollen die entwickelte Idee kurz zusammenfassen: From d74444520cc208b736803e748a6b130b77cbd784 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Fri, 12 Jun 2026 19:47:32 +0200 Subject: [PATCH 08/24] material(ngrx): Teil 2 fuer Angular 22 / NgRx modernisiert (Phase 2) Gegen die offizielle NgRx-Doku (ngrx.io, v21/v22) und das Schematic im Repo verifiziert: - Standalone statt NgModule: provideStore/provideState/provideEffects/ provideStoreDevtools in app.config.ts bzw. Route-providers; NgModule-Weg komplett entfernt - Store-Lesen mit store.selectSignal() (Signal) statt store.select() (Observable); Templates mit @if/@for statt *ngIf/*ngFor, keine AsyncPipe - Effects mit inject()-Feldern; funktionale Effects als gleichwertige Variante ergaenzt (entspricht dem aktuellen Effect-Blueprint, PR #5155) - useDefineForClassFields-Abschnitt entfernt (obsolet, Issue #3654) - concatLatestFrom aus @ngrx/operators (seit v18 umgezogen) - createFeature als moderne Reducer/Selektor-Buendelung ergaenzt - @ngrx/component als Maintenance-Modus eingeordnet - Versionsnote auf Angular 22; durchgaengig wir-Form statt Sie-Form --- material/ngrx-global-store/README.md | 503 ++++++++++++--------------- 1 file changed, 218 insertions(+), 285 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index 92597824..0d29a5dc 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -17,21 +17,17 @@ Dieser Artikel ist **Teil 2** einer dreiteiligen Serie zum Thema State Managemen Im ersten Teil haben wir uns Schritt für Schritt ein eigenes Modell für zentrales State Management erarbeitet und dabei die Grundprinzipien von Redux kennengelernt. Jetzt setzen wir diese Ideen mit dem Framework NgRx in die Praxis um. +> **Hinweis zur Version:** Wir verwenden in diesem Artikel **Angular 22** mit der dazu passenden NgRx-Version (NgRx folgt dem Major-Versionsschema von Angular). Wir setzen durchgängig auf Standalone APIs und Signals: Den Store richten wir ausschließlich über die `provide…`-Funktionen ein und lesen ihn mit `selectSignal()` als Signal aus. Konstruktor-Injection brauchen wir nicht mehr – auch das offizielle Effect-Schematic erzeugt seine Abhängigkeiten inzwischen mit `inject()`. + ## NgRx: Reactive Extensions for Angular -Das Framework *Reactive Extensions for Angular (NgRx)* ist eine der populärsten Implementierungen für State Management mit Angular. Durch die gezielte Ausrichtung auf Angular fügt sich der Code gut in die Strukturen und Lebenszyklen einer Angular-Anwendung ein. NgRx setzt stark auf die Möglichkeiten der reaktiven Programmierung mit RxJS, ist also an vielen Stellen von Observables und Datenströmen geprägt. Die große Community und eine Reihe von verwandten Projekten machen NgRx zum wohl bekanntesten Werkzeug für Zustandsverwaltung mit Angular. +Das Framework *Reactive Extensions for Angular (NgRx)* ist eine der populärsten Implementierungen für State Management mit Angular. Durch die gezielte Ausrichtung auf Angular fügt sich der Code gut in die Strukturen und Lebenszyklen einer Angular-Anwendung ein. NgRx setzt stark auf die Möglichkeiten der reaktiven Programmierung mit RxJS, ist also an vielen Stellen von Observables und Datenströmen geprägt. Über `selectSignal()` integriert es sich außerdem nahtlos in die signal-basierte Welt von modernem Angular. Die große Community und eine Reihe von verwandten Projekten machen NgRx zum wohl bekanntesten Werkzeug für Zustandsverwaltung mit Angular. Wir wollen in diesem Abschnitt die Struktur und die Bausteine in der Welt von NgRx genauer besprechen. Außerdem wollen wir im BookMonkey einen Aspekt mithilfe von NgRx umsetzen, um so alle Bausteine auch praktisch zu üben. ### Projekt vorbereiten -Als Grundlage für diesen praktischen Teil verwenden wir das Beispielprojekt BookMonkey in der Version nach dem Kapitel zu Guards, also bevor wir die Anwendung auf Standalone Components umgestellt haben. Möchten Sie mitentwickeln, so können Sie Ihr bestehendes BookMonkey-Projekt verwenden oder neu starten und den Code über GitHub herunterladen: - -```bash -git clone https://ng-buch.de/bm5-16-guards.git book-monkey-ngrx -cd book-monkey-ngrx -npm install -``` +Als Grundlage für diesen praktischen Teil verwenden wir das Beispielprojekt BookMonkey aus dem Buch. Wir bauen auf der Variante mit Standalone Components auf, wie sie die aktuelle Angular-Version vorgibt. Wer mitentwickeln möchte, kann das bestehende BookMonkey-Projekt verwenden. ### Store einrichten @@ -43,7 +39,9 @@ ng add @ngrx/store-devtools ng add @ngrx/effects ``` -Später wollen wir einen zusätzlichen Baustein kennenlernen, der im originalen Redux nicht vorgesehen ist und der spezifisch für NgRx ist: Effects auf Basis von `@ngrx/effects`. Deshalb haben wir das notwendige Paket in diesem Schritt gleich mit eingefügt. Die Store DevTools sind hilfreich zum Debugging der Anwendung – wir werden später im Abschnitt zu den Redux DevTools genauer darauf eingehen, um den Lesefluss in diesem Kapitel nicht zu unterbrechen. +Später wollen wir einen zusätzlichen Baustein kennenlernen, der im originalen Redux nicht vorgesehen ist und der spezifisch für NgRx ist: Effects auf Basis von `@ngrx/effects`. Deshalb haben wir das notwendige Paket in diesem Schritt gleich mit eingefügt. Die Store DevTools sind hilfreich zum Debugging der Anwendung – wir gehen später im Abschnitt zu den Redux DevTools genauer darauf ein, um den Lesefluss in diesem Kapitel nicht zu unterbrechen. + +Der Befehl `ng add` trägt die passenden `provide…`-Funktionen automatisch in die Datei `app.config.ts` ein. ### Schematics nutzen @@ -53,51 +51,42 @@ Um nach der Einrichtung die Bausteine von NgRx mithilfe der Angular CLI anzulege ng add @ngrx/schematics ``` -Die Schematics von NgRx werden durch diesen Aufruf automatisch im Projekt registriert. Jeder Aufruf von `ng generate` durchsucht dann auch die Skripte in diesem Paket. So können wir bequem einen Befehl wie `ng generate action` verwenden, ohne die Zielkollektion explizit angeben zu müssen. Die Collection wird mit einem Eintrag in der Datei `angular.json` festgelegt, den Sie jederzeit wieder löschen oder ändern können, falls Sie die Skripte von NgRx nicht mehr nutzen möchten. +Die Schematics von NgRx werden durch diesen Aufruf automatisch im Projekt registriert. Jeder Aufruf von `ng generate` durchsucht dann auch die Skripte in diesem Paket. So können wir bequem einen Befehl wie `ng generate action` verwenden, ohne die Zielkollektion explizit angeben zu müssen. Die Collection wird mit einem Eintrag in der Datei `angular.json` festgelegt, den wir jederzeit wieder löschen oder ändern können, falls wir die Skripte von NgRx nicht mehr nutzen möchten. ### Grundstruktur -Die ausgeführten Befehle haben bereits alles Nötige eingerichtet, sodass wir sofort mit der Implementierung beginnen können. Vorher wollen wir jedoch einen Blick auf die Änderungen werfen, die von den Schematics an unserem Projekt vorgenommen wurden. +Die ausgeführten Befehle haben bereits alles Nötige eingerichtet, sodass wir sofort mit der Implementierung beginnen können. Vorher wollen wir jedoch einen Blick auf die Einrichtung des Stores werfen. -Neben allen benötigten Abhängigkeiten in der `package.json` sind einige neue Imports im `AppModule` hinzugekommen. Das `StoreModule` bringt den Kern des NgRx-Stores in die Anwendung. Die verwendete Methode `forRoot()` erwartet zwei Argumente: Im ersten Objekt können wir angeben, welche Reducers für welchen Teil des State-Objekts verantwortlich sind (eine sogenannte `ActionReducerMap`). Üblicherweise nutzen wir dieses Objekt nicht, um die State-Struktur für unsere Features zu definieren, denn dafür existiert ein anderer, dynamischerer Weg. Verwenden wir allerdings das Paket `@ngrx/router-store`, so müssen wir das Mapping für den Router hier statisch im `AppModule` konfigurieren. Im zweiten Argument von `forRoot()` können wir ein Konfigurationsobjekt übergeben. Da wir diese beiden Aspekte momentan nicht nutzen wollen, sind lediglich zwei leere Objekte als Argumente angegeben. - -Außerdem sind zwei weitere Imports für `EffectsModule` und `StoreDevtoolsModule` eingetragen worden. Diese beiden Module binden Effects und die Store DevTools ein, wir wollen uns aber zu diesem Zeitpunkt noch nicht detaillierter damit auseinandersetzen. +In einer Standalone-Anwendung registrieren wir den Store über die Funktion `provideStore()` in der Datei `app.config.ts`. Sie bringt den Kern des NgRx-Stores in die Anwendung. Übergeben wir kein Argument, so startet der Store mit einem leeren State-Objekt – die einzelnen Features fügen ihre Zustände später selbst hinzu. Zusätzlich richten wir mit `provideEffects()` die Infrastruktur für Effects ein und binden mit `provideStoreDevtools()` die Store DevTools ein: ```ts -// app.module.ts -import { NgModule, isDevMode } from '@angular/core'; -import { StoreModule } from '@ngrx/store'; -import { StoreDevtoolsModule } from '@ngrx/store-devtools'; -import { EffectsModule } from '@ngrx/effects'; - -@NgModule({ - // ... - imports: [ +// app.config.ts +import { ApplicationConfig, isDevMode } from '@angular/core'; +import { provideStore } from '@ngrx/store'; +import { provideEffects } from '@ngrx/effects'; +import { provideStoreDevtools } from '@ngrx/store-devtools'; + +export const appConfig: ApplicationConfig = { + providers: [ // ... - StoreModule.forRoot({}, {}), - StoreDevtoolsModule.instrument( - { maxAge: 25, logOnly: !isDevMode() } - ), - EffectsModule.forRoot([]), - ], - // ... -}) -export class AppModule { } + provideStore(), + provideEffects(), + provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }) + ] +}; ``` -Mit dieser Konfiguration ist der Store zwar schon aktiv, aber wir haben noch nicht festgelegt, wie das zentrale State-Objekt strukturiert sein soll. Da wir auch mit NgRx modular entwickeln, setzen wir diese Änderungen allerdings nicht direkt im `AppModule` um. Stattdessen lagern wir alle neuen Bausteine in eigene Dateien aus und nutzen dafür die Angular CLI. - -> **NgRx und Standalone Components:** Im Zusammenhang mit Standalone Components kann es sein, dass die Anwendung kein `AppModule` besitzt, sondern die `AppComponent` direkt gebootstrappt wird. Damit wir in diesem Fall nicht die Module importieren müssen, bietet NgRx verschiedene Funktionen an, um den Store einzurichten: `provideStore()`, `provideEffects()` und `provideState()`. +Mit dieser Konfiguration ist der Store zwar schon aktiv, aber wir haben noch nicht festgelegt, wie das zentrale State-Objekt strukturiert sein soll. Da wir auch mit NgRx modular entwickeln, definieren wir die State-Struktur nicht zentral, sondern lagern alle neuen Bausteine in eigene Dateien aus und registrieren sie pro Feature. ### Feature anlegen -Unsere Anwendung ist bereits in Module strukturiert, die einzelne Features der Anwendung kapseln. Diese Einteilung findet sich auch wieder, wenn es um die Einrichtung des Stores für NgRx geht. Jedes Feature erhält einen eigenen Satz an Actions, Reducers und Effects, die auch nur für genau dieses Feature und den zugehörigen State zuständig sind. So verhindert man eine monolithische Struktur, in der verschiedene Zuständigkeiten ungewollt vermischt werden. Um NgRx für ein existierendes Feature-Modul aufzusetzen, verwenden wir den folgenden Befehl: +Unsere Anwendung ist bereits in Features strukturiert, die einzelne Bereiche kapseln und in der Regel per Lazy Loading geladen werden. Diese Einteilung findet sich auch wieder, wenn es um die Einrichtung des Stores für NgRx geht. Jedes Feature erhält einen eigenen Satz an Actions, Reducers und Effects, die auch nur für genau dieses Feature und den zugehörigen State zuständig sind. So verhindern wir eine monolithische Struktur, in der verschiedene Zuständigkeiten ungewollt vermischt werden. Um NgRx für ein bestehendes Feature aufzusetzen, verwenden wir den folgenden Befehl: ```bash -ng g feature books/store/book --module books/books --api --defaults +ng g feature books/store/book --api --defaults ``` -Dieser Aufruf legt das Feature `book` im Ordner `src/app/books/store` an. Wir haben hier bewusst den Unterordner `store` gewählt, um alle Bestandteile von NgRx sauber in einem gemeinsamen Unterordner zu gruppieren. Wichtig ist, dass der Feature-Name `book` hier im Singular angegeben wird, denn die CLI wird beim Anlegen automatisch ein Plural-s für einige Bausteine hinzufügen. Die nötigen Imports und die Verdrahtung in der Anwendung sollen in das zugehörige `BooksModule` integriert werden, auf das wir mit der Option `--module` verweisen. Mit der Option `--api` generieren wir außerdem das nötige Grundgerüst, um Daten zu behandeln, die von einer API abgerufen werden. Wie sich das auswirkt, werden wir gleich noch betrachten. Die letzte Option `--defaults` setzt weitere notwendige Einstellungen auf die vordefinierten Standardwerte. +Dieser Aufruf legt das Feature `book` im Ordner `src/app/books/store` an. Wir haben hier bewusst den Unterordner `store` gewählt, um alle Bestandteile von NgRx sauber in einem gemeinsamen Unterordner zu gruppieren. Wichtig ist, dass der Feature-Name `book` hier im Singular angegeben wird, denn die CLI fügt beim Anlegen automatisch ein Plural-s für einige Bausteine hinzu. Mit der Option `--api` generieren wir außerdem das nötige Grundgerüst, um Daten zu behandeln, die von einer API abgerufen werden. Wie sich das auswirkt, werden wir gleich noch betrachten. Die Dateistruktur in der Anwendung sieht nun wie folgt aus: @@ -111,34 +100,36 @@ src/ │ │ ├── book.selectors.ts │ │ ├── book.effects.ts │ │ └── ... - │ ├── books.module.ts + │ ├── books.routes.ts │ └── ... - ├── app.module.ts + ├── app.config.ts └── ... ``` -Neben neuen Dateien sind im `BooksModule` weitere Imports hinzugekommen. Hier wurde ebenfalls das `StoreModule` importiert, allerdings mit der Methode `forFeature()`. Außerdem wurde eine Effects-Klasse mit dem `EffectsModule` registriert. +Den Feature-State und die Effects registrieren wir dort, wo wir auch das Feature selbst bereitstellen – typischerweise im `providers`-Array der zugehörigen Route. Dazu nutzen wir `provideState()` für den Reducer und `provideEffects()` für die Effects-Klasse: ```ts -// books/books.module.ts -import { StoreModule } from '@ngrx/store'; +// books/books.routes.ts +import { Routes } from '@angular/router'; +import { provideState } from '@ngrx/store'; +import { provideEffects } from '@ngrx/effects'; + import * as fromBook from './store/book.reducer'; -import { EffectsModule } from '@ngrx/effects'; import { BookEffects } from './store/book.effects'; -@NgModule({ - // ... - imports: [ - // ... - StoreModule.forFeature(fromBook.bookFeatureKey, fromBook.reducer), - EffectsModule.forFeature([BookEffects]) - ] -}) -export class BooksModule { } +export const booksRoutes: Routes = [ + { + path: '', + providers: [ + provideState(fromBook.bookFeatureKey, fromBook.reducer), + provideEffects(BookEffects) + ], + // ... untergeordnete Routen + } +]; ``` -Dieser Aufruf von `forFeature()` ist essenziell, denn er definiert die Struktur des globalen State-Objekts. Die Konstante `fromBook.bookFeatureKey` aus der Datei `book.reducer.ts` enthält den String `book`. Damit wird festgelegt, unter welchem Namen die Zustände dieses Features im globalen State-Objekt zu finden sein werden. Das zentrale State-Objekt wird also durch diesen Aufruf von `forFeature()` automatisch erweitert, und die Reducers aus dem Feature werden in die Anwendung integriert. Diese dynamische Erweiterung ist nötig, damit ein Feature-Modul auch mithilfe von Lazy Loading asynchron zur Laufzeit nachgeladen werden kann. Der Feature-Key `book` verweist auf den Teilbaum im State-Objekt, in den der Feature-State eingebaut wird. - +Dieser Aufruf von `provideState()` ist essenziell, denn er definiert die Struktur des globalen State-Objekts. Die Konstante `fromBook.bookFeatureKey` aus der Datei `book.reducer.ts` enthält den String `book`. Damit legen wir fest, unter welchem Namen die Zustände dieses Features im globalen State-Objekt zu finden sein werden. Das zentrale State-Objekt wird also durch diesen Aufruf von `provideState()` automatisch erweitert, und die Reducers aus dem Feature werden in die Anwendung integriert. Diese dynamische Erweiterung ist nötig, damit ein Feature auch mithilfe von Lazy Loading asynchron zur Laufzeit nachgeladen werden kann. Der Feature-Key `book` verweist auf den Teilbaum im State-Objekt, in den der Feature-State eingebaut wird. ### Struktur des Feature-States definieren Nun folgt der erste inhaltliche Schritt auf dem Weg zum State Management: Wir müssen die Struktur des Feature-States für das Feature `book` definieren. Dazu befindet sich in der Datei `books/store/book.reducer.ts` ein Interface mit dem Namen `State`. Dieser Feature-State ist der erste Zweig des zentralen Objekt-Baums. @@ -149,6 +140,8 @@ Direkt darunter befindet sich die Variable `initialState`. Damit das System wei ```ts // books/store/book.reducer.ts +export const bookFeatureKey = 'book'; + export interface State { books: Book[]; loading: boolean; @@ -160,7 +153,7 @@ export const initialState: State = { }; ``` -Damit haben wir die Struktur unseres Feature-States definiert. Die restlichen Inhalte der Datei ignorieren wir zunächst, darum werden wir uns im übernächsten Schritt kümmern. Unser *gesamter* State der Anwendung hat jetzt den folgenden Aufbau: +Damit haben wir die Struktur unseres Feature-States definiert. Die restlichen Inhalte der Datei ignorieren wir zunächst, darum kümmern wir uns im übernächsten Schritt. Unser *gesamter* State der Anwendung hat jetzt den folgenden Aufbau: ```ts { @@ -171,7 +164,7 @@ Damit haben wir die Struktur unseres Feature-States definiert. Die restlichen In } ``` -Der Name `book` wird durch den Feature-Key definiert, den wir an den Aufruf von `forFeature()` übergeben haben. +Der Name `book` wird durch den Feature-Key definiert, den wir an den Aufruf von `provideState()` übergeben haben. ### Actions: Kommunikation mit dem Store @@ -197,7 +190,7 @@ Actions bilden die Grundlage für die Kommunikation mit dem Store und können Ä - Set language - Increment counter -Technisch ist eine solche Action immer ein Objekt mit einer bestimmten vorgegebenen Struktur. Verpflichtend ist die Eigenschaft `type`, die den Namen der Nachricht angibt und die Nachricht unverwechselbar macht. Jeder Type muss eindeutig sein und er darf immer nur einmal vergeben werden! Zusätzlich können weitere optionale Eigenschaften definiert werden, um Daten in der Action zu transportieren: der sogenannte Payload, der im folgenden Beispiel `data` genannt wird. +Technisch ist eine solche Action immer ein Objekt mit einer bestimmten vorgegebenen Struktur. Verpflichtend ist die Eigenschaft `type`, die den Namen der Nachricht angibt und die Nachricht unverwechselbar macht. Jeder Type muss eindeutig sein und darf immer nur einmal vergeben werden! Zusätzlich können weitere optionale Eigenschaften definiert werden, um Daten in der Action zu transportieren: der sogenannte Payload, der im folgenden Beispiel `data` genannt wird. ```ts { @@ -208,7 +201,7 @@ Technisch ist eine solche Action immer ein Objekt mit einer bestimmten vorgegebe Um eine starke Typisierung zu ermöglichen und Tippfehler zu vermeiden, notieren wir die Objekte jedoch nicht direkt im Code. Stattdessen nutzen wir einen sogenannten *Action Creator* – eine Funktion, die das Objekt mit der richtigen Struktur erzeugt. -Dafür stellt NgRx die Funktion `createAction()` zur Verfügung. Als erstes Argument geben wir hier immer den Namen der Action an. Dieser `type` muss in der gesamten Anwendung eindeutig sein. Um die Nachvollziehbarkeit zu erhöhen und mögliche Kollisionen zu verhindern, wird üblicherweise die Quelle der Action in eckigen Klammern im Namen notiert. Damit erzeugen wir eine Art Namespace für den Action-Typ – es handelt sich dabei aber nur um eine Konvention. +Dafür stellt NgRx die Funktion `createAction()` zur Verfügung. Als erstes Argument geben wir hier immer den Namen der Action an. Dieser `type` muss in der gesamten Anwendung eindeutig sein. Um die Nachvollziehbarkeit zu erhöhen und mögliche Kollisionen zu verhindern, notieren wir üblicherweise die Quelle der Action in eckigen Klammern im Namen. Damit erzeugen wir eine Art Namespace für den Action-Typ – es handelt sich dabei aber nur um eine Konvention. ```ts import { createAction } from '@ngrx/store'; @@ -229,7 +222,7 @@ export const loadBooksSuccess = createAction( ); ``` -Der generische Typparameter der Funktion `props()` gibt an, welche zusätzlichen Eigenschaften die Action enthalten soll. Wir haben hier den generischen Namen `data` gewählt, Sie können die Payload-Propertys allerdings nach Belieben benennen. Das erzeugte Action-Objekt hat den folgenden Aufbau: +Der generische Typparameter der Funktion `props()` gibt an, welche zusätzlichen Eigenschaften die Action enthalten soll. Wir haben hier den generischen Namen `data` gewählt, die Payload-Propertys können wir allerdings nach Belieben benennen. Das erzeugte Action-Objekt hat den folgenden Aufbau: ```ts { @@ -238,7 +231,7 @@ Der generische Typparameter der Funktion `props()` gibt an, welche zusätzlichen } ``` -Zur Abfrage einer HTTP-Schnittstelle benötigt man normalerweise drei zusammengehörige Actions, die diesem Muster folgen: +Zur Abfrage einer HTTP-Schnittstelle benötigen wir normalerweise drei zusammengehörige Actions, die diesem Muster folgen: - `Load XXX`: Daten anfragen - `Load XXX Success`: Daten sind erfolgreich vom Server eingetroffen. @@ -271,11 +264,11 @@ export const loadBooksFailure = createAction( ### Dispatch: Actions in den Store senden -Um mit dem Store zu kommunizieren und Zustandsänderungen anzustoßen, müssen die Actions von den Komponenten in den Store gesendet werden. Der Store kann dazu als Service in die Komponente injiziert werden. +Um mit dem Store zu kommunizieren und Zustandsänderungen anzustoßen, müssen die Actions von den Komponenten in den Store gesendet werden. Den Store fordern wir dazu mit der Funktion `inject()` in der Komponente an. Der Store verfügt über eine Methode `dispatch()`, mit der wir eine Action in den Store dispatchen können. Beim Aufruf der `BookListComponent` soll das Laden der Buchliste angestoßen werden. Deshalb lösen wir dort gleich im Konstruktor die Action `loadBooks` aus. -Wichtig ist, dass das exportierte `loadBooks` aus der Datei `book.actions.ts` selbst noch keine Action ist, sondern ein Action Creator, der ein Action-Objekt erzeugen kann. Dazu muss die Funktion aufgerufen werden. Hat die Action einen Payload, so wird er als Argument an den Action Creator übergeben: +Wichtig ist, dass das exportierte `loadBooks` aus der Datei `book.actions.ts` selbst noch keine Action ist, sondern ein Action Creator, der ein Action-Objekt erzeugen kann. Dazu müssen wir die Funktion aufrufen. Hat die Action einen Payload, so übergeben wir ihn als Argument an den Action Creator: ```ts const myAction = loadBooks(); @@ -286,26 +279,26 @@ Wir müssen die Funktion `loadBooks` also aufrufen, um ein Action-Objekt zu erha ```ts // books/book-list/book-list.component.ts +import { Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; + import { loadBooks } from '../store/book.actions'; @Component({ /* ... */ }) export class BookListComponent { - // ... - constructor( - private service: BookStoreService, - private store: Store - ) { + private store = inject(Store); + + constructor() { this.store.dispatch(loadBooks()); } } ``` -Wenn Sie bereits die Redux DevTools installiert haben, so können Sie nun überprüfen, ob die ausgelöste Action tatsächlich im Store eingetroffen ist. Wir betrachten die DevTools separat im Abschnitt zu den Redux DevTools weiter unten. +Wenn wir bereits die Redux DevTools installiert haben, können wir nun überprüfen, ob die ausgelöste Action tatsächlich im Store eingetroffen ist. Wir betrachten die DevTools separat im Abschnitt zu den Redux DevTools weiter unten. ### Reducers: den State aktualisieren -Nachdem wir die erste Action in den Store dispatcht haben, ist es nun an der Zeit, einen Reducer zu entwickeln, um den State zu verändern. Ein Reducer im Kontext von Redux ist eine Funktion mit zwei Eingabewerten: der aktuelle Zustand und die neu eintreffende Action. Die Aufgabe des Reducers ist es, anhand der Action und des Zustands einen neuen Zustand zu berechnen und zurückzugeben: +Nachdem wir die erste Action in den Store dispatcht haben, ist es nun an der Zeit, einen Reducer zu entwickeln, um den State zu verändern. Ein Reducer im Kontext von Redux ist eine Funktion mit zwei Eingabewerten: dem aktuellen Zustand und der neu eintreffenden Action. Die Aufgabe des Reducers ist es, anhand der Action und des Zustands einen neuen Zustand zu berechnen und zurückzugeben: ```ts function reducer(state: State, action: Action): State {} @@ -319,15 +312,15 @@ Die wichtigste Eigenschaft der Reducer-Funktionen ist ihre "Reinheit": Reducers - **deterministisch:** Die Funktion liefert für gleiche Eingabewerte stets die gleiche Ausgabe. Es dürfen also keine Werte verarbeitet werden, die nicht zweifelsfrei aus den Eingaben ableitbar sind, z. B. Zufallswerte oder die Uhrzeit. - **keine äußeren Zustände:** Es werden nur die Daten verarbeitet, die als Argumente an die Funktion übergeben werden (also hier: State und Action). Es darf nicht auf andere Variablen zugegriffen werden, die außerhalb der Funktion liegen. Eine Ausnahme bilden ausgelagerte Hilfsfunktionen, die allerdings dieselben Anforderungen an eine Pure Function erfüllen müssen. -- **keine Seiteneffekte:** Die Funktion darf keine Aktionen ausführen, die einen Effekt außerhalb ihres Gültigkeitsbereichs haben. HTTP-Requests, Logging, Authentifizierung oder Dispatchen von Actions sind also in den Reducers *nicht* erlaubt. Zu Seiteneffekten zählt auch, das State-Objekt direkt zu manipulieren! +- **keine Seiteneffekte:** Die Funktion darf keine Aktionen ausführen, die einen Effekt außerhalb ihres Gültigkeitsbereichs haben. HTTP-Requests, Logging, Authentifizierung oder das Dispatchen von Actions sind also in den Reducers *nicht* erlaubt. Zu Seiteneffekten zählt auch, das State-Objekt direkt zu manipulieren! Die Einhaltung dieser Einschränkungen ist besonders wichtig, um eine hohe Stabilität des Systems sicherzustellen. Nur wenn wir den strikten Regeln von Redux folgen, kann der Anwendungszustand zuverlässig kontrolliert werden. Ein Reducer darf deshalb ausschließlich den aktuellen State und die eintreffende Action verarbeiten. Alle notwendigen Informationen, um den neuen State zu erzeugen, müssen in State oder Action vorliegen. Außerdem muss der Reducer bei Änderungen stets eine *Kopie* des States zurückgeben, die die gewünschten Änderungen beinhaltet. Es dürfen niemals Änderungen direkt auf dem Objekt ausgeführt werden. Diese Eigenschaft der Immutability haben wir bereits in der Einleitung besprochen. Wir setzen mit NgRx in der Regel nicht auf "echte Unveränderlichkeit", sondern wenden Disziplin an und behandeln die Objekte lediglich als unveränderlich – auch wenn sie prinzipiell veränderlich sind. Während der Entwicklung sind außerdem sogenannte [Runtime Checks](https://ngrx.io/guide/store/configuration/runtime-checks) aktiv, die den State auf Unveränderlichkeit und Serialisierbarkeit prüfen. Jede versehentliche Änderung am State führt dann direkt zu einer Exception. -Um das State-Objekt vor der Verwendung und Änderung zu klonen, können wir den Spread-Operator einsetzen. Bitte beachten Sie, dass dieses Werkzeug stets nur eine flache Kopie (Shallow Copy) erzeugt. Wollen wir Änderungen an tiefer verzweigten Teilen des States vornehmen, müssen wir explizit eine tiefe Kopie (Deep Copy) des Objekts erzeugen. +Um das State-Objekt vor der Verwendung und Änderung zu klonen, können wir den Spread-Operator einsetzen. Wir beachten dabei, dass dieses Werkzeug stets nur eine flache Kopie (Shallow Copy) erzeugt. Wollen wir Änderungen an tiefer verzweigten Teilen des States vornehmen, müssen wir explizit eine tiefe Kopie (Deep Copy) des Objekts erzeugen. -> **Hinweis:** Ein verschachteltes Objekt kann mit dem Spread-Operator geklont werden, indem wir jeden Zweig des Objekts einzeln kopieren. Wird das zu komplex, empfehlen wir Ihnen, ein Hilfsmittel zu verwenden wie die native Funktion `structuredClone()`. Wir haben die verschiedenen Möglichkeiten in einem Blogartikel zusammengefasst: [10 useful operations for pure & immutable data structures](https://angular.schule/blog/2018-03-pure-immutable-operations). +> **Hinweis:** Ein verschachteltes Objekt können wir mit dem Spread-Operator klonen, indem wir jeden Zweig des Objekts einzeln kopieren. Wird das zu komplex, empfiehlt sich ein Hilfsmittel wie die native Funktion `structuredClone()`. Wir haben die verschiedenen Möglichkeiten in einem Blogartikel zusammengefasst: [10 useful operations for pure & immutable data structures](https://angular.schule/blog/2018-03-pure-immutable-operations). Wir wollen nun auch für den BookMonkey passende Reducers entwickeln. Dazu überlegen wir zunächst, welche Zustandsänderungen von den Actions ausgelöst werden: @@ -335,7 +328,7 @@ Wir wollen nun auch für den BookMonkey passende Reducers entwickeln. Dazu über - `loadBooksSuccess`: Buchliste einfügen und Ladeindikator auf `false` setzen - `loadBooksFailure`: Ladeindikator auf `false` setzen -Die Implementierung bringen wir in der Datei `books/store/book.reducer.ts` unter, in der das Grundgerüst der Reducer-Funktion bereits vorbereitet ist. Statt *switch/case* wird hier die Funktion `createReducer()` genutzt. Für jede Fallunterscheidung existiert ein Block, der in ein `on()` gekapselt ist. Als erstes Argument geben wir hier immer die Action an, die behandelt werden soll. Im zweiten Argument ist die Reducer-Funktion notiert, die schließlich anhand der eingehenden Action den neuen State generiert. Für die einzelnen Reducer-Funktionen sollten wir den Body in geschweiften Klammern definieren und von dort das State-Objekt direkt returnen. Außerdem sollte der Rückgabetyp der Funktion immer explizit mit `State` definiert sein. +Die Implementierung bringen wir in der Datei `books/store/book.reducer.ts` unter, in der das Grundgerüst der Reducer-Funktion bereits vorbereitet ist. Statt *switch/case* wird hier die Funktion `createReducer()` genutzt. Für jede Fallunterscheidung existiert ein Block, der in ein `on()` gekapselt ist. Als erstes Argument geben wir hier immer die Action an, die behandelt werden soll. Im zweiten Argument ist die Reducer-Funktion notiert, die schließlich anhand der eingehenden Action den neuen State generiert. Für die einzelnen Reducer-Funktionen sollten wir den Body in geschweiften Klammern definieren und von dort das State-Objekt direkt zurückgeben. Außerdem sollte der Rückgabetyp der Funktion immer explizit mit `State` definiert sein. ```ts // books/store/book.reducer.ts @@ -354,70 +347,31 @@ export const reducer = createReducer( }; }), - on(BookActions.loadBooksFailure, (state, action): State => { + on(BookActions.loadBooksFailure, (state): State => { return { ...state, loading: false }; - }), + }) ); ``` -> **Reducers für mehrere Actions:** Übrigens wird jeder Reducer immer für jede Action durchlaufen. Das bedeutet, dass Sie in einem Reducer auch auf mehrere Actions oder sogar auf Actions aus einem anderen Bereich der Anwendung reagieren können. Dazu können Sie im `on()` mehrere Actions nacheinander als einzelne Argumente angeben. Es gibt jedoch keine direkte Möglichkeit, in einem Reducer auf einen anderen Slice des States zuzugreifen. (Um in einem Reducer einen anderen State-Slice zu lesen, müsste zunächst die Art und Weise geändert werden, wie NgRx die verschiedenen Reducers intern zusammenfasst. Die Funktion `combineReducers()` bietet dafür einen Ansatz.) +> **Reducers für mehrere Actions:** Übrigens wird jeder Reducer immer für jede Action durchlaufen. Das bedeutet, dass wir in einem Reducer auch auf mehrere Actions oder sogar auf Actions aus einem anderen Bereich der Anwendung reagieren können. Dazu geben wir im `on()` mehrere Actions nacheinander als einzelne Argumente an. Es gibt jedoch keine direkte Möglichkeit, in einem Reducer auf einen anderen Slice des States zuzugreifen. -Haben Sie die Implementierung abgeschlossen, so können Sie die State-Änderung in den Redux DevTools nachvollziehen. Für die dispatchte Action `loadBooks` ändert sich das `loading`-Flag im State von `false` auf `true`. +Haben wir die Implementierung abgeschlossen, so können wir die State-Änderung in den Redux DevTools nachvollziehen. Für die dispatchte Action `loadBooks` ändert sich das `loading`-Flag im State von `false` auf `true`. -Die Action `loadBooksSuccess` wird aktuell niemals ausgelöst; das werden wir im übernächsten Schritt mit einem Effect lösen. Wir haben trotzdem bereits definiert, was in diesem Fall mit dem State passieren soll. Dasselbe gilt für `loadBooksFailure`. +Die Action `loadBooksSuccess` wird aktuell niemals ausgelöst; das lösen wir im übernächsten Schritt mit einem Effect. Wir haben trotzdem bereits definiert, was in diesem Fall mit dem State passieren soll. Dasselbe gilt für `loadBooksFailure`. -Für alle unbekannten Actions liefert der Reducer automatisch den aktuellen State unverändert zurück. Wenn es also für eine Action keinen passenden Reducer gibt, führt das nicht zu einem Fehler. Die Bestandteile der Architektur sind so stark entkoppelt, dass sie unabhängig voneinander entwickelt werden können. +Für alle unbekannten Actions liefert der Reducer automatisch den aktuellen State unverändert zurück. Wenn es also für eine Action keinen passenden Reducer gibt, führt das nicht zu einem Fehler. Die Bestandteile der Architektur sind so stark entkoppelt, dass wir sie unabhängig voneinander entwickeln können. ### Selektoren: Daten aus dem State lesen Lassen Sie uns kurz zusammenfassen, wie weit wir bisher gekommen sind: Wir haben Action Creators definiert und die Action `loadBooks` von der `BookListComponent` aus in den Store dispatcht. Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. -Um den Kreislauf des Datenflusses zu schließen, wollen wir die Daten aus dem State nun auslesen und in der Komponente darstellen. Dazu benötigen wir demnächst etwas Theorie und einen Blick hinter die Kulissen: Der Kernbestandteil des Stores ist ein `BehaviorSubject` in Kombination mit dem Operator `scan()`. Alle eingehenden Actions werden reduziert ("aufsummiert"), indem die Reducer-Funktionen angewendet werden. Auf diese Weise wird der jeweils aktuelle Zustand erzeugt und über das Observable ausgegeben. - -Der Store ist selbst ein Observable, das wir verwenden können, um in den Komponenten auf State-Änderungen zu reagieren. Der Datenstrom gibt allerdings stets den vollständigen State aus. Um einzelne Teile daraus zu selektieren, benötigen wir eine Projektion, die sich z. B. mit dem Operator `map()` realisieren lässt. Das folgende Beispiel erstellt ein Observable, das nur das `loading`-Flag liefert: - -```ts -@Component({ /* ... */ }) -export class MyComponent { - loading$: Observable; - - constructor(private store: Store) { - this.loading$ = this.store.pipe( - map(state => state.book.loading) - ); - } -} -``` - -Obwohl diese Herangehensweise theoretisch funktioniert, bringt sie ein konzeptionelles Problem mit sich: Der Store gibt bei *jeder* Änderung den gesamten State aus. Das Observable `loading$` emittiert also auch dann einen neuen Wert, wenn sich das `loading`-Flag gar nicht geändert hat. Werden die Anwendung und der State komplexer, führt das dazu, dass bei jeder noch so trivialen State-Änderung alle Observables in den Komponenten feuern und in der Folge die Change Detection von Angular getriggert wird. +Um den Kreislauf des Datenflusses zu schließen, wollen wir die Daten aus dem State nun auslesen und in der Komponente darstellen. Der Store gibt dabei stets den vollständigen State aus. Um einzelne Teile daraus zu lesen, benötigen wir eine Projektion. In der Praxis werden diese Lesezugriffe schnell komplexer: Mitunter wollen wir Daten nicht nur einfach auslesen, sondern Projektionen über verschiedene Teile des States ausführen. Stellen wir uns vor, wir besitzen eine Liste von Autoren und Autorinnen und eine Liste von Büchern – wollen aber nun nur die Bücher ausgeben, die von einer bestimmten Person verfasst wurden. Dazu ist zusätzliche Logik nötig, die nicht in die Komponenten gehört. Stattdessen lagern wir diese Logik in separate Funktionen aus, die unabhängig von den Komponenten sind. Wir können eine solche Funktion mit einer Datenbankabfrage vergleichen: Die Query wird einmal definiert und kann beliebig komplex sein. Verschiedene Teile der Anwendung können diese Query nutzen und die Daten genau im benötigten Format erhalten. Die Funktionen zur Abfrage von Daten aus dem Store werden *Selektoren* genannt. -Das gilt es zu verhindern! Wir wollen nur dann einen Wert ausgeben, wenn wirklich eine relevante Änderung an den Daten vorliegt. Dazu könnten wir den Operator `distinctUntilChanged()` verwenden. Das Framework NgRx bringt auf seinem `Store` allerdings eine eigene Methode mit, die alle nötigen Funktionalitäten bereits kombiniert: `select()`. - -```ts -@Component({ /* ... */ }) -export class MyComponent { - loading$: Observable; - - constructor(private store: Store) { - this.loading$ = this.store - .select(state => state.book.loading); - } -} -``` - -Das resultierende Observable gibt das Ergebnis nur aus, wenn der Wert sich tatsächlich verändert hat. - -![Diagramm: Der Store gibt bei jeder Änderung den gesamten State aus (loading wechselt false, true, true, false). Der Selektor store.selectSignal(state => state.book.loading) gibt darunter nur dann einen neuen Wert aus, wenn sich das loading-Flag tatsächlich ändert (false, true, false).](./selectors-gap.svg "Selektor mit store.selectSignal() verwenden") - -Wenn wir das Beispiel so implementieren, stoßen wir allerdings auf ein weiteres Problem: Die Struktur des States wird dynamisch verändert, wenn einzelne Feature-Module mithilfe von `forFeature()` einen Teilbaum des States registrieren. Durch eine statische Typanalyse allein ist es also nicht möglich, die vollständige Struktur des States zu ermitteln: Der TypeScript-Compiler weiß nicht, dass das Property `book` im State existiert. - -Dazu kommt, dass wir an dieser Stelle lediglich einen einfachen Teil des States selektiert haben. In der Praxis werden aber die Lesezugriffe auf den State komplexer. Mitunter wollen wir Daten nicht nur einfach auslesen, sondern Projektionen über verschiedene Teile des States ausführen. Stellen Sie sich vor, Sie besitzen eine Liste von Autoren und Autorinnen und eine Liste von Büchern – wollen aber nun nur die Bücher ausgeben, die von einer bestimmten Person verfasst wurden. Dazu ist zusätzliche Logik nötig, die nicht in die Komponenten gehört. Stattdessen soll diese Logik in separate Funktionen ausgelagert werden, die unabhängig von den Komponenten sind. Sie können eine solche Funktion mit einer Datenbankabfrage vergleichen: Die Query wird einmal definiert und kann beliebig komplex sein. Verschiedene Teile der Anwendung können diese Query nutzen und die Daten genau im benötigten Format erhalten. Die Funktionen zur Abfrage von Daten aus dem Store werden *Selektoren* genannt. - -Selektoren werden ebenfalls in eigenen Dateien untergebracht, die unabhängig von den Komponenten sind. Dazu wurde bereits die Datei `books/store/book.selectors.ts` erstellt. Zur Definition von Selektoren bringt NgRx eigene Hilfsfunktionen mit: `createFeatureSelector()` und `createSelector()`. Sie sorgen unter anderem dafür, dass die selektierten Daten korrekt typisiert sind, auch wenn der State zur Laufzeit dynamisch erweitert wird. +Selektoren bringen wir ebenfalls in eigenen Dateien unter, die unabhängig von den Komponenten sind. Dazu wurde bereits die Datei `books/store/book.selectors.ts` erstellt. Zur Definition von Selektoren bringt NgRx eigene Hilfsfunktionen mit: `createFeatureSelector()` und `createSelector()`. Sie sorgen unter anderem dafür, dass die selektierten Daten korrekt typisiert sind, auch wenn der State zur Laufzeit dynamisch erweitert wird. In einem solchen Selektor versteckt sich außerdem ein wichtiges Konzept, das sich *Memoization* nennt. Damit aufwendige Projektionen nicht bei jeder State-Änderung neu ausgeführt werden, speichert jeder Selektor seine zuletzt verarbeiteten Eingabewerte. Haben sich diese Werte nicht geändert, so wird auch die Projektion nicht neu ausgeführt, sondern das zuletzt berechnete Ergebnis wird erneut ausgegeben. Das Prinzip der Memoization ist in die Selektoren bereits eingebaut, wenn wir die mitgelieferten Funktionen verwenden. Damit das allerdings funktioniert, müssen wir Selektoren als Pure Functions definieren: Sie dürfen nur die Werte verarbeiten, die sie als Argumente erhalten, und es dürfen keine Seiteneffekte ausgeführt werden. -Ausgehend davon, dass der Store immer den gesamten State liefert, müssen wir zunächst einen bestimmten Feature-State aus dem großen Objekt selektieren. Dazu nutzen wir die Funktion `createFeatureSelector()`. Sie erstellt einen Selektor, der ein Feature anhand seines Namens auswählt. Dabei verwenden wir den Feature-Key, den wir bereits bei `forFeature()` im `BooksModule` genutzt haben. Damit diese Selektion typsicher funktioniert, müssen wir außerdem das Interface für den Feature-State angeben. Um den State-Slice für das Feature `book` auszuwählen, benötigen wir also den folgenden Feature-Selektor, der bereits automatisch vorbereitet wurde: +Ausgehend davon, dass der Store immer den gesamten State liefert, müssen wir zunächst einen bestimmten Feature-State aus dem großen Objekt selektieren. Dazu nutzen wir die Funktion `createFeatureSelector()`. Sie erstellt einen Selektor, der ein Feature anhand seines Namens auswählt. Dabei verwenden wir den Feature-Key, den wir bereits bei `provideState()` genutzt haben. Damit diese Selektion typsicher funktioniert, geben wir außerdem das Interface für den Feature-State an. Um den State-Slice für das Feature `book` auszuwählen, benötigen wir also den folgenden Feature-Selektor, der bereits automatisch vorbereitet wurde: ```ts // books/store/book.selectors.ts @@ -460,41 +414,52 @@ export const selectAllBooks = createSelector( ); ``` -In der `BookListComponent` verwenden wir nun diese beiden Funktionen, um Observables zu erstellen, die uns die benötigten Daten liefern. Die Methode `store.select()` erhält dazu als Argument einen der eben definierten Selektoren. Wir legen alle Observables direkt in der Komponentenklasse ab, denn wir wollen die Subscription im Template mit der AsyncPipe erledigen. Wenn Sie Ihre Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. Sie müssen also nur selten in der Komponentenklasse direkt auf ein Observable aus dem Store subscriben. +Diese Selektoren lesen wir nun in der `BookListComponent` aus. Der Store stellt dafür die Methode `selectSignal()` bereit: Sie erwartet einen Selektor und liefert ein **Signal** des ausgewählten State-Slice zurück. Im Gegensatz zur Methode `select()`, die ein Observable liefert, erhalten wir mit `selectSignal()` direkt ein Signal – das passt nahtlos in die signal-basierte Welt von modernem Angular und wir benötigen im Template keine `AsyncPipe`. + +Das resultierende Signal aktualisiert sich nur dann, wenn sich der selektierte Wert tatsächlich verändert hat. Obwohl der Store bei *jeder* Änderung den gesamten State neu erzeugt, gibt unser `loading`-Signal also nur dann einen neuen Wert aus, wenn sich das `loading`-Flag wirklich geändert hat. Die folgende Abbildung veranschaulicht das: + +![Diagramm: Der Store gibt bei jeder Änderung den gesamten State aus (loading wechselt false, true, true, false). Der Selektor store.selectSignal(state => state.book.loading) gibt darunter nur dann einen neuen Wert aus, wenn sich das loading-Flag tatsächlich ändert (false, true, false).](./selectors-gap.svg "Selektor mit store.selectSignal() verwenden") -Da wir das Observable `books$` nun aus dem Store beziehen, benötigen wir den `BookStoreService` nicht mehr. Wir können das Argument des Konstruktors also entfernen. +Wir legen die Signale direkt als Propertys in der Komponentenklasse ab. Wenn wir unsere Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. Da wir die Buchliste nun aus dem Store beziehen, benötigen wir den `BookStoreService` in der Komponente nicht mehr. ```ts // books/book-list/book-list.component.ts +import { Component, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import { loadBooks } from '../store/book.actions'; import { selectAllBooks, selectBooksLoading } from '../store/book.selectors'; @Component({ /* ... */ }) export class BookListComponent { - books$: Observable; - loading$: Observable; + private store = inject(Store); - constructor(private store: Store) { - this.books$ = this.store.select(selectAllBooks); - this.loading$ = this.store.select(selectBooksLoading); + books = this.store.selectSignal(selectAllBooks); + loading = this.store.selectSignal(selectBooksLoading); + constructor() { this.store.dispatch(loadBooks()); } } ``` -Im Template der Komponente nutzen wir die Observables, um die Daten darzustellen. Das Observable `books$` wird bereits korrekt verarbeitet. Für den Ladeindikator können wir ein neues Element erstellen, das wir direkt mit `ngIf` ein- und ausblenden. Unser globales Stylesheet bietet dafür die passende CSS-Klasse `loader` an. +Im Template der Komponente lesen wir die Signale aus, indem wir sie wie eine Funktion aufrufen (`books()`). Für die Buchliste nutzen wir den nativen Control Flow mit `@for`, für den Ladeindikator `@if`. Unser globales Stylesheet bietet dafür die passende CSS-Klasse `loader` an. ```html

Books

-
    - +
      + @for (book of books(); track book.isbn) { +
    • + }
    -
    Loading ...
    +@if (loading()) { +
    Loading ...
    +} ``` -Wenn Sie die Anwendung nun aufrufen, sehen Sie den Ladeindikator. Die Komponente ist nun deutlich loser gekoppelt als vorher, denn sie braucht keine eigene spezifische Logik mehr, um den Indikator anzuzeigen oder Bücher zu laden. Sie bezieht lediglich Daten über Observables aus dem Store und löst Actions aus. +Wenn wir die Anwendung nun aufrufen, sehen wir den Ladeindikator. Die Komponente ist nun deutlich loser gekoppelt als vorher, denn sie braucht keine eigene spezifische Logik mehr, um den Indikator anzuzeigen oder Bücher zu laden. Sie bezieht lediglich Daten als Signale aus dem Store und löst Actions aus. ### Effects: Seiteneffekte ausführen @@ -512,50 +477,19 @@ Mit Effects kommen wir in den Genuss des vollen Erlebnisses von reaktiver Progra - Seiteneffekte ausführen kann und - neue Actions generiert. -Technisch ist ein Effect also immer ein `Observable`. Alle so erzeugten Actions werden automatisch in den Store dispatcht – darum kümmert sich das Framework. Effects werden in einer eigenen Klasse untergebracht, die wie ein Service aufgebaut ist: Sie trägt den Decorator `@Injectable()` und kann über ihren Konstruktor Abhängigkeiten anfordern. Damit die Effects funktionieren, muss die Klasse allerdings explizit registriert werden. Dazu muss jede Effects-Klasse in einem Modul mithilfe des `EffectsModule` eingebunden werden. Für das `BooksModule` wurde dieser Schritt bereits bei der Einrichtung erledigt: +Technisch ist ein Effect also immer ein `Observable`. Alle so erzeugten Actions werden automatisch in den Store dispatcht – darum kümmert sich das Framework. Effects bringen wir in einer eigenen Klasse unter, die wie ein Service aufgebaut ist: Sie trägt den Decorator `@Injectable()` und fordert ihre Abhängigkeiten mit `inject()` an. Damit die Effects funktionieren, müssen wir die Klasse explizit registrieren – das haben wir bereits mit `provideEffects(BookEffects)` im `providers`-Array der Route erledigt. Der folgende Datenfluss entsteht dadurch: ![Diagramm: Wie der einfache Redux-Fluss, aber erweitert um Effects. Eine Action triggert nicht nur den Reducer, sondern löst auch einen Effect aus. Der Effect ruft einen Service auf, der mit einer API kommuniziert, und löst seinerseits eine neue Action aus, die wieder in den Reducer fließt.](./ngrx-flow-full.svg "Datenfluss in NgRx mit Effects") -```ts -// books/books.module.ts -import { EffectsModule } from '@ngrx/effects'; -import { BookEffects } from './store/book.effects'; - -@NgModule({ - // ... - imports: [ - // ... - EffectsModule.forFeature([BookEffects]) - ] -}) -export class BooksModule { } -``` - -Die Klasse `BookEffects` befindet sich in der Datei `books/store/book.effects.ts`. Ein Effect wird als ein Property in dieser Klasse definiert. Der Name des Propertys spielt keine Rolle, sollte aber passend zur Aufgabe benannt werden. Das Ziel ist es, hier ein Observable zu entwickeln, das Actions ausgibt. Jeder Effect wird mit der Funktion `createEffect()` gekapselt und wird so automatisch in den Lebenszyklus von NgRx integriert. Ein erster Effect zum Laden von Daten wurde hier bereits automatisch generiert, weil wir beim Anlegen die Einstellung `--api` verwendet haben. Dieses Grundgerüst wollen wir vervollständigen, um die Buchliste per HTTP zu laden. +Die Klasse `BookEffects` befindet sich in der Datei `books/store/book.effects.ts`. Ein Effect wird als ein Property in dieser Klasse definiert. Der Name des Propertys spielt keine Rolle, sollte aber passend zur Aufgabe benannt werden. Das Ziel ist es, hier ein Observable zu entwickeln, das Actions ausgibt. Jeden Effect kapseln wir mit der Funktion `createEffect()`, sodass er automatisch in den Lebenszyklus von NgRx integriert wird. Ein erster Effect zum Laden von Daten wurde bereits automatisch generiert, weil wir beim Anlegen die Einstellung `--api` verwendet haben. Dieses Grundgerüst wollen wir vervollständigen, um die Buchliste per HTTP zu laden. -Zunächst benötigen wir eine Instanz des `BookStoreService`, der die HTTP-Kommunikation kapselt: - -```ts -// books/store/book.effects.ts -import { BookStoreService } from '../../shared/book-store.service'; - -@Injectable() -export class BookEffects { - // ... - constructor( - private actions$: Actions, - private service: BookStoreService - ) {} -} -``` - -In die Klasse wurde bereits der Service `Actions` injiziert. Damit erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. Dieser Datenstrom ist meist die Grundlage für unsere Effects. (Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: [5 useful NgRx effects that don't rely on actions](https://angular.schule/blog/2018-06-5-useful-effects-without-actions).) +In die Klasse holen wir mit `inject()` zwei Abhängigkeiten: den `BookStoreService`, der die HTTP-Kommunikation kapselt, und den Service `Actions`. Über `Actions` erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. Dieser Datenstrom ist meist die Grundlage für unsere Effects. (Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: [5 useful NgRx effects that don't rely on actions](https://angular.schule/blog/2018-06-5-useful-effects-without-actions).) Der Effect `loadBooks$` soll auf die Action `loadBooks` reagieren, den passenden HTTP-Request auslösen und Actions zurück in den Store leiten (Success und Failure). Wir zeigen zunächst den vollständigen Code und gehen die Implementierung anschließend Schritt für Schritt durch: ```ts // books/store/book.effects.ts -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { catchError, map, switchMap } from 'rxjs/operators'; import { of } from 'rxjs'; @@ -565,6 +499,8 @@ import { BookStoreService } from '../../shared/book-store.service'; @Injectable() export class BookEffects { + private actions$ = inject(Actions); + private service = inject(BookStoreService); loadBooks$ = createEffect(() => { return this.actions$.pipe( @@ -577,17 +513,12 @@ export class BookEffects { ) ); }); - - constructor( - private actions$: Actions, - private service: BookStoreService - ) {} } ``` Der Datenfluss beginnt beim Strom aller Actions aus der gesamten Anwendung: `this.actions$`. Wir interessieren uns hier allerdings nur für Actions mit dem bestimmten Typ `loadBooks`. Um den Datenstrom zu filtern, eignet sich der Operator `filter()`. Das führt allerdings zu sehr technischem Code, erschwert die korrekte Typisierung und sagt wenig über die tatsächliche Semantik aus. NgRx bringt deshalb einen eigenen Operator mit, mit dem wir den Datenstrom nach dem Action-Typ filtern können: `ofType()`. Als Argument übergeben wir hier einen Action Creator. Anschließend liegt ein Datenstrom vor, der nur Actions vom Typ `loadBooks` ausgibt. -Für jede dieser Actions wollen wir nun einen HTTP-Request ausführen und die Ergebnisse verarbeiten. Damit betreten wir erneut das Terrain der Higher-Order Observables. Wir müssen uns sorgfältig für einen der vier Flattening-Operatoren entscheiden. Unsere Wahl fällt hier auf `switchMap()`: Wird die Buchliste noch geladen und währenddessen erneut angefragt, soll nur die zuletzt gesendete Anfrage bearbeitet werden. Verwenden Sie bitte nicht immer `switchMap()` in Ihren Effects, sondern wägen Sie sorgfältig ab, welcher Flattening-Operator am besten zu Ihrem jeweiligen Problem passt. +Für jede dieser Actions wollen wir nun einen HTTP-Request ausführen und die Ergebnisse verarbeiten. Damit betreten wir erneut das Terrain der Higher-Order Observables. Wir müssen uns sorgfältig für einen der vier Flattening-Operatoren entscheiden. Unsere Wahl fällt hier auf `switchMap()`: Wird die Buchliste noch geladen und währenddessen erneut angefragt, soll nur die zuletzt gesendete Anfrage bearbeitet werden. Wir sollten allerdings nicht immer `switchMap()` in unseren Effects verwenden, sondern sorgfältig abwägen, welcher Flattening-Operator am besten zum jeweiligen Problem passt. Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookStoreService` mit der Methode `getAll()` einsetzen, die ein Observable zurückgibt. Unser Effect ist nun ein Observable, das ein Array von Büchern liefert. Damit die Buchliste im Store verarbeitet werden kann, müssen wir sie in eine Action verpacken. Wir nutzen dazu den Operator `map()` und wandeln damit die Buchliste um in eine Action `loadBooksSuccess`, die die Liste enthält. @@ -597,65 +528,76 @@ Auf diese Weise erzeugen wir einen Datenstrom, der nacheinander verschiedene Act Bei der Fehlerbehandlung ist es wichtig, den Fehler so dicht wie möglich dort abzufangen, wo er entsteht. Wir setzen `catchError()` deshalb nicht im Hauptdatenstrom ein, sondern hängen `map()` und `catchError()` direkt an den Serviceaufruf an. Das `switchMap()` verarbeitet also ein Observable, das in jedem Fall eine Action liefert – so wird der Hauptdatenstrom nicht durch Fehler gefährdet. Wird ein Effect durch einen Fehler im Hauptdatenstrom gestört, so wird das zugrunde liegende Observable beendet, und die Anwendung kann nicht mehr auf eine weitere Nachricht durch den beendeten Effect reagieren. -Probieren Sie die Anwendung nun aus und beobachten Sie auch die Actions in den Redux DevTools! Nachdem die Buchliste geladen wurde, wird `loadBooksSuccess` dispatcht, und der Reducer fügt die Buchliste in den State ein. Die Selektoren in der Komponente reagieren darauf, und die Bücher werden dargestellt. +Probieren wir die Anwendung nun aus und beobachten wir auch die Actions in den Redux DevTools! Nachdem die Buchliste geladen wurde, wird `loadBooksSuccess` dispatcht, und der Reducer fügt die Buchliste in den State ein. Die Selektoren in der Komponente reagieren darauf, und die Bücher werden dargestellt. -Bitte beachten Sie, dass Sie mit einem ungünstig programmierten Effect schnell eine Endlosschleife erzeugen können, die den Browser "zum Glühen" bringt, wenn parallel `ng serve` läuft und die Anwendung sofort aktualisiert. Der folgende Effect empfängt alle Actions und leitet sie direkt in den Store zurück – es entsteht eine Endlosschleife: +Wir sollten beachten, dass wir mit einem ungünstig programmierten Effect schnell eine Endlosschleife erzeugen können, die den Browser "zum Glühen" bringt, wenn parallel `ng serve` läuft und die Anwendung sofort aktualisiert. Der folgende Effect empfängt alle Actions und leitet sie direkt in den Store zurück – es entsteht eine Endlosschleife: ```ts loopOfDeath$ = createEffect(() => this.actions$); ``` -Wir können übrigens einen Effect so konfigurieren, dass er die ausgegebenen Elemente des Datenstroms nicht als Actions dispatcht. Das ist immer dann nützlich, wenn ein Effect lediglich Seiteneffekte ausführt, aber keine weiteren Aktionen zur Folge hat. Denken Sie z. B. daran, dass wir nach dem erfolgreichen Anlegen eines Buchs zur Detailseite navigieren möchten – nach diesem Schritt sollen keine weiteren Actions getriggert werden. Dazu können wir im zweiten Argument von `createEffect()` die Einstellung `{ dispatch: false }` setzen. +Wir können einen Effect übrigens so konfigurieren, dass er die ausgegebenen Elemente des Datenstroms nicht als Actions dispatcht. Das ist immer dann nützlich, wenn ein Effect lediglich Seiteneffekte ausführt, aber keine weiteren Aktionen zur Folge hat. Denken wir z. B. daran, dass wir nach dem erfolgreichen Anlegen eines Buchs zur Detailseite navigieren möchten – nach diesem Schritt sollen keine weiteren Actions getriggert werden. Dazu setzen wir im zweiten Argument von `createEffect()` die Einstellung `{ dispatch: false }`: ```ts -loopOfDeath$ = createEffect( - () => this.actions$, +navigate$ = createEffect( + () => this.actions$.pipe(/* ... */), { dispatch: false } ); ``` -Ein Tipp aus der Praxis: Nutzen Sie diese Einstellung während der Entwicklung, um einen *Loop of Death* zu verhindern. Sie können einen komplexen Effect dann in Ruhe debuggen, ohne dass die Actions in den Store geleitet werden. Erst wenn die Implementierung fertig ist, aktivieren Sie den Effect, indem Sie das `{ dispatch: false }` wieder entfernen. +Ein Tipp aus der Praxis: Wir können diese Einstellung während der Entwicklung nutzen, um einen *Loop of Death* zu verhindern. So lässt sich ein komplexer Effect in Ruhe debuggen, ohne dass die Actions in den Store geleitet werden. Erst wenn die Implementierung fertig ist, aktivieren wir den Effect, indem wir das `{ dispatch: false }` wieder entfernen. -#### Achtung: Proprietäres Verhalten von TypeScript +#### Funktionale Effects -Bei allen gezeigten Effects haben wir die Syntax mit dem alten proprietären Verhalten von TypeScript (`useDefineForClassFields: false`) verwendet. Zum Zeitpunkt der Fertigstellung dieses Buchs wurde diese Schreibweise in der offiziellen Dokumentation, in der Demo-Anwendung und in den Schematics von NgRx empfohlen. Wir haben uns daher dazu entschlossen, möglichst wenig von den offiziellen Quellen abzuweichen, auch wenn es womöglich bei geänderten Standardeinstellungen zu Fehlern kommen kann. Im Buch beschreiben wir im Teil "Wissenswertes", unter welchen Umständen dieser Fehler entsteht. - -Sollten die Beispiele in Zukunft nicht mehr lauffähig sein, so können Sie den Code leicht umstellen. Das folgende Listing zeigt eine Variante, bei der wir die Klasse `Actions` und den `BookStoreService` mithilfe der Funktion `inject()` anfordern. +Ein Effect muss nicht zwingend als Property einer Klasse definiert werden. NgRx unterstützt auch *funktionale Effects*: Hier definieren wir den Effect als exportierte Konstante und holen `Actions` sowie alle benötigten Services über Default-Parameter mit `inject()`. Über die Option `{ functional: true }` aktivieren wir diese Schreibweise: ```ts // books/store/book.effects.ts import { inject } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { catchError, map, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; -@Injectable() -export class BookEffects { - - private service = inject(BookStoreService); +import * as BookActions from './book.actions'; +import { BookStoreService } from '../../shared/book-store.service'; - loadBooks$ = createEffect(() => { - return inject(Actions).pipe( +export const loadBooks$ = createEffect( + (actions$ = inject(Actions), service = inject(BookStoreService)) => { + return actions$.pipe( ofType(BookActions.loadBooks), switchMap(() => - this.service.getAll().pipe( + service.getAll().pipe( map(data => BookActions.loadBooksSuccess({ data })), catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) ) ) ); - }); -} + }, + { functional: true } +); +``` + +Funktionale Effects sammeln wir üblicherweise in einer Datei und registrieren sie als Ganzes über einen Namespace-Import: + +```ts +import * as bookEffects from './store/book.effects'; +// ... +provideEffects(bookEffects) ``` +Beide Varianten – klassenbasiert mit `inject()` und funktional – sind gleichwertig. Welche wir wählen, ist vor allem eine Frage des Stils. + ### Geschafft! -Sobald der Effect aktiv ist, wird unsere Anwendung die Bücher über HTTP laden und die Liste mittels `loadBooksSuccess` an den Reducer übergeben. Dieser wird einen neuen Zustand mit den geladenen Büchern berechnen. Abschließend wird mithilfe eines Selektors die Oberfläche aktualisiert, und die neuen Bücher werden angezeigt. +Sobald der Effect aktiv ist, wird unsere Anwendung die Bücher über HTTP laden und die Liste mittels `loadBooksSuccess` an den Reducer übergeben. Dieser berechnet einen neuen Zustand mit den geladenen Büchern. Abschließend aktualisiert ein Selektor die Oberfläche, und die neuen Bücher werden angezeigt. -Und damit haben Sie die Rundreise durch alle Bausteine von Redux und NgRx geschafft. Mithilfe eines zentralen Stores haben wir die Zustände der Anwendung zentralisiert und so die Buchliste vom Server abgerufen. Die Komponenten beinhalten mit dieser Architektur nur noch wenig Logik – alles Nötige passiert im Store. +Und damit haben wir die Rundreise durch alle Bausteine von Redux und NgRx geschafft. Mithilfe eines zentralen Stores haben wir die Zustände der Anwendung zentralisiert und so die Buchliste vom Server abgerufen. Die Komponenten beinhalten mit dieser Architektur nur noch wenig Logik – alles Nötige passiert im Store. -Vielleicht haben Sie nun das Gefühl, dass wir für eine kleine Aufgabe unnötig viel Code erzeugt haben. Betrachtet man das vorliegende Beispiel isoliert, so ist diese Empfindung richtig: Für kleine Anwendungen ist der Einsatz von Redux und NgRx nicht zwingend sinnvoll. Steigt allerdings die Komplexität des Projekts, so kann ein zentrales State Management mit klaren Regeln und einer durchdachten Architektur eine sinnvolle Investition sein. Prüfen Sie also für ein Projekt sorgfältig, ob sich der Einsatz von Redux wirklich lohnt. +Vielleicht haben wir nun das Gefühl, dass wir für eine kleine Aufgabe unnötig viel Code erzeugt haben. Betrachtet man das vorliegende Beispiel isoliert, so ist diese Empfindung richtig: Für kleine Anwendungen ist der Einsatz von Redux und dem Global Store nicht zwingend sinnvoll. Steigt allerdings die Komplexität des Projekts, so kann ein zentrales State Management mit klaren Regeln und einer durchdachten Architektur eine sinnvolle Investition sein. Wir sollten also für jedes Projekt sorgfältig prüfen, ob sich der Einsatz wirklich lohnt. Eine leichtgewichtige Alternative für viele Fälle stellen wir in [Teil 3 dieser Serie](/material/ngrx-signal-store) mit dem SignalStore vor. ## Debugging mit den Redux DevTools -Wir möchten Ihnen zum Abschluss des Praxisteils ein wichtiges Hilfsmittel zeigen, das beim Debugging von Anwendungen mit NgRx hilfreich ist: die *Redux DevTools*. +Wir möchten zum Abschluss des Praxisteils ein wichtiges Hilfsmittel zeigen, das beim Debugging von Anwendungen mit NgRx hilfreich ist: die *Redux DevTools*. Ein besonderer Vorteil der Redux-Architektur ist das sogenannte *Time Travel Debugging*. Da stets durch eintreffende Actions ein neuer State im Store erzeugt wird, ist es möglich, die Historie der Ereignisse und Zustände nachzuvollziehen. Die Redux DevTools unterstützen uns dabei mit einer grafischen Oberfläche, über die wir alle Actions und Zustandsänderungen kontrollieren und debuggen können. @@ -667,15 +609,15 @@ Die Redux DevTools sind eine Erweiterung für Google Chrome. Um sie zu verwenden ### Die DevTools in der Anwendung registrieren -Um Informationen aus unserem Store mit dem Entwicklungswerkzeug zu teilen, müssen wir in der Anwendung die passende Schnittstelle schaffen. Dazu bringt NgRx das Modul `@ngrx/store-devtools` mit, das diese Schnittstelle zur Kommunikation implementiert. +Um Informationen aus unserem Store mit dem Entwicklungswerkzeug zu teilen, müssen wir in der Anwendung die passende Schnittstelle schaffen. Dazu bringt NgRx das Paket `@ngrx/store-devtools` mit, das diese Schnittstelle zur Kommunikation implementiert. -Der einfachste Weg zur Einrichtung ist es, das Modul mithilfe von `ng add` in das Projekt einzufügen. Als wir im Praxisteil die Pakete für NgRx installiert haben, haben wir diesen Schritt bereits erledigt. +Der einfachste Weg zur Einrichtung ist es, das Paket mithilfe von `ng add` in das Projekt einzufügen. Als wir im Praxisteil die Pakete für NgRx installiert haben, haben wir diesen Schritt bereits erledigt. ```bash ng add @ngrx/store-devtools ``` -Damit werden die benötigten Abhängigkeiten installiert, und das zugehörige `StoreDevtoolsModule` wird in das `AppModule` der Anwendung eingebunden. +Damit werden die benötigten Abhängigkeiten installiert, und der Aufruf `provideStoreDevtools()` wird in die `app.config.ts` der Anwendung eingetragen (siehe Abschnitt "Grundstruktur"). ### Die DevTools nutzen @@ -699,13 +641,13 @@ Mit diesem Debugging-Werkzeug haben wir also an einem Ort einen zentralen Überb ## Redux und NgRx: Wie geht's weiter? -Wir haben in diesem Kapitel die Grundlagen der Redux-Architektur und des Frameworks NgRx kennengelernt. Tatsächlich geht die Reise aber noch viel weiter! Wir möchten Ihnen in diesem Abschnitt einen Ausblick geben, welche Tools und Konzepte Ihnen dabei noch begegnen werden. +Wir haben in diesem Kapitel die Grundlagen der Redux-Architektur und des Frameworks NgRx kennengelernt. Tatsächlich geht die Reise aber noch viel weiter! Wir möchten in diesem Abschnitt einen Ausblick geben, welche Tools und Konzepte uns dabei noch begegnen werden. ### Actions gruppieren mit `createActionGroup()` Wir haben die Funktion `createAction()` kennengelernt, um einzelne Actions zu erstellen. Zum Laden der Buchliste haben wir auf diese Weise drei einzelne Actions definiert. Dafür war viel repetitiver Code notwendig: Wir mussten den Variablennamen definieren, das Präfix `[Book]` setzen und den Type der Action als String angeben. -Um die Erzeugung der Actions zu vereinfachen, wurde mit NgRx 13.2 die Funktion `createActionGroup()` eingeführt. Damit können wir eine Gruppe von Actions mit einem gemeinsamen Präfix anlegen. Die drei Actions aus dem vorherigen Beispiel lassen sich also wie folgt definieren: +Um die Erzeugung der Actions zu vereinfachen, bietet NgRx die Funktion `createActionGroup()`. Damit können wir eine Gruppe von Actions mit einem gemeinsamen Präfix anlegen. Die drei Actions aus dem vorherigen Beispiel lassen sich also wie folgt definieren: ```ts // books/store/book.actions.ts @@ -721,19 +663,46 @@ export const BookActions = createActionGroup({ }); ``` -Die Besonderheit an diesem Ansatz ist, dass wir die Variablennamen nicht mehr von Hand notieren müssen. Wir können die Actions als z. B. `BookActions.loadBooks()` verwenden – der Name des Propertys wird automatisch aus dem Action Type ermittelt. Auf diese Weise sparen wir uns viel Tipparbeit. +Die Besonderheit an diesem Ansatz ist, dass wir die Variablennamen nicht mehr von Hand notieren müssen. Wir können die Actions z. B. als `BookActions.loadBooks()` verwenden – der Name des Propertys wird automatisch aus dem Action Type ermittelt. Auf diese Weise sparen wir uns viel Tipparbeit. + +Um die Actions zu verwenden, importieren wir nun immer das gesamte Objekt `BookActions`, nicht die einzelnen Actions. In den Komponenten, Reducers und Effects sind also auch Anpassungen nötig, um diesen Ansatz zu nutzen. + +### Reducer und Selektoren bündeln mit `createFeature()` + +Bisher haben wir den Feature-Key, das State-Interface, den Reducer und die Selektoren von Hand notiert und über mehrere Dateien verteilt. NgRx bietet mit `createFeature()` eine kompaktere Variante: Wir übergeben einen Feature-Namen und den Reducer, und NgRx erzeugt daraus automatisch den Feature-Selektor sowie je einen Selektor pro State-Property. + +```ts +// books/store/book.reducer.ts +export const bookFeature = createFeature({ + name: 'book', + reducer: createReducer( + initialState, + on(BookActions.loadBooks, (state): State => ({ ...state, loading: true })), + on(BookActions.loadBooksSuccess, (state, { data }): State => ({ ...state, books: data, loading: false })), + on(BookActions.loadBooksFailure, (state): State => ({ ...state, loading: false })) + ) +}); + +export const { + name, // Feature-Name + reducer, // Feature-Reducer + selectBookState, // Feature-Selektor + selectBooks, // Selektor für das Property "books" + selectLoading // Selektor für das Property "loading" +} = bookFeature; +``` -Bitte beachten Sie: Um die Actions zu verwenden, müssen wir nun immer das gesamte Objekt `BookActions` importieren, nicht die einzelnen Actions. In den Komponenten, Reducers und Effects sind also auch Anpassungen nötig, um diesen Ansatz zu nutzen. +Die generierten Selektoren `selectBooks` und `selectLoading` ersetzen unsere von Hand geschriebenen Selektoren. Eine eigene Datei `book.selectors.ts` benötigen wir damit nur noch für komplexere, abgeleitete Selektoren. Registrieren können wir das Feature direkt als Objekt: `provideState(bookFeature)`. ### Routing -Der Anwendungszustand setzt sich nicht nur aus geladenen Daten und Einstellungen zusammen – die aktuell geladene Route und ihre Parameter gehören ebenfalls in den State. Beachtet man dies nicht, so sieht man gegebenenfalls zum richtigen State die falsche Komponente auf der Oberfläche. Deshalb sollten Informationen zum Routing auch im NgRx-Store abgelegt werden. Den passenden Adapter zwischen Router und Store erhalten wir mit dem Modul `@ngrx/router-store`: +Der Anwendungszustand setzt sich nicht nur aus geladenen Daten und Einstellungen zusammen – die aktuell geladene Route und ihre Parameter gehören ebenfalls in den State. Beachten wir dies nicht, so sehen wir gegebenenfalls zum richtigen State die falsche Komponente auf der Oberfläche. Deshalb sollten Informationen zum Routing auch im NgRx-Store abgelegt werden. Den passenden Adapter zwischen Router und Store erhalten wir mit dem Paket `@ngrx/router-store`: ```bash ng add @ngrx/router-store ``` -Das Modul verfügt über eigene Actions und Reducers und kommuniziert direkt mit dem Router. Dadurch werden alle Aktivitäten des Routers mit Actions abgebildet. Es werden unter anderem die folgenden Actions ausgelöst: +Das Paket verfügt über eigene Actions und Reducers und kommuniziert direkt mit dem Router. Dadurch werden alle Aktivitäten des Routers mit Actions abgebildet. Es werden unter anderem die folgenden Actions ausgelöst: - `routerRequestAction` - `routerNavigationAction` @@ -772,7 +741,7 @@ interface EntityState { } ``` -Damit wir die Daten in dieser Struktur nicht eigenhändig pflegen müssen, stellt das Modul einen Adapter zur Verfügung, den wir zusätzlich initialisieren müssen. Der Adapter nutzt automatisch das Property `id` als Primärschlüssel der Entität. Ist die ID allerdings in einem anderen Property enthalten, z. B. bei einem Buch die ISBN, so müssen wir eine Funktion `selectId` angeben, die die richtige ID auswählt. +Damit wir die Daten in dieser Struktur nicht eigenhändig pflegen müssen, stellt das Paket einen Adapter zur Verfügung, den wir zusätzlich initialisieren müssen. Der Adapter nutzt automatisch das Property `id` als Primärschlüssel der Entität. Ist die ID allerdings in einem anderen Property enthalten, z. B. bei einem Buch die ISBN, so müssen wir eine Funktion `selectId` angeben, die die richtige ID auswählt. ```ts import { createEntityAdapter } from '@ngrx/entity'; @@ -847,7 +816,7 @@ Auf diese Weise können wir die Verwaltung von Entitäten im State sehr effizien ### Testing -Alle auf Grundlage von NgRx entwickelten Bausteine sollten auch getestet werden. Dafür möchten wir Ihnen in diesem Abschnitt einige Hinweise geben. Grundsätzlich werden bei der Initialisierung mit den Schematics von NgRx bereits Grundgerüste für die Unit-Tests angelegt – Sie können also direkt loslegen. +Alle auf Grundlage von NgRx entwickelten Bausteine sollten auch getestet werden. Dafür möchten wir in diesem Abschnitt einige Hinweise geben. Grundsätzlich werden bei der Initialisierung mit den Schematics von NgRx bereits Grundgerüste für die Unit-Tests angelegt – wir können also direkt loslegen. #### Actions @@ -917,9 +886,9 @@ describe('Book Selectors', () => { #### Effects -Effects sind nur schwierig isoliert zu testen, denn sie greifen auf verschiedene Abhängigkeiten aus der Anwendung zu. Dazu ist nicht nur das Modul `@ngrx/effects` nötig, sondern auch alle verwendeten HTTP-Services. Außerdem muss das Observable `actions$: Actions` mit einem Strom von Actions versorgt werden, und wir wollen keinen vollständigen Store aufsetzen. +Effects sind nur schwierig isoliert zu testen, denn sie greifen auf verschiedene Abhängigkeiten aus der Anwendung zu. Dazu ist nicht nur das Paket `@ngrx/effects` nötig, sondern auch alle verwendeten HTTP-Services. Außerdem muss das Observable `actions$: Actions` mit einem Strom von Actions versorgt werden, und wir wollen keinen vollständigen Store aufsetzen. -NgRx bietet dafür die Funktion `provideMockActions()`, die einen gemockten Strom von Actions bereitstellt. Tests für Effects müssen mithilfe von `TestBed` definiert werden, sodass wir die Dependency Injection von Angular nutzen können. Alle verwendeten Services müssen selbstverständlich auch durch Mocks oder Stubs ersetzt werden, damit keine echten HTTP-Requests ausgeführt werden. +NgRx bietet dafür die Funktion `provideMockActions()`, die einen gemockten Strom von Actions bereitstellt. Tests für klassenbasierte Effects definieren wir mithilfe von `TestBed`, sodass wir die Dependency Injection von Angular nutzen können. Alle verwendeten Services müssen selbstverständlich auch durch Mocks oder Stubs ersetzt werden, damit keine echten HTTP-Requests ausgeführt werden. ```ts // books/store/book.effects.spec.ts @@ -991,7 +960,9 @@ describe('BookEffects', () => { }); ``` -Der Aufbau lässt sich vereinfachen, wenn wir nur die Datenströme miteinander vergleichen. Dazu eignet sich das Konzept des *Marble Testing*: Anstatt ein Observable wie üblich zu erzeugen und dann darauf zu subscriben, notieren wir den geplanten Datenstrom als Marble-Diagramm direkt im Test und definieren damit die Eingabe und die erwartete Ausgabe. Die technische Grundlage dafür bietet das Paket [jasmine-marbles](https://www.npmjs.com/package/jasmine-marbles). Das Projekt stellt auch den Matcher `toBeObservable()` zur Verfügung, mit dem wir in der Expectation direkt gegen das erzeugte Observable prüfen können: +> **Funktionale Effects testen:** Funktionale Effects lassen sich noch einfacher prüfen, denn wir können sie direkt als Funktion aufrufen und die benötigten Abhängigkeiten als Argumente übergeben – ganz ohne `TestBed` und `provideMockActions()`. Wir übergeben einfach ein Observable mit den Eingangs-Actions und einen Mock-Service. + +Der Aufbau lässt sich auch vereinfachen, wenn wir nur die Datenströme miteinander vergleichen. Dazu eignet sich das Konzept des *Marble Testing*: Anstatt ein Observable wie üblich zu erzeugen und dann darauf zu subscriben, notieren wir den geplanten Datenstrom als Marble-Diagramm direkt im Test und definieren damit die Eingabe und die erwartete Ausgabe. Die technische Grundlage dafür bietet das Paket [jasmine-marbles](https://www.npmjs.com/package/jasmine-marbles). Das Projekt stellt auch den Matcher `toBeObservable()` zur Verfügung, mit dem wir in der Expectation direkt gegen das erzeugte Observable prüfen können: ```ts // books/store/book.effects.spec.ts @@ -1019,18 +990,28 @@ describe('BookEffects', () => { #### Store -Komponenten und Effects greifen häufig auf den Store zu. Unter anderem werden Actions in den Store gesendet (dispatcht), und der aktuelle Zustand wird mithilfe von Selektoren ermittelt. Wenn man für solche Komponenten oder Effects einen Unit-Test definieren will, benötigt man einen Ersatz für den echten Store. NgRx bietet uns hierfür die Funktion `provideMockStore()` an: Wir können damit einen gemockten Store registrieren, der einen von uns definierten Zustand besitzt. Dieser Zustand bleibt so lange unverändert, bis wir mittels `store.setState()` einen anderen Zustand setzen. Als Beispiel wollen wir den bekannten Effect zum Laden der Bücher so erweitern, dass er nur dann Bücher lädt, wenn die Buchliste im State noch leer ist. Hat das Array bereits Einträge, soll nichts passieren. +Komponenten und Effects greifen häufig auf den Store zu. Unter anderem werden Actions in den Store gesendet (dispatcht), und der aktuelle Zustand wird mithilfe von Selektoren ermittelt. Wenn wir für solche Komponenten oder Effects einen Unit-Test definieren wollen, benötigen wir einen Ersatz für den echten Store. NgRx bietet uns hierfür die Funktion `provideMockStore()` an: Wir können damit einen gemockten Store registrieren, der einen von uns definierten Zustand besitzt. Dieser Zustand bleibt so lange unverändert, bis wir mittels `store.setState()` einen anderen Zustand setzen. + +Als Beispiel wollen wir den bekannten Effect zum Laden der Bücher so erweitern, dass er nur dann Bücher lädt, wenn die Buchliste im State noch leer ist. Hat das Array bereits Einträge, soll nichts passieren. + +Es ist für dieses Szenario notwendig, dass wir den bestehenden State im Effect berücksichtigen. Dies können wir mit dem Operator `concatLatestFrom()` realisieren: Wir übergeben ein anderes Observable als Argument, und der Operator reichert den Hauptdatenstrom mit dem jeweils letzten Element aus diesem Observable an. Das bedeutet also, dass uns in den Effects neben dem Payload aus den Actions auch zusätzlich Daten aus dem Store zur Verfügung stehen. Wir verwenden hier direkt unseren Selektor `selectAllBooks`, um die aktuelle Buchliste aus dem Store zu erhalten. Anhand der Buchliste können wir dann mit dem Operator `filter()` entscheiden, ob die Bücher neu heruntergeladen werden sollen oder nicht. -Es ist für dieses Szenario notwendig, dass wir den bestehenden State im Effect berücksichtigen. Dies können wir mit dem Operator `concatLatestFrom()` aus dem Paket `@ngrx/effects` realisieren: Wir übergeben ein anderes Observable als Argument, und der Operator reichert den Hauptdatenstrom mit dem jeweils letzten Element aus diesem Observable an. Das bedeutet also, dass uns in den Effects neben dem Payload aus den Actions auch zusätzlich Daten aus dem Store zur Verfügung stehen. Wir verwenden hier direkt unseren Selektor `selectAllBooks`, um die aktuelle Buchliste aus dem Store zu erhalten. Anhand der Buchliste können wir dann mit dem Operator `filter()` entscheiden, ob die Bücher neu heruntergeladen werden sollen oder nicht. +> **Hinweis:** Der Operator `concatLatestFrom()` ist mit NgRx 18 vom Paket `@ngrx/effects` in das Paket `@ngrx/operators` umgezogen. Wir importieren ihn deshalb aus `@ngrx/operators`. ```ts // books/store/book.effects.ts +import { inject, Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { concatLatestFrom } from '@ngrx/operators'; import { Store } from '@ngrx/store'; -import { concatLatestFrom } from '@ngrx/effects'; -import { filter } from 'rxjs/operators'; +import { catchError, filter, map, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; @Injectable() export class BookEffects { + private actions$ = inject(Actions); + private service = inject(BookStoreService); + private store = inject(Store); loadBooks$ = createEffect(() => { return this.actions$.pipe( @@ -1045,12 +1026,6 @@ export class BookEffects { ) ); }); - - constructor( - private actions$: Actions, - private service: BookStoreService, - private store: Store - ) {} } ``` @@ -1119,7 +1094,7 @@ import { provideMockStore } from '@ngrx/store/testing'; describe('BookListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ BookListComponent ], + imports: [ BookListComponent ], providers: [ provideMockStore({ selectors: [ @@ -1141,78 +1116,32 @@ describe('BookListComponent', () => { }); ``` -Wie Sie sehen, stellt NgRx einige Hilfen und Konzepte bereit, um die einzelnen Bausteine effektiv zu testen. Vor allem die Funktionen `provideMockActions()` und `provideMockStore()` erleichtern uns das Testing für komplexe Szenarien in NgRx. +Wie wir sehen, stellt NgRx einige Hilfen und Konzepte bereit, um die einzelnen Bausteine effektiv zu testen. Vor allem die Funktionen `provideMockActions()` und `provideMockStore()` erleichtern uns das Testing für komplexe Szenarien in NgRx. ### Hilfsmittel für Komponenten: `@ngrx/component` -Das Paket `@ngrx/component` stellt eine Sammlung von nützlichen Helfern zur Verfügung, um mit reaktiven Datenströmen in Komponenten zu arbeiten. Die Werkzeuge sind dabei nicht auf den Einsatz mit NgRx beschränkt, sondern können auch unabhängig davon eingesetzt werden. Das Paket beinhaltet zwei elementare Bausteine: die Let-Direktive und die PushPipe. - -Üblicherweise verwenden wir die AsyncPipe von Angular, um Observables direkt im Template einer Komponente aufzulösen. Das ist der Weg, den wir vor allem beim Einsatz von NgRx wählen, denn die Daten kommen bereits vollständig aufbereitet durch Observables in die Komponente. Die AsyncPipe kann in jedem Template-Ausdruck eingesetzt werden, also in der Interpolation, in Property Bindings und in Direktiven. Gemeinsam mit `ngIf` und der `as`-Syntax können wir das Ergebnis eines Observables so in einem DOM-Container verfügbar machen. - -```html -{{ title$ | async }} - - - - - {{ myNumber }} - -``` - -So praktisch die AsyncPipe allerdings auch ist – sie birgt einige praktische Probleme. - -#### ngrxLet: Observables auflösen mit falsy Werten +Das Paket `@ngrx/component` stellt eine Sammlung von Helfern zur Verfügung, um mit reaktiven Datenströmen in Komponenten zu arbeiten – vor allem die Let-Direktive `ngrxLet` und die Pipe `ngrxPush`. Sie lösen Observables im Template auf, ohne auf Zone.js angewiesen zu sein, und können so in einer *zoneless* Umgebung als Alternative zur `AsyncPipe` dienen. -Die oben beschriebene Kombination von AsyncPipe und `ngIf` ist hilfreich, um die Daten aus dem Observable `numbers$` im Template verfügbar zu machen. Liefert das Observable allerdings einen *falsy* Wert – also `false`, `0`, `null`, `undefined`, `NaN` oder einen leeren String –, so wird der jeweilige Container durch `ngIf` gar nicht angezeigt. - -Um dieses Problem zu umgehen, kann die Direktive `ngrxLet` eingesetzt werden. Sie löst ein Observable im Template auf und stellt die empfangenen Daten in einer lokalen Variable zur Verfügung. Im Gegensatz zu `ngIf` bleibt das DOM-Element dabei stets sichtbar und wird nicht ausgeblendet. Liefert `numbers$` also eine `0`, so wird der Container im folgenden Beispiel trotzdem angezeigt. Darüber hinaus erhält man mit `ngrxLet` bei Bedarf auch Zugriff auf mögliche Error- und Complete-Ereignisse des Observables. - -```html - - {{ myNumber }} - - - -

    Fehler: {{ e }}

    -
    -``` - -#### PushPipe: Observables auflösen ohne Zone.js - -Die AsyncPipe von Angular hat den Vorteil, dass sie automatisch die betroffene Komponente als *dirty* markiert, sobald ein neuer Wert im Observable ausgegeben wird. Daraufhin wird diese Komponente beim nächsten Durchlauf der Change Detection geprüft, sodass die View aktualisiert wird. Durch diesen Mechanismus ist es möglich, in allen Komponenten die Strategie `OnPush` für die Change Detection zu aktivieren. Die AsyncPipe ist allerdings stets abhängig von der Bibliothek Zone.js, die für bestimmte Ereignisse in der Anwendung automatisch die Change Detection triggert. - -Im Buch beschreiben wir im Abschnitt zur Change Detection einen Weg, um Angular ohne Zone.js zu verwenden. Entwickelt man die Anwendung durchweg mit den Prinzipien der Reaktiven Programmierung, so kann es sinnvoll sein, auf die automatische Change Detection zu verzichten. Hier setzt die Pipe `ngrxPush` von NgRx an, die als Alternative zur AsyncPipe genutzt werden kann. Der elementare Unterschied: Die PushPipe triggert die Change Detection direkt, anstatt die Komponente nur für den nächsten Durchlauf als *dirty* zu markieren. Die PushPipe ist damit unabhängig von Zone.js und kann in einer *zoneless* Umgebung eingesetzt werden. Das kann sich positiv auf die Performance der Anwendung auswirken. - -Übrigens ermittelt die PushPipe automatisch, ob Zone.js in der Anwendung aktiviert ist, und kann dadurch auch das Verhalten der AsyncPipe annehmen. Auch die zuvor beschriebene Direktive `ngrxLet` baut auf derselben Grundlage auf und kann ohne Zone.js eingesetzt werden. - -```html -{{ title$ | ngrxPush }} - - - - - {{ myNumber }} - -``` +> **Hinweis:** Das Paket `@ngrx/component` befindet sich laut offizieller Dokumentation im *Maintenance-Modus*; es erhält nur noch kritische Bugfixes. In modernem Angular lesen wir Store-Daten ohnehin meist als Signal (`selectSignal()`) und stellen sie mit dem nativen Control Flow (`@if`, `@for`) dar. Damit entfallen die meisten Probleme, die `@ngrx/component` ursprünglich gelöst hat, und wir benötigen das Paket nur noch selten. ### Facades: Zustandsverwaltung abstrahieren -Um den Anwendungszustand mit NgRx zu verwalten, kommunizieren die Komponenten mit dem zentralen Store: Sie senden Actions in das System und lesen Daten mithilfe von Observables aus. Dafür müssen wir die Klasse `Store` als direkte Abhängigkeit in den Komponenten verwenden. Außerdem müssen wir die Actions und Selektoren importieren. +Um den Anwendungszustand mit NgRx zu verwalten, kommunizieren die Komponenten mit dem zentralen Store: Sie senden Actions in das System und lesen Daten als Signale aus. Dafür müssen wir die Klasse `Store` als direkte Abhängigkeit in den Komponenten verwenden. Außerdem müssen wir die Actions und Selektoren importieren. Damit sind die Komponenten stets abhängig von den Bausteinen des Frameworks NgRx. Das erschwert insbesondere das Testing und die Wiederverwendbarkeit. Außerdem müssen wir uns für das State Management auf eine spezifische Lösung festlegen. Jeglicher State wird global im Store abgelegt, und ein Mischbetrieb mit anderen Ansätzen führt potenziell zu unsauberem Code in der Komponente. Um diese Unschönheiten zu lösen, können wir eine *Facade* verwenden: Dafür erstellen wir einen Service, der eine Schnittstelle zwischen der Komponente und dem Store bildet. Die Komponente kommuniziert über Methoden und Propertys mit der Facade. Das Framework NgRx bleibt dabei verborgen, und wir halten die Komponente frei von frameworkspezifischem Code. -![Diagramm: Zwischen Component und Store liegt eine Facade. Die Component ruft Methoden der Facade auf (doThings()) und liest Daten über ein Observable (data$). Die Facade kommuniziert per dispatch() und selectSignal() mit dem Store, sodass die Component selbst keinen direkten Kontakt zu NgRx hat.](./facade.svg "NgRx Flow mit Facade") +![Diagramm: Zwischen Component und Store liegt eine Facade. Die Component ruft Methoden der Facade auf (doThings()) und liest Daten über ein Signal (data). Die Facade kommuniziert per dispatch() und selectSignal() mit dem Store, sodass die Component selbst keinen direkten Kontakt zu NgRx hat.](./facade.svg "NgRx Flow mit Facade") -Zur Implementierung nutzen wir einen einfachen Service, der mit dem Store kommuniziert. Als öffentliche Schnittstelle werden gut lesbare Methoden und Propertys angeboten. +Zur Implementierung nutzen wir einen einfachen Service, der mit dem Store kommuniziert. Als öffentliche Schnittstelle bieten wir gut lesbare Methoden und Propertys an. ```ts @Injectable() export class BooksFacade { - books$ = this.store.select(selectAllBooks); + private store = inject(Store); - constructor(private store: Store) {} + books = this.store.selectSignal(selectAllBooks); loadBooks() { this.store.dispatch(loadBooks()); @@ -1225,34 +1154,38 @@ In der Komponente nutzen wir die Facade, um die Daten aus den Propertys zu lesen ```ts @Component({ /* ... */ }) export class BookListComponent { - books$ = this.booksFacade.books$; + private booksFacade = inject(BooksFacade); - constructor(private booksFacade: BooksFacade) { + books = this.booksFacade.books; + + constructor() { this.booksFacade.loadBooks(); } } ``` -Im Unit-Test können wir einen solchen Service leicht durch einen Stub ersetzen. Die Propertys und Methoden können dabei in einem einfachen Objekt nachgebildet werden, ohne die Schnittstelle des Stores ausmocken zu müssen. Im Buch haben wir im Kapitel zum Testing auf diese Weise einen Stub für den `BookStoreService` bereitgestellt. +Im Unit-Test können wir einen solchen Service leicht durch einen Stub ersetzen. Die Propertys und Methoden können dabei in einem einfachen Objekt nachgebildet werden, ohne die Schnittstelle des Stores ausmocken zu müssen. ```ts +import { signal } from '@angular/core'; + const booksFacadeStub = { - books$: of([]), + books: signal([]), loadBooks: () => {} }; ``` Ein weiterer großer Vorteil dieser Idee liegt darin, dass wir die Implementierung für das State Management jederzeit austauschen oder variieren können. So ist es zum Beispiel möglich, bestimmte Teile des States rein lokal in der Facade zu verarbeiten und gar nicht an den zentralen NgRx Store zu übermitteln. Einen Ansatz für die Verwaltung von lokalem State bietet das Projekt `@ngrx/component-store`. Mit dem signal-basierten SignalStore stellen wir in [Teil 3 dieser Serie](/material/ngrx-signal-store) eine moderne Alternative dafür vor. -Für eine Facade empfehlen wir übrigens, keinen Tree-Shakable Provider zu verwenden. Stattdessen tragen wir die Facade unter `providers` in dem Modul ein, das auch den Feature-State bereitstellt. Auf diese Weise ist garantiert, dass der Feature-State korrekt registriert ist, wenn wir die Facade verwenden. +Für eine Facade empfehlen wir übrigens, sie dort zu registrieren, wo wir auch den Feature-State bereitstellen – also im selben `providers`-Array wie `provideState()`. Auf diese Weise ist garantiert, dass der Feature-State korrekt registriert ist, wenn wir die Facade verwenden. ## Fazit -Mithilfe von Redux und NgRx können wir die Zustandsverwaltung in der Anwendung zentralisieren. Redux setzt auf ein unveränderliches Zustandsobjekt, das mithilfe von Pure Functions im Store verwaltet wird. Alle Ereignisse der Anwendung werden durch Actions signalisiert, die Änderungen am Zustand auslösen können. Dabei erzeugen die Reducers einen neuen Zustand, der über ein Observable an alle Interessierten ausgegeben wird. Jeder Baustein von NgRx hat eine klar definierte Aufgabe, was eine konsistente Struktur in das Projekt bringt. Die Teile der Architektur sind stark entkoppelt, sodass sie unabhängig voneinander entwickelt und gewartet werden können. +Mithilfe von Redux und dem NgRx Global Store können wir die Zustandsverwaltung in der Anwendung zentralisieren. Redux setzt auf ein unveränderliches Zustandsobjekt, das mithilfe von Pure Functions im Store verwaltet wird. Alle Ereignisse der Anwendung werden durch Actions signalisiert, die Änderungen am Zustand auslösen können. Dabei erzeugen die Reducers einen neuen Zustand, den wir über Selektoren – als Observable oder als Signal – auslesen. Jeder Baustein von NgRx hat eine klar definierte Aufgabe, was eine konsistente Struktur in das Projekt bringt. Die Teile der Architektur sind stark entkoppelt, sodass wir sie unabhängig voneinander entwickeln und warten können. -Mit Redux lassen sich komplexe Strukturen in Projekten harmonisieren. Das bedeutet allerdings nicht, dass Redux für jede Anwendung die passende Architektur ist. Bewerten Sie deshalb für ein Projekt zunächst, ob sich der Einsatz von Redux wirklich lohnt. Die Thematik ist nicht trivial – um eine solche zentrale Zustandsverwaltung sicher und effektiv einzusetzen, braucht es Zeit und Übung. Die [offizielle Dokumentation von NgRx](https://ngrx.io/) ist ein guter Ausgangspunkt, um alle Funktionen und Konzepte weiter zu studieren. +Mit Redux lassen sich komplexe Strukturen in Projekten harmonisieren. Das bedeutet allerdings nicht, dass Redux für jede Anwendung die passende Architektur ist. Wir sollten deshalb für jedes Projekt zunächst bewerten, ob sich der Einsatz wirklich lohnt. Die Thematik ist nicht trivial – um eine solche zentrale Zustandsverwaltung sicher und effektiv einzusetzen, braucht es Zeit und Übung. Die [offizielle Dokumentation von NgRx](https://ngrx.io/) ist ein guter Ausgangspunkt, um alle Funktionen und Konzepte weiter zu studieren. -Zusätzlich haben wir das Beispiel aus diesem Kapitel [auf GitHub](https://github.com/book-monkey5/16e-ngrx) zur Verfügung gestellt, sodass Sie den Code vollständig nachvollziehen können. +Für viele Anwendungen ist der Global Store aber zu schwergewichtig. NgRx bietet mit dem **SignalStore** eine leichtgewichtige, signal-basierte Alternative, die wir uns im nächsten Teil ansehen. --- From 518cdcd6ef4b6ac6f0e3a7b4ec7fdcbb7e4a421a Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Fri, 12 Jun 2026 19:54:12 +0200 Subject: [PATCH 09/24] material(ngrx): Teil 2 Feinschliff nach Verifikation - '--defaults' aus 'ng add @ngrx/store' und 'ng g feature' entfernt (ist kein NgRx-Schematic-Flag) - concatLatestFrom-Historie praezisiert (in @ngrx/operators seit v18, in @ngrx/effects seit v17 deprecated) --- material/ngrx-global-store/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index 0d29a5dc..6e56d1c3 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -34,7 +34,7 @@ Als Grundlage für diesen praktischen Teil verwenden wir das Beispielprojekt Boo Im Projektverzeichnis müssen wir zunächst alle Abhängigkeiten installieren, die wir für die Arbeit mit NgRx benötigen. NgRx verfügt über eigene Schematics zur Einrichtung in einem bestehenden Angular-Projekt. Die folgenden Befehle integrieren einen vorbereiteten Store in die bestehende Anwendung: ```bash -ng add @ngrx/store --defaults +ng add @ngrx/store ng add @ngrx/store-devtools ng add @ngrx/effects ``` @@ -83,7 +83,7 @@ Mit dieser Konfiguration ist der Store zwar schon aktiv, aber wir haben noch nic Unsere Anwendung ist bereits in Features strukturiert, die einzelne Bereiche kapseln und in der Regel per Lazy Loading geladen werden. Diese Einteilung findet sich auch wieder, wenn es um die Einrichtung des Stores für NgRx geht. Jedes Feature erhält einen eigenen Satz an Actions, Reducers und Effects, die auch nur für genau dieses Feature und den zugehörigen State zuständig sind. So verhindern wir eine monolithische Struktur, in der verschiedene Zuständigkeiten ungewollt vermischt werden. Um NgRx für ein bestehendes Feature aufzusetzen, verwenden wir den folgenden Befehl: ```bash -ng g feature books/store/book --api --defaults +ng g feature books/store/book --api ``` Dieser Aufruf legt das Feature `book` im Ordner `src/app/books/store` an. Wir haben hier bewusst den Unterordner `store` gewählt, um alle Bestandteile von NgRx sauber in einem gemeinsamen Unterordner zu gruppieren. Wichtig ist, dass der Feature-Name `book` hier im Singular angegeben wird, denn die CLI fügt beim Anlegen automatisch ein Plural-s für einige Bausteine hinzu. Mit der Option `--api` generieren wir außerdem das nötige Grundgerüst, um Daten zu behandeln, die von einer API abgerufen werden. Wie sich das auswirkt, werden wir gleich noch betrachten. @@ -996,7 +996,7 @@ Als Beispiel wollen wir den bekannten Effect zum Laden der Bücher so erweitern, Es ist für dieses Szenario notwendig, dass wir den bestehenden State im Effect berücksichtigen. Dies können wir mit dem Operator `concatLatestFrom()` realisieren: Wir übergeben ein anderes Observable als Argument, und der Operator reichert den Hauptdatenstrom mit dem jeweils letzten Element aus diesem Observable an. Das bedeutet also, dass uns in den Effects neben dem Payload aus den Actions auch zusätzlich Daten aus dem Store zur Verfügung stehen. Wir verwenden hier direkt unseren Selektor `selectAllBooks`, um die aktuelle Buchliste aus dem Store zu erhalten. Anhand der Buchliste können wir dann mit dem Operator `filter()` entscheiden, ob die Bücher neu heruntergeladen werden sollen oder nicht. -> **Hinweis:** Der Operator `concatLatestFrom()` ist mit NgRx 18 vom Paket `@ngrx/effects` in das Paket `@ngrx/operators` umgezogen. Wir importieren ihn deshalb aus `@ngrx/operators`. +> **Hinweis:** Seit NgRx 18 importieren wir den Operator `concatLatestFrom()` aus dem Paket `@ngrx/operators`. Zuvor war er Teil von `@ngrx/effects` (dort seit NgRx 17 als veraltet markiert). ```ts // books/store/book.effects.ts From 1192b93eba83931a0b3d7d01638311fd688b290b Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Sat, 13 Jun 2026 00:16:28 +0200 Subject: [PATCH 10/24] material(ngrx): Teil 3 (SignalStore) geschrieben + gegen ngrx.io verifiziert Komplett neuer Artikel (war nicht im Buch). Struktur: architektonischer Unterschied zum Global Store / Vorstellung / Vergleich / Fazit. - gesamte @ngrx/signals-Doku (17 Seiten) per Headless-Chrome in den Context geholt, jede API gegen ngrx.io verifiziert (kein Raten) - signalStore/withState/withComputed/withMethods/withHooks, patchState, rxMethod, withEntities, signalStoreFeature, lokal vs. providedIn:'root' - durchgehendes BookMonkey-Beispiel analog zu Teil 2 (Buchliste + Ladeindikator) - Vergleichstabelle Global Store vs. SignalStore, Entscheidungshilfe im Fazit - ehrlich eingeordnet: keine offizielle Redux-DevTools-Anbindung, Events-Plugin (seit 19.2) fuer Flux-Stil, ersetzt @ngrx/component-store - adversarialer Review-Agent: keine erfundene API, keine falschen Signaturen; protectedState-Aussage praezisiert --- material/ngrx-signal-store/README.md | 375 ++++++++++++++++++++++++++- 1 file changed, 373 insertions(+), 2 deletions(-) diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index 4225901d..992792f3 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -1,7 +1,7 @@ --- title: "State Management mit NgRx – Teil 3: SignalStore" published: "2026-06-11" -lastModified: "2026-06-12" +lastModified: "2026-06-13" hidden: true --- @@ -15,7 +15,378 @@ Dieser Artikel ist **Teil 3** einer dreiteiligen Serie zum Thema State Managemen [[toc]] -> **Hinweis:** Dieser Teil wird gerade geschrieben. Der SignalStore aus dem Paket `@ngrx/signals` stellt eine leichtgewichtige, signal-basierte Alternative zum Global Store vor. Wir zeigen ihn am selben Beispiel wie in Teil 2 (Buchliste laden mit Ladeindikator) und geben eine Entscheidungshilfe, wann sich welcher Ansatz lohnt. +In [Teil 2](/material/ngrx-global-store) haben wir den **Global Store** kennengelernt: das klassische Redux-Pattern mit Actions, Reducers, Selektoren und Effects. Wir setzen für diesen Teil voraus, dass der Global Store bekannt ist – wir bauen direkt darauf auf und vergleichen beide Ansätze. + +Der Global Store ist mächtig, bringt für viele Anwendungsfälle aber spürbar viel Zeremonie mit: Schon das Laden einer simplen Buchliste verteilt sich über Actions, einen Reducer, Selektoren und einen Effect. Genau hier setzt der **SignalStore** aus dem Paket `@ngrx/signals` an. Er ist eine leichtgewichtige, signal-basierte Alternative, die mit deutlich weniger Code auskommt und sich nahtlos in modernes, signal-zentriertes Angular einfügt. + +> **Hinweis zur Version:** Wir verwenden **Angular 22** und die dazu passende NgRx-Version. Der SignalStore wohnt im Paket `@ngrx/signals` und wird unabhängig vom Global Store (`@ngrx/store`) installiert. + +## Der architektonische Unterschied zum Global Store + +Bevor wir Code schreiben, lohnt sich der Blick auf die grundlegend andere Architektur. Global Store und SignalStore lösen dasselbe Problem – zentrale Zustandsverwaltung – auf gegensätzliche Weise. + +**Der Global Store ist ein zentrales, indirektes System.** Es gibt genau einen globalen State-Baum. Eine Komponente *beschreibt* mit einer Action nur, *was passiert ist* ("Load Books"); ein Reducer entscheidet an ganz anderer Stelle, *wie* sich der State daraufhin ändert; ein Effect kümmert sich wiederum getrennt um Seiteneffekte. Lesen und Schreiben sind strikt entkoppelt, der Datenfluss ist streng unidirektional. Diese Indirektion ist der eigentliche Wert des Patterns: Jeder Schritt ist nachvollziehbar, auditierbar (Redux DevTools, Time Travel) und klar getrennt – sie ist aber auch die Quelle des vielen Codes. + +**Der SignalStore ist ein zusammengesetzter Service mit direktem Zugriff.** Ein Store ist hier kein globaler Baum, sondern ein ganz normaler, injizierbarer Service, den wir *deklarativ aus Bausteinen zusammensetzen* (`withState`, `withComputed`, `withMethods`, …). Den Zustand lesen wir direkt als **Signal** (`store.books()`), und wir ändern ihn, indem wir eine selbst definierte **Methode** aufrufen, die den State über `patchState()` aktualisiert. Es gibt keine verpflichtende Trennung in Action und Reducer mehr: Das "Was passiert" und das "Wie ändert sich der State" liegen zusammen in einer Methode. Das ist weniger Indirektion, weniger Code – aber eben auch weniger eingebaute Nachvollziehbarkeit. + +Ein zweiter, ebenso wichtiger Unterschied ist der **Geltungsbereich**: + +- Der **Global Store** ist immer **global**. Jeder Feature-State hängt im einen zentralen State-Objekt. +- Ein **SignalStore** kann **global *oder* lokal** sein. Wir können ihn mit `{ providedIn: 'root' }` global bereitstellen – oder ihn an eine Komponente binden, sodass er mit der Komponente entsteht und wieder zerstört wird. Damit eignet sich der SignalStore auch hervorragend für komponenten- oder feature-lokalen Zustand. Konzeptionell ersetzt er damit auch das ältere Projekt `@ngrx/component-store`. + +Kurz gesagt: Der Global Store ist *ein* großes, strenges System. Der SignalStore ist eine Sammlung *vieler* kleiner, flexibler Stores. + +## Der SignalStore im Detail + +Schauen wir uns die Bausteine konkret an. Als durchgehendes Beispiel verwenden wir wieder den BookMonkey und laden – genau wie in Teil 2 – die Buchliste vom Server, samt Ladeindikator. So lässt sich der direkte Vergleich zum Global Store ziehen. + +### Installation + +Der SignalStore wird über ein eigenes Paket installiert: + +```bash +ng add @ngrx/signals +``` + +Anders als beim Global Store gibt es hier **nichts in der `app.config.ts` zu registrieren** – kein `provideStore()`, kein `provideEffects()`. Ein SignalStore ist ein gewöhnlicher Service und wird dort bereitgestellt, wo wir ihn brauchen. + +### Ein Store als Komposition von Features + +Einen SignalStore erzeugen wir mit der Funktion `signalStore()`. Sie nimmt eine Reihe von *Features* entgegen und gibt einen injizierbaren Service zurück. Das grundlegendste Feature ist `withState()`: Es definiert den initialen Zustand. Für jeden State-Slice wird automatisch ein passendes Signal erzeugt. + +```ts +// books/book.store.ts +import { signalStore, withState } from '@ngrx/signals'; +import { Book } from '../shared/book'; + +type BookState = { + books: Book[]; + loading: boolean; +}; + +const initialState: BookState = { + books: [], + loading: false +}; + +export const BookStore = signalStore( + withState(initialState) +); +``` + +Allein dadurch besitzt eine Instanz von `BookStore` bereits zwei Signale: `books: Signal` und `loading: Signal`. Verschachtelte Objekte werden übrigens zu einem `DeepSignal`, das zusätzlich für jede Eigenschaft ein eigenes Signal bereitstellt. + +### Den Store bereitstellen: lokal oder global + +Ein SignalStore ist standardmäßig **an keinen Injector gebunden**. Wir entscheiden bewusst, wo er lebt. + +**Global** (eine geteilte Instanz für die ganze Anwendung) – über die Option `{ providedIn: 'root' }` direkt bei der Definition: + +```ts +export const BookStore = signalStore( + { providedIn: 'root' }, + withState(initialState) +); +``` + +**Lokal** (eine eigene Instanz pro Komponente, gebunden an deren Lebenszyklus) – über das `providers`-Array der Komponente oder Route: + +```ts +@Component({ + // ... + providers: [BookStore] +}) +export class BookListComponent { + private store = inject(BookStore); +} +``` + +Diese Wahlfreiheit ist einer der größten Vorteile gegenüber dem Global Store: Zustand, der nur eine Komponente betrifft, muss nicht global abgelegt werden. + +### Zustand lesen + +Da jeder State-Slice ein Signal ist, lesen wir ihn direkt – im Code wie im Template. Eine `AsyncPipe` brauchen wir nicht mehr; wir rufen das Signal einfach als Funktion auf: + +```html +

    Books

    + +@if (store.loading()) { +
    Loading ...
    +} @else { +
      + @for (book of store.books(); track book.isbn) { +
    • + } +
    +} +``` + +Der Vergleich zu Teil 2 ist aufschlussreich: Dort haben wir das Signal erst über `store.selectSignal(selectAllBooks)` aus dem globalen Store herausgelöst. Beim SignalStore *ist* der State bereits ein Signal – ein Zwischenschritt über Selektoren entfällt. + +### Abgeleitete Werte: `withComputed` + +Berechnete Werte ergänzen wir mit dem Feature `withComputed()`. Es bekommt die bereits definierten Signale hinein und gibt neue, abgeleitete Signale zurück – das Pendant zu den Selektoren des Global Store, nur ohne separate Datei und ohne `createSelector()`. + +```ts +import { computed } from '@angular/core'; +import { signalStore, withComputed, withState } from '@ngrx/signals'; + +export const BookStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withComputed(({ books }) => ({ + booksCount: computed(() => books().length) + })) +); +``` + +Wie bei den memoisierten Selektoren in Teil 2 wird auch ein `computed`-Signal nur neu berechnet, wenn sich seine Eingaben tatsächlich ändern. + +### Zustand ändern: `withMethods` und `patchState` + +Verhalten fügen wir mit dem Feature `withMethods()` hinzu. Die Factory erhält die Store-Instanz (und kann über `inject()` Abhängigkeiten anfordern) und gibt ein Objekt mit Methoden zurück. Den Zustand aktualisieren wir innerhalb der Methoden mit der Funktion `patchState()` – immer **immutabel**, ganz wie in einem Reducer: + +```ts +import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; + +export const BookStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withMethods((store) => ({ + reset(): void { + patchState(store, { books: [], loading: false }); + } + })) +); +``` + +`patchState()` nimmt den Store gefolgt von einem partiellen State-Objekt oder einer Updater-Funktion entgegen. Hier liegt der zentrale Unterschied zum Global Store: Es gibt keine Action und keinen Reducer als Zwischenschicht – die Methode beschreibt die Zustandsänderung direkt. + +Übrigens ist der State eines SignalStore standardmäßig **vor Änderungen von außen geschützt** (Option `protectedState`, Default `true`): `patchState()` greift dann nur innerhalb des Stores. Erst wenn wir den Store mit `{ protectedState: false }` definieren, sind Änderungen auch von außen möglich. Der geschützte Standard hält den Datenfluss kontrollierbar – ähnlich der read-only-Eigenschaft des Global-Store-States. + +### Asynchrone Seiteneffekte: `rxMethod` + +Für das Laden der Bücher brauchen wir einen asynchronen Seiteneffekt – die Aufgabe, die im Global Store ein Effect übernommen hat. Einfache asynchrone Abläufe lassen sich direkt mit einer `async`-Methode und einem Promise erledigen. Für reaktive Abläufe mit RxJS bietet das Interop-Plugin die Funktion `rxMethod()` aus `@ngrx/signals/rxjs-interop`. Sie nimmt eine Kette von RxJS-Operatoren entgegen und gibt eine reaktive Methode zurück. + +Damit bauen wir den vollständigen `BookStore`. Zum sicheren Verarbeiten der HTTP-Antwort nutzen wir den Operator `tapResponse` aus `@ngrx/operators`: + +```ts +// books/book.store.ts +import { computed, inject } from '@angular/core'; +import { pipe, switchMap, tap } from 'rxjs'; +import { + patchState, + signalStore, + withComputed, + withMethods, + withState +} from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { tapResponse } from '@ngrx/operators'; + +import { Book } from '../shared/book'; +import { BookStoreService } from '../shared/book-store.service'; + +type BookState = { + books: Book[]; + loading: boolean; +}; + +const initialState: BookState = { + books: [], + loading: false +}; + +export const BookStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withComputed(({ books }) => ({ + booksCount: computed(() => books().length) + })), + withMethods((store, service = inject(BookStoreService)) => ({ + loadBooks: rxMethod( + pipe( + tap(() => patchState(store, { loading: true })), + switchMap(() => + service.getAll().pipe( + tapResponse({ + next: books => patchState(store, { books }), + error: console.error, + finalize: () => patchState(store, { loading: false }) + }) + ) + ) + ) + ) + })) +); +``` + +Hier steckt der gesamte Lade-Ablauf in *einer* Datei und *einer* Methode: Ladeindikator setzen, HTTP-Request mit `switchMap()` auslösen, Ergebnis in den State schreiben, Ladeindikator zurücksetzen. In Teil 2 war für genau diese Logik ein Effect, drei Actions und ein Reducer nötig. + +Die Komponente injiziert den Store und stößt das Laden an – mehr braucht sie nicht: + +```ts +// books/book-list/book-list.component.ts +import { Component, inject } from '@angular/core'; +import { BookStore } from '../book.store'; + +@Component({ + selector: 'bm-book-list', + templateUrl: './book-list.component.html' +}) +export class BookListComponent { + protected store = inject(BookStore); + + constructor() { + this.store.loadBooks(); + } +} +``` + +### Lebenszyklus: `withHooks` + +Oft soll ein Store sich beim Start selbst initialisieren, statt dass eine Komponente das Laden anstößt. Dafür gibt es das Feature `withHooks()` mit den Methoden `onInit` und `onDestroy`: + +```ts +import { withHooks } from '@ngrx/signals'; + +export const BookStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withMethods(/* ... */), + withHooks({ + onInit(store) { + store.loadBooks(); + } + }) +); +``` + +Damit lädt der Store seine Daten von selbst, und die Komponente liest nur noch die Signale aus. + +### Entitäten verwalten: `withEntities` + +Für Sammlungen von Objekten bietet das Plugin `@ngrx/signals/entities` das Feature `withEntities()` – das Gegenstück zu `@ngrx/entity` aus Teil 2. Es legt die Signale `entityMap`, `ids` und `entities` an und bringt eine Reihe von Updatern mit (`addEntity`, `setAllEntities`, `updateEntity`, `removeEntity`, …), die wir mit `patchState()` kombinieren. + +Standardmäßig erwartet `withEntities` ein Property `id`. Da ein Buch im BookMonkey über seine `isbn` identifiziert wird, geben wir – wie schon bei `@ngrx/entity` – einen eigenen ID-Selektor an: + +```ts +import { inject } from '@angular/core'; +import { switchMap } from 'rxjs'; +import { patchState, signalStore, withMethods } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { tapResponse } from '@ngrx/operators'; +import { + SelectEntityId, + setAllEntities, + withEntities +} from '@ngrx/signals/entities'; + +import { Book } from '../shared/book'; +import { BookStoreService } from '../shared/book-store.service'; + +const selectId: SelectEntityId = book => book.isbn; + +export const BookStore = signalStore( + { providedIn: 'root' }, + withEntities(), + withMethods((store, service = inject(BookStoreService)) => ({ + loadBooks: rxMethod( + switchMap(() => + service.getAll().pipe( + tapResponse({ + next: books => patchState(store, setAllEntities(books, { selectId })), + error: console.error + }) + ) + ) + ) + })) +); +``` + +Über das Signal `store.entities()` erhalten wir die Buchliste als Array – die mühsame Pflege von `entityMap` und `ids` übernimmt das Plugin. + +### Wiederverwendbare Bausteine: `signalStoreFeature` + +Ein besonders elegantes Konzept sind eigene Features. Mit `signalStoreFeature()` bündeln wir State, computed Signals und Methoden zu einem wiederverwendbaren Baustein – etwa einen Lade-Status, den viele Stores brauchen: + +```ts +import { computed } from '@angular/core'; +import { signalStoreFeature, withComputed, withState } from '@ngrx/signals'; + +export function withRequestStatus() { + return signalStoreFeature( + withState<{ requestStatus: 'idle' | 'pending' | 'fulfilled' }>({ + requestStatus: 'idle' + }), + withComputed(({ requestStatus }) => ({ + isPending: computed(() => requestStatus() === 'pending') + })) + ); +} +``` + +Dieses Feature lässt sich anschließend in beliebig vielen Stores einsetzen: `signalStore(withEntities(), withRequestStatus())`. Eine derart einfache Wiederverwendung quer über Stores hinweg gibt es im Global Store nicht. + +### Testing + +Da ein SignalStore ein gewöhnlicher Service ist, testen wir ihn auch wie einen Service: Instanz beziehen, Methoden aufrufen, Signale auslesen. Mocks und ein vollständiges Store-Setup wie beim Global Store sind nicht nötig: + +```ts +import { TestBed } from '@angular/core/testing'; + +describe('BookStore', () => { + it('zählt die geladenen Bücher', () => { + const store = TestBed.inject(BookStore); + + expect(store.booksCount()).toBe(0); + // ... Bücher laden ... + expect(store.booksCount()).toBe(3); + }); +}); +``` + +> **Hinweis:** Injiziert eine Methode einen Service, ersetzen wir ihn im Test wie gewohnt per `{ provide: …, useValue: … }`. Den geschützten State können wir in Tests mit dem Helfer `unprotected()` aus `@ngrx/signals/testing` gezielt setzen. + +### Was ist mit den Redux DevTools? + +Ein ehrlicher Punkt zum Abschluss der Vorstellung: Für den SignalStore gibt es **keine offizielle Anbindung an die Redux DevTools** und damit auch kein eingebautes Time Travel Debugging wie in Teil 2. Die Angular DevTools sollen Signals künftig unterstützen; bis dahin bietet das Community-Paket `@angular-architects/ngrx-toolkit` ein Feature `withDevtools()` an. + +Wer den klaren, ereignisbasierten Datenfluss des Redux-Patterns auch mit dem SignalStore möchte, kann das **Events-Plugin** nutzen (`@ngrx/signals/events`, seit NgRx 19.2). Es ergänzt den SignalStore um Events, Reducer und Effects und stellt so einen unidirektionalen Fluss im Stil von Redux her – die beiden Welten lassen sich also annähern. + +## SignalStore und Global Store im Vergleich + +Beide Bausteine stammen aus dem NgRx-Projekt und lösen dieselbe Aufgabe, setzen aber unterschiedliche Schwerpunkte: + +| Aspekt | Global Store (`@ngrx/store`) | SignalStore (`@ngrx/signals`) | +| --- | --- | --- | +| Grundidee | zentrales Redux-Pattern | deklarativ zusammengesetzter Service | +| Anzahl Stores | ein globaler State-Baum | viele kleine Stores | +| Geltungsbereich | immer global | lokal (komponentengebunden) **oder** global | +| Lesen | Selektoren + `selectSignal()` | State *ist* direkt ein Signal: `store.books()` | +| Schreiben | Action `dispatch()` → Reducer | Methode aufrufen → `patchState()` | +| Indirektion | hoch (Action/Reducer/Effect getrennt) | gering (Methode ändert State direkt) | +| Seiteneffekte | Effects (`@ngrx/effects`) | Methoden (`async`) oder `rxMethod` | +| Boilerplate | viel (Actions, Reducer, Selektoren, Effects) | wenig (`with…`-Features in einer Datei) | +| Entitäten | `@ngrx/entity` | `withEntities` (`@ngrx/signals/entities`) | +| DevTools / Time Travel | ja (Redux DevTools) | nicht offiziell (Angular DevTools / Community) | +| Erzwungener unidirektionaler Fluss | ja | optional (Events-Plugin) | + +Der entscheidende Gegensatz: Der Global Store kauft sich mit *mehr Code* *mehr Struktur und Nachvollziehbarkeit*. Der SignalStore kauft sich mit *weniger Struktur* *deutlich weniger Code und mehr Flexibilität* – insbesondere die Möglichkeit, Zustand lokal zu halten. + +## Fazit: Wann was? + +SignalStore und Global Store sind keine Konkurrenten, die einander ablösen – sie decken unterschiedliche Bedürfnisse ab. + +**Der SignalStore ist heute für die meisten Fälle die naheliegende erste Wahl.** Er ist signal-nativ, kommt mit wenig Code aus, lässt sich lokal wie global einsetzen und ist leicht zu testen. Für komponenten- oder feature-lokalen Zustand, für überschaubare Anwendungen und überall dort, wo der volle Redux-Apparat zu schwer wäre, ist er ideal. Er ersetzt damit auch das ältere `@ngrx/component-store`. + +**Der Global Store lohnt sich, wenn die Stärken des Redux-Patterns wirklich gebraucht werden:** ein großer, von vielen Stellen geteilter und komplex verwobener Anwendungszustand, ein strikt nachvollziehbarer, auditierbarer Datenfluss und der Komfort der Redux DevTools mit Time Travel. In großen Anwendungen und Teams kann die strenge, einheitliche Struktur ein echter Vorteil sein – genau die Investition, die wir in Teil 2 beschrieben haben. + +Eine gute Faustregel: **Wir beginnen mit dem SignalStore und greifen zum Global Store, wenn die Anwendung dessen Struktur und Werkzeuge tatsächlich erfordert.** Beide Ansätze lassen sich in einer Anwendung auch kombinieren – und mit dem Events-Plugin nähert sich der SignalStore bei Bedarf sogar dem Redux-Stil an. + +Damit schließen wir unsere dreiteilige Reise durch das State Management mit NgRx ab: vom selbst hergeleiteten Modell in [Teil 1](/material/ngrx-intro) über den klassischen [Global Store](/material/ngrx-global-store) bis zum modernen SignalStore. Welcher Weg der richtige ist, entscheidet am Ende die konkrete Anwendung – und mit beiden Werkzeugen im Gepäck treffen wir diese Entscheidung fundiert. --- From 14258601092ef55fab843d40e2b4918f3e0d161a Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Sat, 13 Jun 2026 09:12:43 +0200 Subject: [PATCH 11/24] material(ngrx): Review-Fixes Teil 1-3 (Sprache, Inhalt, Struktur) Form-neutrale Korrekturen aus dem kritischen Lektorat: - T1: Em-Dash in Zitat-Attribution -> Halbgeviertstrich; 'weiter gehen' -> 'weiter treiben'; toter Vorwaerts-Verweis -> Link auf Teil 2; shareReplay-Formulierung praezisiert - T2: 'Lassen Sie uns' -> 'Fassen wir' (letzte Sie-Form); fehlende Leerzeile vor Ueberschrift; Effect-Fehlerfall klarer; root- vs. feature-provideEffects erklaert - T3: 'haengt im einen' -> korrekt; Kongruenz 'waren'; Kasus 'einem Lade-Status'; Wiederverwendungs-Aussage entschaerft; withEntities als Variante gekennzeichnet; component-store-Redundanz entfernt; Wortwdh. Anrede-Umstellung (du) noch offen (Rueckfrage). --- material/ngrx-global-store/README.md | 7 ++++--- material/ngrx-intro/README.md | 8 ++++---- material/ngrx-signal-store/README.md | 14 +++++++------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index 6e56d1c3..e1399c96 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -106,7 +106,7 @@ src/ └── ... ``` -Den Feature-State und die Effects registrieren wir dort, wo wir auch das Feature selbst bereitstellen – typischerweise im `providers`-Array der zugehörigen Route. Dazu nutzen wir `provideState()` für den Reducer und `provideEffects()` für die Effects-Klasse: +Den Feature-State und die Effects registrieren wir dort, wo wir auch das Feature selbst bereitstellen – typischerweise im `providers`-Array der zugehörigen Route. Dazu nutzen wir `provideState()` für den Reducer und `provideEffects()` für die Effects-Klasse. Das leere `provideEffects()` aus der `app.config.ts` richtet nur die Effects-Infrastruktur ein; die eigentlichen Effects-Klassen registrieren wir pro Feature: ```ts // books/books.routes.ts @@ -130,6 +130,7 @@ export const booksRoutes: Routes = [ ``` Dieser Aufruf von `provideState()` ist essenziell, denn er definiert die Struktur des globalen State-Objekts. Die Konstante `fromBook.bookFeatureKey` aus der Datei `book.reducer.ts` enthält den String `book`. Damit legen wir fest, unter welchem Namen die Zustände dieses Features im globalen State-Objekt zu finden sein werden. Das zentrale State-Objekt wird also durch diesen Aufruf von `provideState()` automatisch erweitert, und die Reducers aus dem Feature werden in die Anwendung integriert. Diese dynamische Erweiterung ist nötig, damit ein Feature auch mithilfe von Lazy Loading asynchron zur Laufzeit nachgeladen werden kann. Der Feature-Key `book` verweist auf den Teilbaum im State-Objekt, in den der Feature-State eingebaut wird. + ### Struktur des Feature-States definieren Nun folgt der erste inhaltliche Schritt auf dem Weg zum State Management: Wir müssen die Struktur des Feature-States für das Feature `book` definieren. Dazu befindet sich in der Datei `books/store/book.reducer.ts` ein Interface mit dem Namen `State`. Dieser Feature-State ist der erste Zweig des zentralen Objekt-Baums. @@ -363,7 +364,7 @@ Für alle unbekannten Actions liefert der Reducer automatisch den aktuellen Stat ### Selektoren: Daten aus dem State lesen -Lassen Sie uns kurz zusammenfassen, wie weit wir bisher gekommen sind: Wir haben Action Creators definiert und die Action `loadBooks` von der `BookListComponent` aus in den Store dispatcht. Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. +Fassen wir kurz zusammen, wie weit wir bisher gekommen sind: Wir haben Action Creators definiert und die Action `loadBooks` von der `BookListComponent` aus in den Store dispatcht. Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. Um den Kreislauf des Datenflusses zu schließen, wollen wir die Daten aus dem State nun auslesen und in der Komponente darstellen. Der Store gibt dabei stets den vollständigen State aus. Um einzelne Teile daraus zu lesen, benötigen wir eine Projektion. In der Praxis werden diese Lesezugriffe schnell komplexer: Mitunter wollen wir Daten nicht nur einfach auslesen, sondern Projektionen über verschiedene Teile des States ausführen. Stellen wir uns vor, wir besitzen eine Liste von Autoren und Autorinnen und eine Liste von Büchern – wollen aber nun nur die Bücher ausgeben, die von einer bestimmten Person verfasst wurden. Dazu ist zusätzliche Logik nötig, die nicht in die Komponenten gehört. Stattdessen lagern wir diese Logik in separate Funktionen aus, die unabhängig von den Komponenten sind. Wir können eine solche Funktion mit einer Datenbankabfrage vergleichen: Die Query wird einmal definiert und kann beliebig komplex sein. Verschiedene Teile der Anwendung können diese Query nutzen und die Daten genau im benötigten Format erhalten. Die Funktionen zur Abfrage von Daten aus dem Store werden *Selektoren* genannt. @@ -526,7 +527,7 @@ Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookS Auf diese Weise erzeugen wir einen Datenstrom, der nacheinander verschiedene Actions ausgeben kann. Die Funktion `createEffect()` sorgt dafür, dass dieser Datenstrom automatisch abonniert wird. Die resultierenden Actions werden in den Store dispatcht und durchlaufen dort die Reducers. In einem Effect sollten wir also niemals selbst die Methode `store.dispatch()` aufrufen. -Bei der Fehlerbehandlung ist es wichtig, den Fehler so dicht wie möglich dort abzufangen, wo er entsteht. Wir setzen `catchError()` deshalb nicht im Hauptdatenstrom ein, sondern hängen `map()` und `catchError()` direkt an den Serviceaufruf an. Das `switchMap()` verarbeitet also ein Observable, das in jedem Fall eine Action liefert – so wird der Hauptdatenstrom nicht durch Fehler gefährdet. Wird ein Effect durch einen Fehler im Hauptdatenstrom gestört, so wird das zugrunde liegende Observable beendet, und die Anwendung kann nicht mehr auf eine weitere Nachricht durch den beendeten Effect reagieren. +Bei der Fehlerbehandlung ist es wichtig, den Fehler so dicht wie möglich dort abzufangen, wo er entsteht. Wir setzen `catchError()` deshalb nicht im Hauptdatenstrom ein, sondern hängen `map()` und `catchError()` direkt an den Serviceaufruf an. Das `switchMap()` verarbeitet also ein Observable, das in jedem Fall eine Action liefert – so wird der Hauptdatenstrom nicht durch Fehler gefährdet. Wird ein Effect durch einen Fehler im Hauptdatenstrom gestört, so wird das zugrunde liegende Observable beendet, und der Effect kann danach nicht mehr auf weitere Actions reagieren. Probieren wir die Anwendung nun aus und beobachten wir auch die Actions in den Redux DevTools! Nachdem die Buchliste geladen wurde, wird `loadBooksSuccess` dispatcht, und der Reducer fügt die Buchliste in den State ein. Die Selektoren in der Komponente reagieren darauf, und die Bücher werden dargestellt. diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md index 09bf894d..eb8eba4b 100644 --- a/material/ngrx-intro/README.md +++ b/material/ngrx-intro/README.md @@ -17,7 +17,7 @@ Dieser Artikel ist **Teil 1** einer dreiteiligen Serie zum Thema State Managemen > NgRx provides robust state management for small and large projects. It enforces proper separation of concerns. Using it from the start reduces the risk of spaghetti when the project evolves. > -> — Minko Gechev, Mitglied des Angular-Teams +> – Minko Gechev, Mitglied des Angular-Teams Wir haben unsere Anwendung bisher stets komponentenzentriert und serviceorientiert aufgebaut: Die Komponenten unserer Anwendung kommunizieren auf klar definierten Wegen über Property Bindings und Event Bindings. Um Daten zu erhalten und zu senden, nutzen die Komponenten verschiedene Services, in denen die HTTP-Kommunikation gekapselt ist oder über die wir Daten austauschen können. @@ -30,7 +30,7 @@ Mit wachsender Größe der Anwendung ergeben sich immer wieder folgende Fragen: - Wie reagieren wir an verschiedenen Stellen auf Ereignisse, die in der Anwendung auftreten? - Wie verwalten wir die Daten, die über die gesamte Anwendung verteilt sind? -Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookStoreService`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter gehen: Bislang waren Komponenten die "Hüterinnen" der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine abgeschottete Einheit zu den anderen Komponenten. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. +Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookStoreService`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter treiben: Bislang waren Komponenten die "Hüterinnen" der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine abgeschottete Einheit zu den anderen Komponenten. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. Wir wollen in diesem Kapitel besprechen, wie eine solche zentrale Zustandsverwaltung (engl. *State Management*) realisiert werden kann. Dabei lernen wir das Architekturmuster *Redux* kennen und nutzen das populäre Framework *Reactive Extensions for Angular (NgRx)*, um den Anwendungszustand zu verwalten und unsere Prozesse zu koordinieren. @@ -272,11 +272,11 @@ const result = messages.reduce(calculateState, initialState); // Erwartetes Ergebnis: { counter: 1, anotherProperty: 'foobar' } ``` -Mit einer solchen Reducer-Funktion und einer Liste von Nachrichten können wir demnach jeden gewünschten Zustand erzeugen. Wichtig ist dabei vor allem, dass die Reducer-Funktion eine *Pure Function* ist. Sie liefert also für die gleichen Eingabewerte stets die gleiche Ausgabe und erzeugt keine Seiteneffekte. Dazu darf die Funktion ausschließlich die übergebenen Parameter verwenden und keinen eigenen Zustand verwalten. Wir gehen auf die Eigenschaften einer Pure Function später noch genauer ein. +Mit einer solchen Reducer-Funktion und einer Liste von Nachrichten können wir demnach jeden gewünschten Zustand erzeugen. Wichtig ist dabei vor allem, dass die Reducer-Funktion eine *Pure Function* ist. Sie liefert also für die gleichen Eingabewerte stets die gleiche Ausgabe und erzeugt keine Seiteneffekte. Dazu darf die Funktion ausschließlich die übergebenen Parameter verwenden und keinen eigenen Zustand verwalten. Auf die Eigenschaften einer Pure Function gehen wir in [Teil 2](/material/ngrx-global-store) noch genauer ein. In den vorherigen Beispielen haben wir allerdings kein Array von Nachrichten verwendet, sondern alle eingehenden Nachrichten wurden direkt an `calculateState()` weitergegeben. Wir wollen den Service nun etwas umstrukturieren: Dazu setzen wir ein neues Subject ein, das alle Nachrichten nacheinander in einem Datenstrom `messages$` liefert. Die Methode `dispatch()` leitet die Nachrichten also nur an das Subject weiter. -Wir wollen dann erneut die Funktion `calculateState()` nutzen, um aus der Sammlung aller Nachrichten den jeweils neuen Zustand zu generieren. Dieses Mal greifen wir auf das große Toolset von RxJS zurück und verwenden den Operator `scan()`. Das ehemalige `BehaviorSubject` für den State wird von `shareReplay(1)` abgelöst, um das resultierende Observable mit allen Subscribern zu teilen und den jeweils letzten Wert an alle neuen Subscriber zu übermitteln. Um den Prozess einmalig anzustoßen, nutzen wir außerdem den Operator `startWith()` und erzeugen ein erstes Element im Strom der Nachrichten. +Wir wollen dann erneut die Funktion `calculateState()` nutzen, um aus der Sammlung aller Nachrichten den jeweils neuen Zustand zu generieren. Dieses Mal greifen wir auf das große Toolset von RxJS zurück und verwenden den Operator `scan()`. Die Caching-Eigenschaft des ehemaligen `BehaviorSubject` übernimmt nun `shareReplay(1)`: So teilen wir das resultierende Observable mit allen Subscribern und übermitteln den jeweils letzten Wert an alle neuen Subscriber. Um den Prozess einmalig anzustoßen, nutzen wir außerdem den Operator `startWith()` und erzeugen ein erstes Element im Strom der Nachrichten. ```ts @Injectable({ providedIn: 'root' }) diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index 992792f3..88c780f6 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -31,7 +31,7 @@ Bevor wir Code schreiben, lohnt sich der Blick auf die grundlegend andere Archit Ein zweiter, ebenso wichtiger Unterschied ist der **Geltungsbereich**: -- Der **Global Store** ist immer **global**. Jeder Feature-State hängt im einen zentralen State-Objekt. +- Der **Global Store** ist immer **global**. Jeder Feature-State ist Teil des einen zentralen State-Objekts. - Ein **SignalStore** kann **global *oder* lokal** sein. Wir können ihn mit `{ providedIn: 'root' }` global bereitstellen – oder ihn an eine Komponente binden, sodass er mit der Komponente entsteht und wieder zerstört wird. Damit eignet sich der SignalStore auch hervorragend für komponenten- oder feature-lokalen Zustand. Konzeptionell ersetzt er damit auch das ältere Projekt `@ngrx/component-store`. Kurz gesagt: Der Global Store ist *ein* großes, strenges System. Der SignalStore ist eine Sammlung *vieler* kleiner, flexibler Stores. @@ -222,7 +222,7 @@ export const BookStore = signalStore( ); ``` -Hier steckt der gesamte Lade-Ablauf in *einer* Datei und *einer* Methode: Ladeindikator setzen, HTTP-Request mit `switchMap()` auslösen, Ergebnis in den State schreiben, Ladeindikator zurücksetzen. In Teil 2 war für genau diese Logik ein Effect, drei Actions und ein Reducer nötig. +Hier steckt der gesamte Lade-Ablauf in *einer* Datei und *einer* Methode: Ladeindikator setzen, HTTP-Request mit `switchMap()` auslösen, Ergebnis in den State schreiben, Ladeindikator zurücksetzen. In Teil 2 waren für genau diese Logik ein Effect, drei Actions und ein Reducer nötig. Die Komponente injiziert den Store und stößt das Laden an – mehr braucht sie nicht: @@ -306,11 +306,11 @@ export const BookStore = signalStore( ); ``` -Über das Signal `store.entities()` erhalten wir die Buchliste als Array – die mühsame Pflege von `entityMap` und `ids` übernimmt das Plugin. +In dieser Variante ersetzt `withEntities` das manuelle `books`-Array aus `withState`. Über das Signal `store.entities()` erhalten wir die Buchliste als Array – die mühsame Pflege von `entityMap` und `ids` übernimmt das Plugin. ### Wiederverwendbare Bausteine: `signalStoreFeature` -Ein besonders elegantes Konzept sind eigene Features. Mit `signalStoreFeature()` bündeln wir State, computed Signals und Methoden zu einem wiederverwendbaren Baustein – etwa einen Lade-Status, den viele Stores brauchen: +Ein besonders elegantes Konzept sind eigene Features. Mit `signalStoreFeature()` bündeln wir State, computed Signals und Methoden zu einem wiederverwendbaren Baustein – etwa einem Lade-Status, den viele Stores brauchen: ```ts import { computed } from '@angular/core'; @@ -328,7 +328,7 @@ export function withRequestStatus() { } ``` -Dieses Feature lässt sich anschließend in beliebig vielen Stores einsetzen: `signalStore(withEntities(), withRequestStatus())`. Eine derart einfache Wiederverwendung quer über Stores hinweg gibt es im Global Store nicht. +Dieses Feature lässt sich anschließend in beliebig vielen Stores einsetzen: `signalStore(withEntities(), withRequestStatus())`. Eine derart bequeme, geschlossene Wiederverwendung quer über Stores hinweg bietet der Global Store nicht. ### Testing @@ -374,13 +374,13 @@ Beide Bausteine stammen aus dem NgRx-Projekt und lösen dieselbe Aufgabe, setzen | DevTools / Time Travel | ja (Redux DevTools) | nicht offiziell (Angular DevTools / Community) | | Erzwungener unidirektionaler Fluss | ja | optional (Events-Plugin) | -Der entscheidende Gegensatz: Der Global Store kauft sich mit *mehr Code* *mehr Struktur und Nachvollziehbarkeit*. Der SignalStore kauft sich mit *weniger Struktur* *deutlich weniger Code und mehr Flexibilität* – insbesondere die Möglichkeit, Zustand lokal zu halten. +Der entscheidende Gegensatz: Der Global Store erkauft sich mit *mehr Code* *mehr Struktur und Nachvollziehbarkeit*. Der SignalStore bekommt für *weniger Struktur* *deutlich weniger Code und mehr Flexibilität* – insbesondere die Möglichkeit, Zustand lokal zu halten. ## Fazit: Wann was? SignalStore und Global Store sind keine Konkurrenten, die einander ablösen – sie decken unterschiedliche Bedürfnisse ab. -**Der SignalStore ist heute für die meisten Fälle die naheliegende erste Wahl.** Er ist signal-nativ, kommt mit wenig Code aus, lässt sich lokal wie global einsetzen und ist leicht zu testen. Für komponenten- oder feature-lokalen Zustand, für überschaubare Anwendungen und überall dort, wo der volle Redux-Apparat zu schwer wäre, ist er ideal. Er ersetzt damit auch das ältere `@ngrx/component-store`. +**Der SignalStore ist heute für die meisten Fälle die naheliegende erste Wahl.** Er ist signal-nativ, kommt mit wenig Code aus, lässt sich lokal wie global einsetzen und ist leicht zu testen. Für komponenten- oder feature-lokalen Zustand, für überschaubare Anwendungen und überall dort, wo der volle Redux-Apparat zu schwer wäre, ist er ideal. **Der Global Store lohnt sich, wenn die Stärken des Redux-Patterns wirklich gebraucht werden:** ein großer, von vielen Stellen geteilter und komplex verwobener Anwendungszustand, ein strikt nachvollziehbarer, auditierbarer Datenfluss und der Komfort der Redux DevTools mit Time Travel. In großen Anwendungen und Teams kann die strenge, einheitliche Struktur ein echter Vorteil sein – genau die Investition, die wir in Teil 2 beschrieben haben. From 7c5ce4b1bae35c9bf2568c7971c3cd509d0c244d Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Sat, 13 Jun 2026 10:53:18 +0200 Subject: [PATCH 12/24] material(ngrx): Teil 1 letzte formelle Sie-Anrede auf wir-Form (Serien-Konsistenz) - 'Bitte beachten Sie' -> 'Wir muessen beachten' - 'Welches der Frameworks Sie ... Ihren Favoriten' -> wir/unseren, 'Ihnen empfehlen' -> 'empfehlen' - Pronomen 'sie' (Methode/Funktion/Frameworks) bleiben korrekt - lastModified T1+T2 auf 2026-06-13 Alle drei Teile jetzt durchgaengig wir-Form (konsistent mit den uebrigen Material-Artikeln). --- material/ngrx-global-store/README.md | 2 +- material/ngrx-intro/README.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index e1399c96..ab54299f 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -1,7 +1,7 @@ --- title: "State Management mit NgRx – Teil 2: Global Store mit NgRx" published: "2026-06-11" -lastModified: "2026-06-12" +lastModified: "2026-06-13" hidden: true --- diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md index eb8eba4b..2b35d040 100644 --- a/material/ngrx-intro/README.md +++ b/material/ngrx-intro/README.md @@ -1,7 +1,7 @@ --- title: "State Management mit NgRx – Teil 1: Wie kommen wir zu zentralem State Management?" published: "2026-06-11" -lastModified: "2026-06-12" +lastModified: "2026-06-13" hidden: true --- @@ -166,7 +166,7 @@ export class StateService { Wir haben durch die "Pseudo-Immutability" den Weg geebnet, um die Strategie für die Change Detection zu optimieren: Wenn ein Objekt bei einer Änderung stets eine neue Referenz erhält, so können wir in den Kindkomponenten die Strategie `OnPush` einsetzen. Dies kann die Performance der Anwendung entscheidend verbessern. -Bitte beachten Sie, dass der Spread-Operator stets nur eine flache Kopie (Shallow Copy) eines Objekts erstellt. Ist ein Objekt oder Array verschachtelt, so müssen wir bei Änderungen auch immer das direkt betroffene Objekt kopieren. +Wir müssen beachten, dass der Spread-Operator stets nur eine flache Kopie (Shallow Copy) eines Objekts erstellt. Ist ein Objekt oder Array verschachtelt, so müssen wir bei Änderungen auch immer das direkt betroffene Objekt kopieren. ### Nachrichten @@ -340,7 +340,7 @@ Die originale Implementierung von Redux stammt aus der Welt von React. Alle enth NgRx ist das bekannteste Projekt aus dieser Kategorie. Das Framework wurde von Mitgliedern des Angular-Teams aktiv mitentwickelt und gilt als De-facto-Standard für zentrales State Management mit Angular. Es lohnt sich außerdem, einen Blick auf die Community-Projekte NGXS und Elf zu werfen. -Welches der Frameworks Sie für die Zustandsverwaltung einsetzen sollten, hängt von den konkreten Anforderungen und auch von persönlichen Präferenzen ab. Sie sollten alle Projekte vergleichen und Ihren Favoriten nach Kriterien wie Codestruktur und Features auswählen. Dazu möchten wir Ihnen einen Blogartikel empfehlen, in dem NgRx, NGXS, Akita und eine eigene Lösung mit RxJS gegenübergestellt werden: [Angular state management comparison](https://ordina-jworks.github.io/angular/2018/10/08/angular-state-management-comparison.html). +Welches der Frameworks wir für die Zustandsverwaltung einsetzen, hängt von den konkreten Anforderungen und auch von persönlichen Präferenzen ab. Wir sollten alle Projekte vergleichen und unseren Favoriten nach Kriterien wie Codestruktur und Features auswählen. Dazu möchten wir einen Blogartikel empfehlen, in dem NgRx, NGXS, Akita und eine eigene Lösung mit RxJS gegenübergestellt werden: [Angular state management comparison](https://ordina-jworks.github.io/angular/2018/10/08/angular-state-management-comparison.html). --- From a516afa286627e77f8ca4b11e4c655f7ce01b116 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Sat, 13 Jun 2026 21:35:02 +0200 Subject: [PATCH 13/24] material(ngrx): Teil 2 + Teil 3 auf volle CRUD-Tiefe erweitert (Option C) Beide Teile zeigen jetzt ein vollstaendiges, baufaehiges Feature (Laden + Anlegen/Aendern/Loeschen + Fehler-State + UI-Anzeige), gegen ngrx.io verifiziert (adversarialer Review: keine Fehler). Teil 3 (SignalStore), neu strukturiert zu durchgehendem Beispiel: - error-Feld im State; patchState-Updater-Funktion fuer immutables Array-Update (add/update/delete) gezeigt - Schreib-Methoden via rxMethod + concatMap + tapResponse + Fehler-State - EIN konsolidierter Komplett-Store (Laden + CRUD + computed + withHooks) - Komponente mit Aktionen (Loeschen-Button) + Fehleranzeige im Template - Route-providers fuer lazy Feature ergaenzt - withEntities-Variante zeigt addEntity/updateEntity/removeEntity als Automatisierung der manuellen Updates (Phantom-APIs behoben) - Test mit gemocktem BookStoreService Teil 2 (Global Store), neuer Abschnitt 'Buecher anlegen, aendern, loeschen': - error-Feld im State + loadBooksFailure setzt error - Create/Update/Delete-Actions-Trios, Effects mit concatMap (statt switchMap) fuer Schreibvorgaenge, immutable Reducer-Updates - Mehrfach-on() fuer die Fehlerfaelle, selectBooksError + selectSignal - Komponente mit dispatch + Fehleranzeige - macht den Boilerplate-Kontrast zu Teil 3 explizit --- material/ngrx-global-store/README.md | 168 ++++++++++++- material/ngrx-signal-store/README.md | 360 +++++++++++++++++++++++---- 2 files changed, 469 insertions(+), 59 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index ab54299f..b02b52c4 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -135,9 +135,9 @@ Dieser Aufruf von `provideState()` ist essenziell, denn er definiert die Struktu Nun folgt der erste inhaltliche Schritt auf dem Weg zum State Management: Wir müssen die Struktur des Feature-States für das Feature `book` definieren. Dazu befindet sich in der Datei `books/store/book.reducer.ts` ein Interface mit dem Namen `State`. Dieser Feature-State ist der erste Zweig des zentralen Objekt-Baums. -Im Interface `State` legen wir fest, welche Zustände und Daten wir speichern möchten. Es soll zunächst nur darum gehen, die Buchliste vom Server abzurufen, im Store zu speichern und schließlich darzustellen. Wir benötigen also eine Liste von Büchern und integrieren außerdem einen Ladeindikator. +Im Interface `State` legen wir fest, welche Zustände und Daten wir speichern möchten. Es soll zunächst nur darum gehen, die Buchliste vom Server abzurufen, im Store zu speichern und schließlich darzustellen. Wir benötigen also eine Liste von Büchern und integrieren außerdem einen Ladeindikator sowie ein Feld für eventuelle Fehlermeldungen. -Direkt darunter befindet sich die Variable `initialState`. Damit das System weiß, welche Zustände direkt nach dem Start herrschen, müssen wir hier einen initialen Zustand definieren. Für unsere Anwendung ist die Buchliste beim Start leer und der Ladeindikator steht auf `false`: +Direkt darunter befindet sich die Variable `initialState`. Damit das System weiß, welche Zustände direkt nach dem Start herrschen, müssen wir hier einen initialen Zustand definieren. Für unsere Anwendung ist die Buchliste beim Start leer, der Ladeindikator steht auf `false`, und es liegt zunächst kein Fehler vor: ```ts // books/store/book.reducer.ts @@ -146,11 +146,13 @@ export const bookFeatureKey = 'book'; export interface State { books: Book[]; loading: boolean; + error: string | null; } export const initialState: State = { books: [], - loading: false + loading: false, + error: null }; ``` @@ -348,8 +350,8 @@ export const reducer = createReducer( }; }), - on(BookActions.loadBooksFailure, (state): State => { - return { ...state, loading: false }; + on(BookActions.loadBooksFailure, (state, action): State => { + return { ...state, loading: false, error: action.error }; }) ); ``` @@ -588,6 +590,162 @@ provideEffects(bookEffects) Beide Varianten – klassenbasiert mit `inject()` und funktional – sind gleichwertig. Welche wir wählen, ist vor allem eine Frage des Stils. +### Bücher anlegen, ändern und löschen + +Bisher haben wir die Buchliste nur geladen. Eine echte Anwendung muss Daten auch verändern. Im Global Store durchläuft jede Mutation denselben Weg wie das Laden: Eine Komponente dispatcht eine Action, ein Effect erledigt den HTTP-Request, eine Erfolgs-Action landet im Reducer, und der Reducer passt den State an. + +**Actions.** Für Anlegen, Ändern und Löschen definieren wir jeweils ein Trio aus Auslöser-, Erfolgs- und Fehler-Action: + +```ts +// books/store/book.actions.ts +export const createBook = createAction('[Book] Create Book', props<{ book: Book }>()); +export const createBookSuccess = createAction('[Book] Create Book Success', props<{ book: Book }>()); +export const createBookFailure = createAction('[Book] Create Book Failure', props<{ error: string }>()); + +export const updateBook = createAction('[Book] Update Book', props<{ book: Book }>()); +export const updateBookSuccess = createAction('[Book] Update Book Success', props<{ book: Book }>()); +export const updateBookFailure = createAction('[Book] Update Book Failure', props<{ error: string }>()); + +export const deleteBook = createAction('[Book] Delete Book', props<{ isbn: string }>()); +export const deleteBookSuccess = createAction('[Book] Delete Book Success', props<{ isbn: string }>()); +export const deleteBookFailure = createAction('[Book] Delete Book Failure', props<{ error: string }>()); +``` + +Schon an dieser Liste zeigt sich der Preis des Patterns: Drei CRUD-Operationen ergeben neun zusätzliche Actions. (Kompakter notieren lässt sich das mit `createActionGroup()`, siehe Abschnitt "Wie geht's weiter?".) + +**Effects.** Jede Mutation löst einen HTTP-Request aus. Ein wichtiger Unterschied zum Laden betrifft den Flattening-Operator: Beim Laden ist `switchMap()` richtig (eine neue Anfrage macht die alte überflüssig). Bei **schreibenden** Operationen wollen wir laufende Requests aber *nicht* abbrechen – sonst ginge womöglich ein Speichervorgang verloren. Hier ist `concatMap()` die sichere Wahl. Der `BookStoreService` bietet dazu die Methoden `create()`, `update()` und `remove()` an: + +```ts +// books/store/book.effects.ts (Ergänzung in der Klasse BookEffects) +import { concatMap } from 'rxjs/operators'; + +createBook$ = createEffect(() => { + return this.actions$.pipe( + ofType(BookActions.createBook), + concatMap(({ book }) => + this.service.create(book).pipe( + map(created => BookActions.createBookSuccess({ book: created })), + catchError(error => of(BookActions.createBookFailure({ error: error.message }))) + ) + ) + ); +}); + +updateBook$ = createEffect(() => { + return this.actions$.pipe( + ofType(BookActions.updateBook), + concatMap(({ book }) => + this.service.update(book).pipe( + map(updated => BookActions.updateBookSuccess({ book: updated })), + catchError(error => of(BookActions.updateBookFailure({ error: error.message }))) + ) + ) + ); +}); + +deleteBook$ = createEffect(() => { + return this.actions$.pipe( + ofType(BookActions.deleteBook), + concatMap(({ isbn }) => + this.service.remove(isbn).pipe( + map(() => BookActions.deleteBookSuccess({ isbn })), + catchError(error => of(BookActions.deleteBookFailure({ error: error.message }))) + ) + ) + ); +}); +``` + +**Reducer.** Im Reducer passen wir den State immutabel an: anhängen mit dem Spread-Operator, ersetzen mit `map()`, entfernen mit `filter()`. Auf die drei Fehlerfälle reagieren wir mit *einem* `on()` gleichzeitig (das ist möglich, weil ein Reducer für jede Action durchlaufen wird): + +```ts +// books/store/book.reducer.ts (Ergänzung in createReducer) +on(BookActions.createBookSuccess, (state, action): State => ({ + ...state, + books: [...state.books, action.book] +})), + +on(BookActions.updateBookSuccess, (state, action): State => ({ + ...state, + books: state.books.map(b => b.isbn === action.book.isbn ? action.book : b) +})), + +on(BookActions.deleteBookSuccess, (state, action): State => ({ + ...state, + books: state.books.filter(b => b.isbn !== action.isbn) +})), + +on( + BookActions.createBookFailure, + BookActions.updateBookFailure, + BookActions.deleteBookFailure, + (state, action): State => ({ ...state, error: action.error }) +) +``` + +**Selektor und Komponente.** Für die Fehleranzeige ergänzen wir einen Selektor `selectBooksError` und lesen ihn – wie schon `books` und `loading` – per `selectSignal()`. Aktionen lösen wir aus, indem wir die Action Creators dispatchen: + +```ts +// books/store/book.selectors.ts +export const selectBooksError = createSelector( + selectBookState, + state => state.error +); +``` + +```ts +// books/book-list/book-list.component.ts +import { Component, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import * as BookActions from '../store/book.actions'; +import { selectAllBooks, selectBooksError, selectBooksLoading } from '../store/book.selectors'; +import { Book } from '../../shared/book'; + +@Component({ /* ... */ }) +export class BookListComponent { + private store = inject(Store); + + books = this.store.selectSignal(selectAllBooks); + loading = this.store.selectSignal(selectBooksLoading); + error = this.store.selectSignal(selectBooksError); + + constructor() { + this.store.dispatch(BookActions.loadBooks()); + } + + deleteBook(isbn: string): void { + this.store.dispatch(BookActions.deleteBook({ isbn })); + } + + createBook(book: Book): void { + this.store.dispatch(BookActions.createBook({ book })); + } +} +``` + +Im Template zeigen wir die Fehlermeldung an und lösen das Löschen über einen Button aus: + +```html + +@if (error(); as error) { +
    {{ error }}
    +} + +
      + @for (book of books(); track book.isbn) { +
    • + {{ book.title }} + +
    • + } +
    +``` + +Damit haben wir ein vollständiges CRUD-Feature. Für drei Operationen waren neun Actions, drei Effects und drei Reducer-Fälle nötig – genau diese Menge an Bausteinen zieht der SignalStore in [Teil 3](/material/ngrx-signal-store) auf wenige Methoden in einer einzigen Datei zusammen. + +> **Tipp:** Die wiederkehrenden Array-Operationen im Reducer (`[...state.books, …]`, `map()`, `filter()`) übernimmt das Paket `@ngrx/entity` mit fertigen Adaptern (`addOne`, `updateOne`, `removeOne`) – siehe den Abschnitt "Entity Management" weiter unten. + ### Geschafft! Sobald der Effect aktiv ist, wird unsere Anwendung die Bücher über HTTP laden und die Liste mittels `loadBooksSuccess` an den Reducer übergeben. Dieser berechnet einen neuen Zustand mit den geladenen Büchern. Abschließend aktualisiert ein Selektor die Oberfläche, und die neuen Bücher werden angezeigt. diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index 88c780f6..d76f18c3 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -38,7 +38,7 @@ Kurz gesagt: Der Global Store ist *ein* großes, strenges System. Der SignalStor ## Der SignalStore im Detail -Schauen wir uns die Bausteine konkret an. Als durchgehendes Beispiel verwenden wir wieder den BookMonkey und laden – genau wie in Teil 2 – die Buchliste vom Server, samt Ladeindikator. So lässt sich der direkte Vergleich zum Global Store ziehen. +Schauen wir uns die Bausteine konkret an. Als durchgehendes Beispiel verwenden wir wieder den BookMonkey. Wir bauen Schritt für Schritt einen vollständigen `BookStore`, der die Buchliste vom Server lädt **und** Bücher anlegen, ändern und löschen kann – inklusive Lade- und Fehleranzeige. So lässt sich der direkte Vergleich zum Global Store aus Teil 2 ziehen. ### Installation @@ -54,6 +54,8 @@ Anders als beim Global Store gibt es hier **nichts in der `app.config.ts` zu reg Einen SignalStore erzeugen wir mit der Funktion `signalStore()`. Sie nimmt eine Reihe von *Features* entgegen und gibt einen injizierbaren Service zurück. Das grundlegendste Feature ist `withState()`: Es definiert den initialen Zustand. Für jeden State-Slice wird automatisch ein passendes Signal erzeugt. +Unser State umfasst die Buchliste, ein `loading`-Flag für den Ladeindikator und ein `error`-Feld, in dem wir Fehlermeldungen für die Oberfläche ablegen: + ```ts // books/book.store.ts import { signalStore, withState } from '@ngrx/signals'; @@ -62,11 +64,13 @@ import { Book } from '../shared/book'; type BookState = { books: Book[]; loading: boolean; + error: string | null; }; const initialState: BookState = { books: [], - loading: false + loading: false, + error: null }; export const BookStore = signalStore( @@ -74,7 +78,7 @@ export const BookStore = signalStore( ); ``` -Allein dadurch besitzt eine Instanz von `BookStore` bereits zwei Signale: `books: Signal` und `loading: Signal`. Verschachtelte Objekte werden übrigens zu einem `DeepSignal`, das zusätzlich für jede Eigenschaft ein eigenes Signal bereitstellt. +Allein dadurch besitzt eine Instanz von `BookStore` bereits drei Signale: `books: Signal`, `loading: Signal` und `error: Signal`. Verschachtelte Objekte werden übrigens zu einem `DeepSignal`, das zusätzlich für jede Eigenschaft ein eigenes Signal bereitstellt. ### Den Store bereitstellen: lokal oder global @@ -89,7 +93,7 @@ export const BookStore = signalStore( ); ``` -**Lokal** (eine eigene Instanz pro Komponente, gebunden an deren Lebenszyklus) – über das `providers`-Array der Komponente oder Route: +**Lokal** (eine eigene Instanz pro Komponente, gebunden an deren Lebenszyklus) – über das `providers`-Array der Komponente: ```ts @Component({ @@ -101,24 +105,46 @@ export class BookListComponent { } ``` -Diese Wahlfreiheit ist einer der größten Vorteile gegenüber dem Global Store: Zustand, der nur eine Komponente betrifft, muss nicht global abgelegt werden. +Auch eine **Route** kann den Store bereitstellen – ideal für ein lazy geladenes Feature, dessen State mit der Route entsteht und verschwindet: + +```ts +// books/books.routes.ts +import { Routes } from '@angular/router'; +import { BookStore } from './book.store'; + +export const booksRoutes: Routes = [ + { + path: '', + providers: [BookStore], // eigener Store-Lebenszyklus für dieses Feature + children: [ + // ... Routen des Features + ] + } +]; +``` + +Diese Wahlfreiheit ist einer der größten Vorteile gegenüber dem Global Store: Zustand, der nur eine Komponente oder ein Feature betrifft, muss nicht global abgelegt werden. Für unser Beispiel verwenden wir einen global bereitgestellten Store (`{ providedIn: 'root' }`). ### Zustand lesen -Da jeder State-Slice ein Signal ist, lesen wir ihn direkt – im Code wie im Template. Eine `AsyncPipe` brauchen wir nicht mehr; wir rufen das Signal einfach als Funktion auf: +Da jeder State-Slice ein Signal ist, lesen wir ihn direkt – im Code wie im Template. Eine `AsyncPipe` brauchen wir nicht mehr; wir rufen das Signal einfach als Funktion auf. Im Template zeigen wir Ladeindikator, Fehlermeldung und Buchliste an: ```html

    Books

    @if (store.loading()) {
    Loading ...
    -} @else { -
      - @for (book of store.books(); track book.isbn) { -
    • - } -
    } + +@if (store.error(); as error) { +
    {{ error }}
    +} + +
      + @for (book of store.books(); track book.isbn) { +
    • {{ book.title }}
    • + } +
    ``` Der Vergleich zu Teil 2 ist aufschlussreich: Dort haben wir das Signal erst über `store.selectSignal(selectAllBooks)` aus dem globalen Store herausgelöst. Beim SignalStore *ist* der State bereits ein Signal – ein Zwischenschritt über Selektoren entfällt. @@ -144,40 +170,149 @@ Wie bei den memoisierten Selektoren in Teil 2 wird auch ein `computed`-Signal nu ### Zustand ändern: `withMethods` und `patchState` -Verhalten fügen wir mit dem Feature `withMethods()` hinzu. Die Factory erhält die Store-Instanz (und kann über `inject()` Abhängigkeiten anfordern) und gibt ein Objekt mit Methoden zurück. Den Zustand aktualisieren wir innerhalb der Methoden mit der Funktion `patchState()` – immer **immutabel**, ganz wie in einem Reducer: +Verhalten fügen wir mit dem Feature `withMethods()` hinzu. Die Factory erhält die Store-Instanz (und kann über `inject()` Abhängigkeiten anfordern) und gibt ein Objekt mit Methoden zurück. Den Zustand aktualisieren wir innerhalb der Methoden mit der Funktion `patchState()` – immer **immutabel**, ganz wie in einem Reducer. + +`patchState()` nimmt den Store gefolgt von einem **partiellen State-Objekt** oder einer **Updater-Funktion** entgegen. Die Updater-Funktion bekommt den aktuellen State und gibt die Änderung zurück – damit aktualisieren wir auch Arrays und verschachtelte Strukturen sauber unveränderlich: ```ts import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; +import { Book } from '../shared/book'; export const BookStore = signalStore( { providedIn: 'root' }, withState(initialState), withMethods((store) => ({ - reset(): void { - patchState(store, { books: [], loading: false }); + // partielles State-Objekt: + clearError(): void { + patchState(store, { error: null }); + }, + // Updater-Funktion: neuen State aus dem alten ableiten (immutabel!) + addBookLocal(book: Book): void { + patchState(store, state => ({ books: [...state.books, book] })); } })) ); ``` -`patchState()` nimmt den Store gefolgt von einem partiellen State-Objekt oder einer Updater-Funktion entgegen. Hier liegt der zentrale Unterschied zum Global Store: Es gibt keine Action und keinen Reducer als Zwischenschicht – die Methode beschreibt die Zustandsänderung direkt. +Hier liegt der zentrale Unterschied zum Global Store: Es gibt keine Action und keinen Reducer als Zwischenschicht – die Methode beschreibt die Zustandsänderung direkt. Übrigens ist der State eines SignalStore standardmäßig **vor Änderungen von außen geschützt** (Option `protectedState`, Default `true`): `patchState()` greift dann nur innerhalb des Stores. Erst wenn wir den Store mit `{ protectedState: false }` definieren, sind Änderungen auch von außen möglich. Der geschützte Standard hält den Datenfluss kontrollierbar – ähnlich der read-only-Eigenschaft des Global-Store-States. -### Asynchrone Seiteneffekte: `rxMethod` +### Asynchrone Seiteneffekte mit `rxMethod`: Daten laden -Für das Laden der Bücher brauchen wir einen asynchronen Seiteneffekt – die Aufgabe, die im Global Store ein Effect übernommen hat. Einfache asynchrone Abläufe lassen sich direkt mit einer `async`-Methode und einem Promise erledigen. Für reaktive Abläufe mit RxJS bietet das Interop-Plugin die Funktion `rxMethod()` aus `@ngrx/signals/rxjs-interop`. Sie nimmt eine Kette von RxJS-Operatoren entgegen und gibt eine reaktive Methode zurück. +Für das Laden der Bücher brauchen wir einen asynchronen Seiteneffekt – die Aufgabe, die im Global Store ein Effect übernommen hat. Einfache Abläufe lassen sich direkt mit einer `async`-Methode und einem Promise erledigen. Für reaktive Abläufe mit RxJS bietet das Interop-Plugin die Funktion `rxMethod()` aus `@ngrx/signals/rxjs-interop`. Sie nimmt eine Kette von RxJS-Operatoren entgegen und gibt eine reaktive Methode zurück. -Damit bauen wir den vollständigen `BookStore`. Zum sicheren Verarbeiten der HTTP-Antwort nutzen wir den Operator `tapResponse` aus `@ngrx/operators`: +Zum sicheren Verarbeiten der HTTP-Antwort nutzen wir den Operator `tapResponse` aus `@ngrx/operators`. Er ruft je nach Ausgang den `next`-, `error`- oder `finalize`-Callback auf – und ein Fehler im Service-Aufruf beendet dadurch nicht den reaktiven Strom der Methode: + +```ts +import { inject } from '@angular/core'; +import { pipe, switchMap, tap } from 'rxjs'; +import { patchState, withMethods } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { tapResponse } from '@ngrx/operators'; +import { BookStoreService } from '../shared/book-store.service'; + +// innerhalb von signalStore(...): +withMethods((store, service = inject(BookStoreService)) => ({ + loadBooks: rxMethod( + pipe( + tap(() => patchState(store, { loading: true, error: null })), + switchMap(() => + service.getAll().pipe( + tapResponse({ + next: books => patchState(store, { books }), + error: (err: { message: string }) => + patchState(store, { error: err.message }), + finalize: () => patchState(store, { loading: false }) + }) + ) + ) + ) + ) +})) +``` + +Die so erzeugte Methode rufen wir später einfach als `store.loadBooks()` auf. Wir haben hier `switchMap()` gewählt: Wird während eines laufenden Ladevorgangs erneut geladen, soll nur die letzte Anfrage zählen. + +### Schreiben: Bücher anlegen, ändern und löschen + +Jetzt zum Kern jeder echten Anwendung: Daten verändern. Wir definieren je eine Methode zum Anlegen, Ändern und Löschen. Jede löst einen HTTP-Request aus (über den `BookStoreService`, der `create()`, `update()` und `remove()` anbietet) und schreibt das Ergebnis anschließend immutabel in den State. + +Ein wichtiger Unterschied zum Laden betrifft den Flattening-Operator: Beim Laden ist `switchMap()` richtig (eine neue Anfrage macht die alte überflüssig). Bei **schreibenden** Operationen wollen wir laufende Requests aber *nicht* abbrechen – sonst ginge womöglich ein Speichervorgang verloren. Hier ist `concatMap()` die sichere Wahl: Die Anfragen werden der Reihe nach abgearbeitet. + +```ts +import { concatMap, pipe } from 'rxjs'; + +// innerhalb von withMethods((store, service = inject(BookStoreService)) => ({ ... })): + +// Anlegen: an die Liste anhängen +addBook: rxMethod( + pipe( + concatMap(book => + service.create(book).pipe( + tapResponse({ + next: created => + patchState(store, state => ({ books: [...state.books, created] })), + error: (err: { message: string }) => + patchState(store, { error: err.message }) + }) + ) + ) + ) +), + +// Ändern: das passende Buch in der Liste ersetzen +updateBook: rxMethod( + pipe( + concatMap(book => + service.update(book).pipe( + tapResponse({ + next: updated => + patchState(store, state => ({ + books: state.books.map(b => b.isbn === updated.isbn ? updated : b) + })), + error: (err: { message: string }) => + patchState(store, { error: err.message }) + }) + ) + ) + ) +), + +// Löschen: das Buch anhand seiner ISBN aus der Liste filtern +deleteBook: rxMethod( + pipe( + concatMap(isbn => + service.remove(isbn).pipe( + tapResponse({ + next: () => + patchState(store, state => ({ + books: state.books.filter(b => b.isbn !== isbn) + })), + error: (err: { message: string }) => + patchState(store, { error: err.message }) + }) + ) + ) + ) +) +``` + +An diesen drei Methoden sieht man das Muster für jede Mutation: HTTP-Request auslösen, bei Erfolg den State **immutabel** anpassen (anhängen mit Spread, ersetzen mit `map()`, entfernen mit `filter()`), bei Fehler die Meldung in `error` schreiben. Genau diese immutable Logik ist auch das Herz eines Reducers im Global Store – nur dass wir sie hier direkt in der Methode notieren, ohne Action und Reducer dazwischen. + +### Das vollständige Beispiel + +Setzen wir alle Bausteine zusammen, ergibt sich der komplette, kopierbare `BookStore`. Über `withHooks()` lädt er seine Daten beim ersten Verwenden gleich selbst (`onInit`), sodass die Komponente nur noch liest und Aktionen auslöst: ```ts // books/book.store.ts import { computed, inject } from '@angular/core'; -import { pipe, switchMap, tap } from 'rxjs'; +import { concatMap, pipe, switchMap, tap } from 'rxjs'; import { patchState, signalStore, withComputed, + withHooks, withMethods, withState } from '@ngrx/signals'; @@ -190,11 +325,13 @@ import { BookStoreService } from '../shared/book-store.service'; type BookState = { books: Book[]; loading: boolean; + error: string | null; }; const initialState: BookState = { books: [], - loading: false + loading: false, + error: null }; export const BookStore = signalStore( @@ -206,30 +343,85 @@ export const BookStore = signalStore( withMethods((store, service = inject(BookStoreService)) => ({ loadBooks: rxMethod( pipe( - tap(() => patchState(store, { loading: true })), + tap(() => patchState(store, { loading: true, error: null })), switchMap(() => service.getAll().pipe( tapResponse({ next: books => patchState(store, { books }), - error: console.error, + error: (err: { message: string }) => + patchState(store, { error: err.message }), finalize: () => patchState(store, { loading: false }) }) ) ) ) + ), + addBook: rxMethod( + pipe( + concatMap(book => + service.create(book).pipe( + tapResponse({ + next: created => + patchState(store, state => ({ books: [...state.books, created] })), + error: (err: { message: string }) => + patchState(store, { error: err.message }) + }) + ) + ) + ) + ), + updateBook: rxMethod( + pipe( + concatMap(book => + service.update(book).pipe( + tapResponse({ + next: updated => + patchState(store, state => ({ + books: state.books.map(b => b.isbn === updated.isbn ? updated : b) + })), + error: (err: { message: string }) => + patchState(store, { error: err.message }) + }) + ) + ) + ) + ), + deleteBook: rxMethod( + pipe( + concatMap(isbn => + service.remove(isbn).pipe( + tapResponse({ + next: () => + patchState(store, state => ({ + books: state.books.filter(b => b.isbn !== isbn) + })), + error: (err: { message: string }) => + patchState(store, { error: err.message }) + }) + ) + ) + ) ) - })) + })), + withHooks({ + onInit(store) { + store.loadBooks(); + } + }) ); ``` -Hier steckt der gesamte Lade-Ablauf in *einer* Datei und *einer* Methode: Ladeindikator setzen, HTTP-Request mit `switchMap()` auslösen, Ergebnis in den State schreiben, Ladeindikator zurücksetzen. In Teil 2 waren für genau diese Logik ein Effect, drei Actions und ein Reducer nötig. +Das ist der gesamte State-Management-Code für ein vollständiges CRUD-Feature – eine Datei, ohne Actions, Reducer, Selektoren-Dateien oder Effects-Klassen. -Die Komponente injiziert den Store und stößt das Laden an – mehr braucht sie nicht: +### Die Komponente: lesen und Aktionen auslösen + +Die Komponente injiziert den Store, liest die Signale im Template und ruft für Nutzeraktionen die Store-Methoden auf. Da der Store sich über `onInit` selbst lädt, braucht die Komponente keinen Lade-Code mehr: ```ts // books/book-list/book-list.component.ts import { Component, inject } from '@angular/core'; import { BookStore } from '../book.store'; +import { Book } from '../shared/book'; @Component({ selector: 'bm-book-list', @@ -238,48 +430,64 @@ import { BookStore } from '../book.store'; export class BookListComponent { protected store = inject(BookStore); - constructor() { - this.store.loadBooks(); + deleteBook(isbn: string): void { + this.store.deleteBook(isbn); + } + + createBook(book: Book): void { + this.store.addBook(book); } } ``` -### Lebenszyklus: `withHooks` +Im Template lesen wir die Signale und lösen über Events die Methoden aus – ein Löschen-Button pro Buch, eine Fehlermeldung aus dem `error`-Signal: -Oft soll ein Store sich beim Start selbst initialisieren, statt dass eine Komponente das Laden anstößt. Dafür gibt es das Feature `withHooks()` mit den Methoden `onInit` und `onDestroy`: +```html + +

    Books ({{ store.booksCount() }})

    -```ts -import { withHooks } from '@ngrx/signals'; +@if (store.loading()) { +
    Loading ...
    +} -export const BookStore = signalStore( - { providedIn: 'root' }, - withState(initialState), - withMethods(/* ... */), - withHooks({ - onInit(store) { - store.loadBooks(); - } - }) -); +@if (store.error(); as error) { +
    + {{ error }} + +
    +} + +
      + @for (book of store.books(); track book.isbn) { +
    • + {{ book.title }} + +
    • + } +
    ``` -Damit lädt der Store seine Daten von selbst, und die Komponente liest nur noch die Signale aus. +Ein Formular zum Anlegen würde im `(ngSubmit)` einfach `createBook(neuesBuch)` aufrufen. Weil der Store global (`providedIn: 'root'`) bereitsteht, teilen sich alle Komponenten dieselbe Instanz: Eine Detail- oder Formularkomponente, die `store.addBook()` aufruft, verändert denselben State, den die Liste anzeigt – die Oberfläche aktualisiert sich dank der Signale automatisch. -### Entitäten verwalten: `withEntities` +### Weniger Boilerplate: Entitäten mit `withEntities` -Für Sammlungen von Objekten bietet das Plugin `@ngrx/signals/entities` das Feature `withEntities()` – das Gegenstück zu `@ngrx/entity` aus Teil 2. Es legt die Signale `entityMap`, `ids` und `entities` an und bringt eine Reihe von Updatern mit (`addEntity`, `setAllEntities`, `updateEntity`, `removeEntity`, …), die wir mit `patchState()` kombinieren. +Die immutablen Array-Operationen aus unseren CRUD-Methoden (`[...state.books, created]`, `map(...)`, `filter(...)`) wiederholen sich in jeder Anwendung. Genau diese Routine nimmt uns das Plugin `@ngrx/signals/entities` mit dem Feature `withEntities()` ab – das Gegenstück zu `@ngrx/entity` aus Teil 2. Es legt die Signale `entityMap`, `ids` und `entities` an und bringt fertige Updater mit: `addEntity`, `updateEntity`, `removeEntity`, `setAllEntities` und weitere. -Standardmäßig erwartet `withEntities` ein Property `id`. Da ein Buch im BookMonkey über seine `isbn` identifiziert wird, geben wir – wie schon bei `@ngrx/entity` – einen eigenen ID-Selektor an: +Standardmäßig erwartet `withEntities` ein Property `id`. Da ein Buch im BookMonkey über seine `isbn` identifiziert wird, geben wir – wie schon bei `@ngrx/entity` – einen eigenen ID-Selektor an. Bei `add*`-, `set*`- und `update*`-Updatern übergeben wir ihn als zweites Argument; die `remove*`-Updater ermitteln die ID automatisch: ```ts +// books/book.store.ts (Entity-Variante) import { inject } from '@angular/core'; -import { switchMap } from 'rxjs'; +import { concatMap, pipe, switchMap } from 'rxjs'; import { patchState, signalStore, withMethods } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { SelectEntityId, + addEntity, + removeEntity, setAllEntities, + updateEntity, withEntities } from '@ngrx/signals/entities'; @@ -301,12 +509,43 @@ export const BookStore = signalStore( }) ) ) + ), + addBook: rxMethod( + concatMap(book => + service.create(book).pipe( + tapResponse({ + next: created => patchState(store, addEntity(created, { selectId })), + error: console.error + }) + ) + ) + ), + updateBook: rxMethod( + concatMap(book => + service.update(book).pipe( + tapResponse({ + next: updated => + patchState(store, updateEntity({ id: updated.isbn, changes: updated }, { selectId })), + error: console.error + }) + ) + ) + ), + deleteBook: rxMethod( + concatMap(isbn => + service.remove(isbn).pipe( + tapResponse({ + next: () => patchState(store, removeEntity(isbn)), + error: console.error + }) + ) + ) ) })) ); ``` -In dieser Variante ersetzt `withEntities` das manuelle `books`-Array aus `withState`. Über das Signal `store.entities()` erhalten wir die Buchliste als Array – die mühsame Pflege von `entityMap` und `ids` übernimmt das Plugin. +Die Entity-Updater automatisieren also exakt die immutablen Operationen, die wir zuvor von Hand geschrieben haben. Die Buchliste lesen wir nun über das mitgelieferte Signal `store.entities()`; die Pflege von `entityMap` und `ids` übernimmt das Plugin. Wollen wir zusätzlich eigene Felder wie `loading` oder `error` führen, kombinieren wir `withEntities()` einfach mit einem `withState(...)` für diese Felder. ### Wiederverwendbare Bausteine: `signalStoreFeature` @@ -332,23 +571,36 @@ Dieses Feature lässt sich anschließend in beliebig vielen Stores einsetzen: `s ### Testing -Da ein SignalStore ein gewöhnlicher Service ist, testen wir ihn auch wie einen Service: Instanz beziehen, Methoden aufrufen, Signale auslesen. Mocks und ein vollständiges Store-Setup wie beim Global Store sind nicht nötig: +Da ein SignalStore ein gewöhnlicher Service ist, testen wir ihn auch wie einen Service: Instanz beziehen, Methoden aufrufen, Signale auslesen. Ein vollständiges Store-Setup wie beim Global Store ist nicht nötig. Den HTTP-Service ersetzen wir wie gewohnt per `{ provide: …, useValue: … }`: ```ts +// books/book.store.spec.ts import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { BookStore } from './book.store'; +import { BookStoreService } from '../shared/book-store.service'; +import { book } from './test-helper'; describe('BookStore', () => { - it('zählt die geladenen Bücher', () => { + it('lädt Bücher und zählt sie', () => { + TestBed.configureTestingModule({ + providers: [ + { + provide: BookStoreService, + useValue: { getAll: () => of([book(1), book(2), book(3)]) } + } + ] + }); + const store = TestBed.inject(BookStore); + store.loadBooks(); - expect(store.booksCount()).toBe(0); - // ... Bücher laden ... expect(store.booksCount()).toBe(3); }); }); ``` -> **Hinweis:** Injiziert eine Methode einen Service, ersetzen wir ihn im Test wie gewohnt per `{ provide: …, useValue: … }`. Den geschützten State können wir in Tests mit dem Helfer `unprotected()` aus `@ngrx/signals/testing` gezielt setzen. +> **Hinweis:** Den geschützten State können wir in Tests mit dem Helfer `unprotected()` aus `@ngrx/signals/testing` gezielt setzen, falls einmal kein passender öffentlicher Setter zur Verfügung steht. ### Was ist mit den Redux DevTools? @@ -364,7 +616,7 @@ Beide Bausteine stammen aus dem NgRx-Projekt und lösen dieselbe Aufgabe, setzen | --- | --- | --- | | Grundidee | zentrales Redux-Pattern | deklarativ zusammengesetzter Service | | Anzahl Stores | ein globaler State-Baum | viele kleine Stores | -| Geltungsbereich | immer global | lokal (komponentengebunden) **oder** global | +| Geltungsbereich | immer global | lokal (komponenten-/routengebunden) **oder** global | | Lesen | Selektoren + `selectSignal()` | State *ist* direkt ein Signal: `store.books()` | | Schreiben | Action `dispatch()` → Reducer | Methode aufrufen → `patchState()` | | Indirektion | hoch (Action/Reducer/Effect getrennt) | gering (Methode ändert State direkt) | From 07d77cbb605bd670000aaa034c74286d088f19eb Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Sat, 13 Jun 2026 22:13:35 +0200 Subject: [PATCH 14/24] material(ngrx): lauffaehige Beispiel-Apps + Vitest-Beweis fuer Teil 2 & 3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zwei echte Angular-21-Apps (Vitest, zoneless) in den jeweiligen Artikel-Verzeichnissen unter demo/ — beweisen, dass der Artikel-Code laeuft. NgRx 21 (latest; NgRx 22 ist noch nicht auf npm). material/ngrx-global-store/demo (@ngrx/store + @ngrx/effects): - Book-Modell, In-Memory BookStoreService, Actions/Reducer/Selektoren/ Effects mit Laden + CRUD + Fehler-State (Code wie im Artikel) - 16 Vitest-Tests gruen: Reducer (alle CRUD-Transitions), Selektoren, Effects (provideMockActions, switchMap/concatMap), App (provideMockStore) - ng build erfolgreich material/ngrx-signal-store/demo (@ngrx/signals + @ngrx/operators): - signalStore mit withState/withComputed/withMethods/rxMethod/withHooks, Laden + CRUD + Fehler-State (Code wie im Artikel) - 7 Vitest-Tests gruen: Store (Laden, add/update/delete, Fehler, clearError) + App-Komponente - ng build erfolgreich Beim Bauen entdeckt & gefixt: Teil 3 nutzte store.clearError() im Template, der konsolidierte Store definierte es nicht -> ergaenzt (in App und Artikel). --- material/ngrx-global-store/demo/.editorconfig | 17 + material/ngrx-global-store/demo/.gitignore | 44 + material/ngrx-global-store/demo/.prettierrc | 12 + .../demo/.vscode/extensions.json | 4 + .../demo/.vscode/launch.json | 20 + .../ngrx-global-store/demo/.vscode/mcp.json | 9 + .../ngrx-global-store/demo/.vscode/tasks.json | 42 + material/ngrx-global-store/demo/README.md | 19 + material/ngrx-global-store/demo/angular.json | 73 + .../ngrx-global-store/demo/package-lock.json | 8647 +++++++++++++++++ material/ngrx-global-store/demo/package.json | 35 + .../ngrx-global-store/demo/public/favicon.ico | Bin 0 -> 15086 bytes .../demo/src/app/app.config.ts | 15 + .../ngrx-global-store/demo/src/app/app.css | 0 .../ngrx-global-store/demo/src/app/app.html | 1 + .../demo/src/app/app.spec.ts | 28 + .../ngrx-global-store/demo/src/app/app.ts | 10 + .../demo/src/app/book-list/book-list.html | 18 + .../demo/src/app/book-list/book-list.ts | 30 + .../demo/src/app/shared/book-store.service.ts | 36 + .../demo/src/app/shared/book.ts | 5 + .../demo/src/app/store/book.actions.ts | 18 + .../demo/src/app/store/book.effects.spec.ts | 64 + .../demo/src/app/store/book.effects.ts | 63 + .../demo/src/app/store/book.reducer.spec.ts | 56 + .../demo/src/app/store/book.reducer.ts | 53 + .../demo/src/app/store/book.selectors.spec.ts | 22 + .../demo/src/app/store/book.selectors.ts | 21 + .../ngrx-global-store/demo/src/index.html | 13 + material/ngrx-global-store/demo/src/main.ts | 6 + .../ngrx-global-store/demo/src/styles.css | 1 + .../ngrx-global-store/demo/tsconfig.app.json | 15 + material/ngrx-global-store/demo/tsconfig.json | 33 + .../ngrx-global-store/demo/tsconfig.spec.json | 15 + material/ngrx-signal-store/README.md | 3 + material/ngrx-signal-store/demo/.editorconfig | 17 + material/ngrx-signal-store/demo/.gitignore | 44 + material/ngrx-signal-store/demo/.prettierrc | 12 + .../demo/.vscode/extensions.json | 4 + .../demo/.vscode/launch.json | 20 + .../ngrx-signal-store/demo/.vscode/mcp.json | 9 + .../ngrx-signal-store/demo/.vscode/tasks.json | 42 + material/ngrx-signal-store/demo/README.md | 19 + material/ngrx-signal-store/demo/angular.json | 73 + .../ngrx-signal-store/demo/package-lock.json | 8637 ++++++++++++++++ material/ngrx-signal-store/demo/package.json | 34 + .../ngrx-signal-store/demo/public/favicon.ico | Bin 0 -> 15086 bytes .../demo/src/app/app.config.ts | 8 + .../ngrx-signal-store/demo/src/app/app.css | 0 .../ngrx-signal-store/demo/src/app/app.html | 1 + .../demo/src/app/app.spec.ts | 21 + .../ngrx-signal-store/demo/src/app/app.ts | 10 + .../demo/src/app/book-list/book-list.html | 21 + .../demo/src/app/book-list/book-list.ts | 19 + .../demo/src/app/book.store.spec.ts | 84 + .../demo/src/app/book.store.ts | 106 + .../demo/src/app/shared/book-store.service.ts | 36 + .../demo/src/app/shared/book.ts | 5 + .../ngrx-signal-store/demo/src/index.html | 13 + material/ngrx-signal-store/demo/src/main.ts | 6 + .../ngrx-signal-store/demo/src/styles.css | 1 + .../ngrx-signal-store/demo/tsconfig.app.json | 15 + material/ngrx-signal-store/demo/tsconfig.json | 33 + .../ngrx-signal-store/demo/tsconfig.spec.json | 15 + 64 files changed, 18753 insertions(+) create mode 100644 material/ngrx-global-store/demo/.editorconfig create mode 100644 material/ngrx-global-store/demo/.gitignore create mode 100644 material/ngrx-global-store/demo/.prettierrc create mode 100644 material/ngrx-global-store/demo/.vscode/extensions.json create mode 100644 material/ngrx-global-store/demo/.vscode/launch.json create mode 100644 material/ngrx-global-store/demo/.vscode/mcp.json create mode 100644 material/ngrx-global-store/demo/.vscode/tasks.json create mode 100644 material/ngrx-global-store/demo/README.md create mode 100644 material/ngrx-global-store/demo/angular.json create mode 100644 material/ngrx-global-store/demo/package-lock.json create mode 100644 material/ngrx-global-store/demo/package.json create mode 100644 material/ngrx-global-store/demo/public/favicon.ico create mode 100644 material/ngrx-global-store/demo/src/app/app.config.ts create mode 100644 material/ngrx-global-store/demo/src/app/app.css create mode 100644 material/ngrx-global-store/demo/src/app/app.html create mode 100644 material/ngrx-global-store/demo/src/app/app.spec.ts create mode 100644 material/ngrx-global-store/demo/src/app/app.ts create mode 100644 material/ngrx-global-store/demo/src/app/book-list/book-list.html create mode 100644 material/ngrx-global-store/demo/src/app/book-list/book-list.ts create mode 100644 material/ngrx-global-store/demo/src/app/shared/book-store.service.ts create mode 100644 material/ngrx-global-store/demo/src/app/shared/book.ts create mode 100644 material/ngrx-global-store/demo/src/app/store/book.actions.ts create mode 100644 material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts create mode 100644 material/ngrx-global-store/demo/src/app/store/book.effects.ts create mode 100644 material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts create mode 100644 material/ngrx-global-store/demo/src/app/store/book.reducer.ts create mode 100644 material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts create mode 100644 material/ngrx-global-store/demo/src/app/store/book.selectors.ts create mode 100644 material/ngrx-global-store/demo/src/index.html create mode 100644 material/ngrx-global-store/demo/src/main.ts create mode 100644 material/ngrx-global-store/demo/src/styles.css create mode 100644 material/ngrx-global-store/demo/tsconfig.app.json create mode 100644 material/ngrx-global-store/demo/tsconfig.json create mode 100644 material/ngrx-global-store/demo/tsconfig.spec.json create mode 100644 material/ngrx-signal-store/demo/.editorconfig create mode 100644 material/ngrx-signal-store/demo/.gitignore create mode 100644 material/ngrx-signal-store/demo/.prettierrc create mode 100644 material/ngrx-signal-store/demo/.vscode/extensions.json create mode 100644 material/ngrx-signal-store/demo/.vscode/launch.json create mode 100644 material/ngrx-signal-store/demo/.vscode/mcp.json create mode 100644 material/ngrx-signal-store/demo/.vscode/tasks.json create mode 100644 material/ngrx-signal-store/demo/README.md create mode 100644 material/ngrx-signal-store/demo/angular.json create mode 100644 material/ngrx-signal-store/demo/package-lock.json create mode 100644 material/ngrx-signal-store/demo/package.json create mode 100644 material/ngrx-signal-store/demo/public/favicon.ico create mode 100644 material/ngrx-signal-store/demo/src/app/app.config.ts create mode 100644 material/ngrx-signal-store/demo/src/app/app.css create mode 100644 material/ngrx-signal-store/demo/src/app/app.html create mode 100644 material/ngrx-signal-store/demo/src/app/app.spec.ts create mode 100644 material/ngrx-signal-store/demo/src/app/app.ts create mode 100644 material/ngrx-signal-store/demo/src/app/book-list/book-list.html create mode 100644 material/ngrx-signal-store/demo/src/app/book-list/book-list.ts create mode 100644 material/ngrx-signal-store/demo/src/app/book.store.spec.ts create mode 100644 material/ngrx-signal-store/demo/src/app/book.store.ts create mode 100644 material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts create mode 100644 material/ngrx-signal-store/demo/src/app/shared/book.ts create mode 100644 material/ngrx-signal-store/demo/src/index.html create mode 100644 material/ngrx-signal-store/demo/src/main.ts create mode 100644 material/ngrx-signal-store/demo/src/styles.css create mode 100644 material/ngrx-signal-store/demo/tsconfig.app.json create mode 100644 material/ngrx-signal-store/demo/tsconfig.json create mode 100644 material/ngrx-signal-store/demo/tsconfig.spec.json diff --git a/material/ngrx-global-store/demo/.editorconfig b/material/ngrx-global-store/demo/.editorconfig new file mode 100644 index 00000000..f166060d --- /dev/null +++ b/material/ngrx-global-store/demo/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/material/ngrx-global-store/demo/.gitignore b/material/ngrx-global-store/demo/.gitignore new file mode 100644 index 00000000..854acd5f --- /dev/null +++ b/material/ngrx-global-store/demo/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/material/ngrx-global-store/demo/.prettierrc b/material/ngrx-global-store/demo/.prettierrc new file mode 100644 index 00000000..d6c16d7e --- /dev/null +++ b/material/ngrx-global-store/demo/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] +} diff --git a/material/ngrx-global-store/demo/.vscode/extensions.json b/material/ngrx-global-store/demo/.vscode/extensions.json new file mode 100644 index 00000000..77b37457 --- /dev/null +++ b/material/ngrx-global-store/demo/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/material/ngrx-global-store/demo/.vscode/launch.json b/material/ngrx-global-store/demo/.vscode/launch.json new file mode 100644 index 00000000..925af837 --- /dev/null +++ b/material/ngrx-global-store/demo/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/material/ngrx-global-store/demo/.vscode/mcp.json b/material/ngrx-global-store/demo/.vscode/mcp.json new file mode 100644 index 00000000..956af8c6 --- /dev/null +++ b/material/ngrx-global-store/demo/.vscode/mcp.json @@ -0,0 +1,9 @@ +{ + // For more information, visit: https://angular.dev/ai/mcp + "servers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} diff --git a/material/ngrx-global-store/demo/.vscode/tasks.json b/material/ngrx-global-store/demo/.vscode/tasks.json new file mode 100644 index 00000000..244306f9 --- /dev/null +++ b/material/ngrx-global-store/demo/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/material/ngrx-global-store/demo/README.md b/material/ngrx-global-store/demo/README.md new file mode 100644 index 00000000..cf837f8c --- /dev/null +++ b/material/ngrx-global-store/demo/README.md @@ -0,0 +1,19 @@ +# Beispiel-App: Global Store mit NgRx + +Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil 2: Global Store](../README.md)**. + +Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern und löschen (CRUD) mit Actions, Reducer, Selektoren und Effects, inklusive Fehler-State. + +- **Angular 21** + **NgRx 21** (`@ngrx/store`, `@ngrx/effects`) +- Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. + +## Befehle + +```bash +npm install +npm test # Vitest: Reducer-, Selektor- und Effect-Tests +npm start # ng serve +npm run build +``` + +Die Tests beweisen das State Management isoliert (Reducer als Pure Functions, Effects mit `provideMockActions`, Selektoren, Komponente mit `provideMockStore`). diff --git a/material/ngrx-global-store/demo/angular.json b/material/ngrx-global-store/demo/angular.json new file mode 100644 index 00000000..3043937e --- /dev/null +++ b/material/ngrx-global-store/demo/angular.json @@ -0,0 +1,73 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm" + }, + "newProjectRoot": "projects", + "projects": { + "demo": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "demo:build:production" + }, + "development": { + "buildTarget": "demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/material/ngrx-global-store/demo/package-lock.json b/material/ngrx-global-store/demo/package-lock.json new file mode 100644 index 00000000..31b657f6 --- /dev/null +++ b/material/ngrx-global-store/demo/package-lock.json @@ -0,0 +1,8647 @@ +{ + "name": "demo", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "demo", + "version": "0.0.0", + "dependencies": { + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", + "@ngrx/effects": "^21.1.1", + "@ngrx/operators": "^21.1.1", + "@ngrx/store": "^21.1.1", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.2.7", + "@angular/cli": "^21.2.7", + "@angular/compiler-cli": "^21.2.0", + "jsdom": "^28.0.0", + "prettier": "^3.8.1", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/abtesting": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", + "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", + "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", + "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", + "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", + "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", + "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", + "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", + "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", + "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", + "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", + "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", + "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", + "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", + "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2102.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.15.tgz", + "integrity": "sha512-EZQXu6j7J7OUxmpxIO2mxd58NTlCb7HOAOfLLGdk7lRcwZeCxnjGRzb72tXlJgIEi3IoOYwcW0ft2cg7r/d6qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.15", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.15.tgz", + "integrity": "sha512-x7EwuQtMGHANVznHpwyEi/3lMo/kRoaxV2w7lXbRGjTezwv4SPaOBwhrIGCbAVFs5B1ziff4jZzors4owlCTgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.15.tgz", + "integrity": "sha512-kHHV694KUPuXlItjvnxUspn+KYjlTKu8KINX/kT1qlogzLYojpnRwcUtZyRPUz1f/M1d3TUHVFeBKx+h5HoNcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.15", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.3.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/build": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.15.tgz", + "integrity": "sha512-APJ5v0/hL38CKSqSQDos5Y8/O2LSiSSqP9FagdcMT5j9JAlENXdhVaA3IxPMXDWcxtzD5T1IlN8Z9aFxDXxHyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2102.15", + "@babel/core": "7.29.0", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.4", + "beasties": "0.4.1", + "browserslist": "^4.26.0", + "esbuild": "0.27.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.0", + "picomatch": "4.0.4", + "piscina": "5.1.4", + "rolldown": "1.0.0-rc.4", + "sass": "1.97.3", + "semver": "7.7.4", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.15", + "undici": "7.24.4", + "vite": "7.3.2", + "watchpack": "2.5.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.1" + }, + "peerDependencies": { + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.2.15", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^21.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.15.tgz", + "integrity": "sha512-8CT1iST5CwdxHEzC0NVFUsMenAY7aE30ayTAw5PpA6MPs6bdHlL6QxTmkqThW7hoCj6PJ56YfQMzSQU+aYRLQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2102.15", + "@angular-devkit/core": "21.2.15", + "@angular-devkit/schematics": "21.2.15", + "@inquirer/prompts": "7.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.2.15", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.48.1", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.3.1", + "parse5-html-rewriting-stream": "8.0.0", + "semver": "7.7.4", + "yargs": "18.0.0", + "zod": "4.3.6" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.17.tgz", + "integrity": "sha512-hqAQxRfi5ldFE42suAXRcY+JCANrUh7fuSQ/DtZ7L896id5BT/exuv6dWNBC1PyAfQmRbpD5Pt6/pd+tNLyhDQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.17.tgz", + "integrity": "sha512-p+NdjYiwAz9Zmu2yul0LlMXaFjMISVVa24+/MVMoKFeQeI82QE8jDywPlnOSHQHvdCcQVpS7saeEriZzX3JuBQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.17.tgz", + "integrity": "sha512-KithZ3b0HBFH0NbUcswBcjpN9y09vLbarMD7qmGWTnGUBk4W8cn4sbT8zJyv9CRKg9ZcuUBeJYKUfUPn/u/5OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.29.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.17", + "typescript": ">=5.9 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/core": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.17.tgz", + "integrity": "sha512-wYHpwIdnUnjQFOJJNqRcGx7LS3u64jT+R9L0TnMR/ViBM9dQgGYImlSikkftg2yrFCNo5aKRxhG2LLskQurVdg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.17.tgz", + "integrity": "sha512-WKu8XeRSNZo+a+aDDZ3M5OtReF7KYqR/PmZ2l1lSf6N5EEAmc+Ky4aqbRhTL/mTSfHrO4+TDJ4C5A2tFmuwIeA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.17", + "@angular/core": "21.2.17", + "@angular/platform-browser": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.17.tgz", + "integrity": "sha512-ROdSliejY37g1EphYmweYdm5cHM8HY3X4tbWt4ubxmhTyYgfN3nxrxfGQ/n7Mz5tDY9VXVLIGDgjLOGYOo4uTQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "21.2.17", + "@angular/common": "21.2.17", + "@angular/core": "21.2.17" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/router": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.17.tgz", + "integrity": "sha512-RSCtK5ppAV6y6wfRLHSK2a9Wc/vm8j0wsC+/j9PH9yQmppWFVXDWsg5E39MKOIpnoYVx2+hI6eak6+wYtZTe1A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.17", + "@angular/core": "21.2.17", + "@angular/platform-browser": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.4.tgz", + "integrity": "sha512-yI8kNhHiOrLb8Rlulsk07DeQz0PwyT69FX9dkz5rAp7p9RUwFKEXnZpBGzURiLHgi66YqIWxOHn1nij8Lrg27Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.5" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", + "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", + "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", + "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", + "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", + "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", + "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", + "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@ngrx/effects": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-21.1.1.tgz", + "integrity": "sha512-JQUv2tYyjkbdsE40WYtJDsiI+3qtn4V5m0Ybr2CYw9zKpVQshDV5MLLAni+SuijlHAWJkw8x4wwnYmNyG/Rmng==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^21.0.0", + "@ngrx/store": "21.1.1", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/operators": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-21.1.1.tgz", + "integrity": "sha512-2ChT1rVi6w8xHrmIAqRFuvY7YLAJiH7idfmbimjOYHzJ7pgaz0+4WVqyCp9pv2UiDEXuvpUOvs3afgS5m1LDNQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngrx/store": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-21.1.1.tgz", + "integrity": "sha512-B0/IyBTqBExmwPAmrpcJrhlrn+FeeitvOW8ltf7O3kJb2vRKduV5Oi34GLiwYQ8eAukTbKyWQv/VV02gxLeEMg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^21.0.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", + "integrity": "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", + "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", + "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", + "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.15.tgz", + "integrity": "sha512-xdTirIUUegMXtyEDsYabCRcAnlVatVYb6S8v77fgK2eqUhGalI2e/3+L51N3XF1+6K2vEhyDpmmhFZLsdhLYbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.15", + "@angular-devkit/schematics": "21.2.15", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", + "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.14.1", + "@algolia/client-abtesting": "5.48.1", + "@algolia/client-analytics": "5.48.1", + "@algolia/client-common": "5.48.1", + "@algolia/client-insights": "5.48.1", + "@algolia/client-personalization": "5.48.1", + "@algolia/client-query-suggestions": "5.48.1", + "@algolia/client-search": "5.48.1", + "@algolia/ingestion": "1.48.1", + "@algolia/monitoring": "1.48.1", + "@algolia/recommend": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", + "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/immutable": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", + "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", + "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.1", + "@lmdb/lmdb-darwin-x64": "3.5.1", + "@lmdb/lmdb-linux-arm": "3.5.1", + "@lmdb/lmdb-linux-arm64": "3.5.1", + "@lmdb/lmdb-linux-x64": "3.5.1", + "@lmdb/lmdb-win32-arm64": "3.5.1", + "@lmdb/lmdb-win32-x64": "3.5.1" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", + "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", + "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", + "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", + "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.113.0", + "@rolldown/pluginutils": "1.0.0-rc.4" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-x64": "1.0.0-rc.4", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/material/ngrx-global-store/demo/package.json b/material/ngrx-global-store/demo/package.json new file mode 100644 index 00000000..10b8ba66 --- /dev/null +++ b/material/ngrx-global-store/demo/package.json @@ -0,0 +1,35 @@ +{ + "name": "demo", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "packageManager": "npm@10.9.4", + "dependencies": { + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", + "@ngrx/effects": "^21.1.1", + "@ngrx/operators": "^21.1.1", + "@ngrx/store": "^21.1.1", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.2.7", + "@angular/cli": "^21.2.7", + "@angular/compiler-cli": "^21.2.0", + "jsdom": "^28.0.0", + "prettier": "^3.8.1", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } +} diff --git a/material/ngrx-global-store/demo/public/favicon.ico b/material/ngrx-global-store/demo/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 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 Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/material/ngrx-global-store/demo/src/app/app.config.ts b/material/ngrx-global-store/demo/src/app/app.config.ts new file mode 100644 index 00000000..74874b49 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/app.config.ts @@ -0,0 +1,15 @@ +import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideState, provideStore } from '@ngrx/store'; +import { provideEffects } from '@ngrx/effects'; + +import * as fromBook from './store/book.reducer'; +import { BookEffects } from './store/book.effects'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideStore(), + provideState(fromBook.bookFeatureKey, fromBook.reducer), + provideEffects(BookEffects) + ] +}; diff --git a/material/ngrx-global-store/demo/src/app/app.css b/material/ngrx-global-store/demo/src/app/app.css new file mode 100644 index 00000000..e69de29b diff --git a/material/ngrx-global-store/demo/src/app/app.html b/material/ngrx-global-store/demo/src/app/app.html new file mode 100644 index 00000000..475128a5 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/app.html @@ -0,0 +1 @@ + diff --git a/material/ngrx-global-store/demo/src/app/app.spec.ts b/material/ngrx-global-store/demo/src/app/app.spec.ts new file mode 100644 index 00000000..31c6559f --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/app.spec.ts @@ -0,0 +1,28 @@ +import { TestBed } from '@angular/core/testing'; +import { provideMockStore } from '@ngrx/store/testing'; + +import { App } from './app'; +import { selectAllBooks, selectBooksError, selectBooksLoading } from './store/book.selectors'; + +describe('App', () => { + it('erzeugt die App und zeigt die Buchliste an', async () => { + await TestBed.configureTestingModule({ + imports: [App], + providers: [ + provideMockStore({ + selectors: [ + { selector: selectAllBooks, value: [] }, + { selector: selectBooksLoading, value: false }, + { selector: selectBooksError, value: null } + ] + }) + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + + const h1 = fixture.nativeElement.querySelector('h1') as HTMLElement; + expect(h1?.textContent).toContain('Books'); + }); +}); diff --git a/material/ngrx-global-store/demo/src/app/app.ts b/material/ngrx-global-store/demo/src/app/app.ts new file mode 100644 index 00000000..029de745 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/app.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { BookList } from './book-list/book-list'; + +@Component({ + selector: 'app-root', + imports: [BookList], + templateUrl: './app.html', + styleUrl: './app.css' +}) +export class App {} diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.html b/material/ngrx-global-store/demo/src/app/book-list/book-list.html new file mode 100644 index 00000000..624d28b6 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/book-list/book-list.html @@ -0,0 +1,18 @@ +

    Books ({{ books().length }})

    + +@if (loading()) { +

    Loading ...

    +} + +@if (error(); as error) { +

    {{ error }}

    +} + +
      + @for (book of books(); track book.isbn) { +
    • + {{ book.title }} + +
    • + } +
    diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.ts b/material/ngrx-global-store/demo/src/app/book-list/book-list.ts new file mode 100644 index 00000000..e06a29e3 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/book-list/book-list.ts @@ -0,0 +1,30 @@ +import { Component, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import * as BookActions from '../store/book.actions'; +import { selectAllBooks, selectBooksError, selectBooksLoading } from '../store/book.selectors'; +import { Book } from '../shared/book'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.html' +}) +export class BookList { + private store = inject(Store); + + books = this.store.selectSignal(selectAllBooks); + loading = this.store.selectSignal(selectBooksLoading); + error = this.store.selectSignal(selectBooksError); + + constructor() { + this.store.dispatch(BookActions.loadBooks()); + } + + deleteBook(isbn: string): void { + this.store.dispatch(BookActions.deleteBook({ isbn })); + } + + addBook(book: Book): void { + this.store.dispatch(BookActions.createBook({ book })); + } +} diff --git a/material/ngrx-global-store/demo/src/app/shared/book-store.service.ts b/material/ngrx-global-store/demo/src/app/shared/book-store.service.ts new file mode 100644 index 00000000..0d4157fb --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/shared/book-store.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { Book } from './book'; + +/** + * In-Memory-Stand-in für ein echtes HTTP-Backend. + * Die Methoden geben – wie ein HttpClient-Service – Observables zurück, + * sodass der Effect-Code identisch zum Artikel bleibt. + */ +@Injectable({ providedIn: 'root' }) +export class BookStoreService { + private books: Book[] = [ + { isbn: '978-3-86490-900-1', title: 'Angular', rating: 5 }, + { isbn: '978-3-86490-466-2', title: 'Angular für Einsteiger', rating: 4 }, + { isbn: '978-3-96009-187-4', title: 'Webentwicklung mit Angular', rating: 4 } + ]; + + getAll(): Observable { + return of([...this.books]); + } + + create(book: Book): Observable { + this.books = [...this.books, book]; + return of(book); + } + + update(book: Book): Observable { + this.books = this.books.map(b => (b.isbn === book.isbn ? book : b)); + return of(book); + } + + remove(isbn: string): Observable { + this.books = this.books.filter(b => b.isbn !== isbn); + return of(undefined); + } +} diff --git a/material/ngrx-global-store/demo/src/app/shared/book.ts b/material/ngrx-global-store/demo/src/app/shared/book.ts new file mode 100644 index 00000000..fda322a3 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/shared/book.ts @@ -0,0 +1,5 @@ +export interface Book { + isbn: string; + title: string; + rating?: number; +} diff --git a/material/ngrx-global-store/demo/src/app/store/book.actions.ts b/material/ngrx-global-store/demo/src/app/store/book.actions.ts new file mode 100644 index 00000000..63a95e95 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/store/book.actions.ts @@ -0,0 +1,18 @@ +import { createAction, props } from '@ngrx/store'; +import { Book } from '../shared/book'; + +export const loadBooks = createAction('[Book] Load Books'); +export const loadBooksSuccess = createAction('[Book] Load Books Success', props<{ data: Book[] }>()); +export const loadBooksFailure = createAction('[Book] Load Books Failure', props<{ error: string }>()); + +export const createBook = createAction('[Book] Create Book', props<{ book: Book }>()); +export const createBookSuccess = createAction('[Book] Create Book Success', props<{ book: Book }>()); +export const createBookFailure = createAction('[Book] Create Book Failure', props<{ error: string }>()); + +export const updateBook = createAction('[Book] Update Book', props<{ book: Book }>()); +export const updateBookSuccess = createAction('[Book] Update Book Success', props<{ book: Book }>()); +export const updateBookFailure = createAction('[Book] Update Book Failure', props<{ error: string }>()); + +export const deleteBook = createAction('[Book] Delete Book', props<{ isbn: string }>()); +export const deleteBookSuccess = createAction('[Book] Delete Book Success', props<{ isbn: string }>()); +export const deleteBookFailure = createAction('[Book] Delete Book Failure', props<{ error: string }>()); diff --git a/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts new file mode 100644 index 00000000..21d381fb --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts @@ -0,0 +1,64 @@ +import { TestBed } from '@angular/core/testing'; +import { Observable, of, throwError } from 'rxjs'; +import { Action } from '@ngrx/store'; +import { provideMockActions } from '@ngrx/effects/testing'; + +import { BookEffects } from './book.effects'; +import { BookStoreService } from '../shared/book-store.service'; +import * as BookActions from './book.actions'; +import { Book } from '../shared/book'; + +const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); + +describe('BookEffects', () => { + let actions$: Observable; + + function setup(serviceMock: Partial): BookEffects { + TestBed.configureTestingModule({ + providers: [ + BookEffects, + provideMockActions(() => actions$), + { provide: BookStoreService, useValue: serviceMock } + ] + }); + return TestBed.inject(BookEffects); + } + + it('loadBooks$ feuert loadBooksSuccess mit den geladenen Büchern', () => { + const books = [b('1'), b('2')]; + const effects = setup({ getAll: () => of(books) }); + actions$ = of(BookActions.loadBooks()); + + let result: Action | undefined; + effects.loadBooks$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.loadBooksSuccess({ data: books })); + }); + + it('loadBooks$ feuert loadBooksFailure bei einem Fehler', () => { + const effects = setup({ getAll: () => throwError(() => ({ message: 'Netzwerkfehler' })) }); + actions$ = of(BookActions.loadBooks()); + + let result: Action | undefined; + effects.loadBooks$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.loadBooksFailure({ error: 'Netzwerkfehler' })); + }); + + it('createBook$ feuert createBookSuccess', () => { + const book = b('3', 'Neu'); + const effects = setup({ create: x => of(x) }); + actions$ = of(BookActions.createBook({ book })); + + let result: Action | undefined; + effects.createBook$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.createBookSuccess({ book })); + }); + + it('deleteBook$ feuert deleteBookSuccess mit der ISBN', () => { + const effects = setup({ remove: () => of(undefined) }); + actions$ = of(BookActions.deleteBook({ isbn: '1' })); + + let result: Action | undefined; + effects.deleteBook$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.deleteBookSuccess({ isbn: '1' })); + }); +}); diff --git a/material/ngrx-global-store/demo/src/app/store/book.effects.ts b/material/ngrx-global-store/demo/src/app/store/book.effects.ts new file mode 100644 index 00000000..a1b4e49c --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/store/book.effects.ts @@ -0,0 +1,63 @@ +import { inject, Injectable } from '@angular/core'; +import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { catchError, concatMap, map, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; + +import * as BookActions from './book.actions'; +import { BookStoreService } from '../shared/book-store.service'; + +@Injectable() +export class BookEffects { + private actions$ = inject(Actions); + private service = inject(BookStoreService); + + // Lesen: switchMap – eine neue Anfrage macht die alte überflüssig. + loadBooks$ = createEffect(() => + this.actions$.pipe( + ofType(BookActions.loadBooks), + switchMap(() => + this.service.getAll().pipe( + map(data => BookActions.loadBooksSuccess({ data })), + catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) + ) + ) + ) + ); + + // Schreiben: concatMap – laufende Requests werden nicht abgebrochen. + createBook$ = createEffect(() => + this.actions$.pipe( + ofType(BookActions.createBook), + concatMap(({ book }) => + this.service.create(book).pipe( + map(created => BookActions.createBookSuccess({ book: created })), + catchError(error => of(BookActions.createBookFailure({ error: error.message }))) + ) + ) + ) + ); + + updateBook$ = createEffect(() => + this.actions$.pipe( + ofType(BookActions.updateBook), + concatMap(({ book }) => + this.service.update(book).pipe( + map(updated => BookActions.updateBookSuccess({ book: updated })), + catchError(error => of(BookActions.updateBookFailure({ error: error.message }))) + ) + ) + ) + ); + + deleteBook$ = createEffect(() => + this.actions$.pipe( + ofType(BookActions.deleteBook), + concatMap(({ isbn }) => + this.service.remove(isbn).pipe( + map(() => BookActions.deleteBookSuccess({ isbn })), + catchError(error => of(BookActions.deleteBookFailure({ error: error.message }))) + ) + ) + ) + ); +} diff --git a/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts new file mode 100644 index 00000000..cff9d9f5 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts @@ -0,0 +1,56 @@ +import { initialState, reducer, State } from './book.reducer'; +import * as BookActions from './book.actions'; +import { Book } from '../shared/book'; + +const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); + +describe('Book Reducer', () => { + it('loadBooks setzt loading=true und error=null', () => { + const state = reducer({ ...initialState, error: 'alt' }, BookActions.loadBooks()); + expect(state.loading).toBe(true); + expect(state.error).toBeNull(); + }); + + it('loadBooksSuccess speichert die Bücher und beendet das Laden', () => { + const books = [b('1'), b('2')]; + const state = reducer({ ...initialState, loading: true }, BookActions.loadBooksSuccess({ data: books })); + expect(state.books).toEqual(books); + expect(state.loading).toBe(false); + }); + + it('loadBooksFailure setzt die Fehlermeldung', () => { + const state = reducer({ ...initialState, loading: true }, BookActions.loadBooksFailure({ error: 'boom' })); + expect(state.loading).toBe(false); + expect(state.error).toBe('boom'); + }); + + it('createBookSuccess hängt ein Buch immutabel an', () => { + const start: State = { ...initialState, books: [b('1')] }; + const state = reducer(start, BookActions.createBookSuccess({ book: b('2') })); + expect(state.books.map(x => x.isbn)).toEqual(['1', '2']); + expect(start.books.length).toBe(1); // Original unverändert + }); + + it('updateBookSuccess ersetzt das passende Buch', () => { + const start: State = { ...initialState, books: [b('1', 'Alt'), b('2', 'Zwei')] }; + const state = reducer(start, BookActions.updateBookSuccess({ book: b('1', 'Neu') })); + expect(state.books.find(x => x.isbn === '1')?.title).toBe('Neu'); + }); + + it('deleteBookSuccess entfernt das Buch anhand der ISBN', () => { + const start: State = { ...initialState, books: [b('1'), b('2')] }; + const state = reducer(start, BookActions.deleteBookSuccess({ isbn: '1' })); + expect(state.books.map(x => x.isbn)).toEqual(['2']); + }); + + it('alle Failure-Actions setzen error (Mehrfach-on)', () => { + expect(reducer(initialState, BookActions.createBookFailure({ error: 'c' })).error).toBe('c'); + expect(reducer(initialState, BookActions.updateBookFailure({ error: 'u' })).error).toBe('u'); + expect(reducer(initialState, BookActions.deleteBookFailure({ error: 'd' })).error).toBe('d'); + }); + + it('lässt den State bei unbekannter Action unverändert', () => { + const state = reducer(initialState, { type: 'unbekannt' } as never); + expect(state).toBe(initialState); + }); +}); diff --git a/material/ngrx-global-store/demo/src/app/store/book.reducer.ts b/material/ngrx-global-store/demo/src/app/store/book.reducer.ts new file mode 100644 index 00000000..7b6929bb --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/store/book.reducer.ts @@ -0,0 +1,53 @@ +import { createReducer, on } from '@ngrx/store'; +import { Book } from '../shared/book'; +import * as BookActions from './book.actions'; + +export const bookFeatureKey = 'book'; + +export interface State { + books: Book[]; + loading: boolean; + error: string | null; +} + +export const initialState: State = { + books: [], + loading: false, + error: null +}; + +export const reducer = createReducer( + initialState, + + on(BookActions.loadBooks, (state): State => ({ ...state, loading: true, error: null })), + on(BookActions.loadBooksSuccess, (state, action): State => ({ + ...state, + books: action.data, + loading: false + })), + on(BookActions.loadBooksFailure, (state, action): State => ({ + ...state, + loading: false, + error: action.error + })), + + on(BookActions.createBookSuccess, (state, action): State => ({ + ...state, + books: [...state.books, action.book] + })), + on(BookActions.updateBookSuccess, (state, action): State => ({ + ...state, + books: state.books.map(b => (b.isbn === action.book.isbn ? action.book : b)) + })), + on(BookActions.deleteBookSuccess, (state, action): State => ({ + ...state, + books: state.books.filter(b => b.isbn !== action.isbn) + })), + + on( + BookActions.createBookFailure, + BookActions.updateBookFailure, + BookActions.deleteBookFailure, + (state, action): State => ({ ...state, error: action.error }) + ) +); diff --git a/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts new file mode 100644 index 00000000..7472bd87 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts @@ -0,0 +1,22 @@ +import { selectAllBooks, selectBooksError, selectBooksLoading } from './book.selectors'; +import { Book } from '../shared/book'; + +const b = (isbn: string): Book => ({ isbn, title: `Title ${isbn}` }); + +describe('Book Selectors', () => { + const rootState = { + book: { books: [b('1'), b('2')], loading: true, error: 'x' } + } as never; + + it('selectAllBooks liefert die Buchliste', () => { + expect(selectAllBooks(rootState)).toEqual([b('1'), b('2')]); + }); + + it('selectBooksLoading liefert das loading-Flag', () => { + expect(selectBooksLoading(rootState)).toBe(true); + }); + + it('selectBooksError liefert die Fehlermeldung', () => { + expect(selectBooksError(rootState)).toBe('x'); + }); +}); diff --git a/material/ngrx-global-store/demo/src/app/store/book.selectors.ts b/material/ngrx-global-store/demo/src/app/store/book.selectors.ts new file mode 100644 index 00000000..8208871e --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/store/book.selectors.ts @@ -0,0 +1,21 @@ +import { createFeatureSelector, createSelector } from '@ngrx/store'; +import * as fromBook from './book.reducer'; + +export const selectBookState = createFeatureSelector( + fromBook.bookFeatureKey +); + +export const selectAllBooks = createSelector( + selectBookState, + state => state.books +); + +export const selectBooksLoading = createSelector( + selectBookState, + state => state.loading +); + +export const selectBooksError = createSelector( + selectBookState, + state => state.error +); diff --git a/material/ngrx-global-store/demo/src/index.html b/material/ngrx-global-store/demo/src/index.html new file mode 100644 index 00000000..f9f06e72 --- /dev/null +++ b/material/ngrx-global-store/demo/src/index.html @@ -0,0 +1,13 @@ + + + + + Demo + + + + + + + + diff --git a/material/ngrx-global-store/demo/src/main.ts b/material/ngrx-global-store/demo/src/main.ts new file mode 100644 index 00000000..5df75f9c --- /dev/null +++ b/material/ngrx-global-store/demo/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig) + .catch((err) => console.error(err)); diff --git a/material/ngrx-global-store/demo/src/styles.css b/material/ngrx-global-store/demo/src/styles.css new file mode 100644 index 00000000..90d4ee00 --- /dev/null +++ b/material/ngrx-global-store/demo/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/material/ngrx-global-store/demo/tsconfig.app.json b/material/ngrx-global-store/demo/tsconfig.app.json new file mode 100644 index 00000000..264f459b --- /dev/null +++ b/material/ngrx-global-store/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/material/ngrx-global-store/demo/tsconfig.json b/material/ngrx-global-store/demo/tsconfig.json new file mode 100644 index 00000000..2ab74427 --- /dev/null +++ b/material/ngrx-global-store/demo/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/material/ngrx-global-store/demo/tsconfig.spec.json b/material/ngrx-global-store/demo/tsconfig.spec.json new file mode 100644 index 00000000..d3837063 --- /dev/null +++ b/material/ngrx-global-store/demo/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "vitest/globals" + ] + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.spec.ts" + ] +} diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index d76f18c3..4b8da4b5 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -341,6 +341,9 @@ export const BookStore = signalStore( booksCount: computed(() => books().length) })), withMethods((store, service = inject(BookStoreService)) => ({ + clearError(): void { + patchState(store, { error: null }); + }, loadBooks: rxMethod( pipe( tap(() => patchState(store, { loading: true, error: null })), diff --git a/material/ngrx-signal-store/demo/.editorconfig b/material/ngrx-signal-store/demo/.editorconfig new file mode 100644 index 00000000..f166060d --- /dev/null +++ b/material/ngrx-signal-store/demo/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/material/ngrx-signal-store/demo/.gitignore b/material/ngrx-signal-store/demo/.gitignore new file mode 100644 index 00000000..854acd5f --- /dev/null +++ b/material/ngrx-signal-store/demo/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/material/ngrx-signal-store/demo/.prettierrc b/material/ngrx-signal-store/demo/.prettierrc new file mode 100644 index 00000000..d6c16d7e --- /dev/null +++ b/material/ngrx-signal-store/demo/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] +} diff --git a/material/ngrx-signal-store/demo/.vscode/extensions.json b/material/ngrx-signal-store/demo/.vscode/extensions.json new file mode 100644 index 00000000..77b37457 --- /dev/null +++ b/material/ngrx-signal-store/demo/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/material/ngrx-signal-store/demo/.vscode/launch.json b/material/ngrx-signal-store/demo/.vscode/launch.json new file mode 100644 index 00000000..925af837 --- /dev/null +++ b/material/ngrx-signal-store/demo/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/material/ngrx-signal-store/demo/.vscode/mcp.json b/material/ngrx-signal-store/demo/.vscode/mcp.json new file mode 100644 index 00000000..956af8c6 --- /dev/null +++ b/material/ngrx-signal-store/demo/.vscode/mcp.json @@ -0,0 +1,9 @@ +{ + // For more information, visit: https://angular.dev/ai/mcp + "servers": { + "angular-cli": { + "command": "npx", + "args": ["-y", "@angular/cli", "mcp"] + } + } +} diff --git a/material/ngrx-signal-store/demo/.vscode/tasks.json b/material/ngrx-signal-store/demo/.vscode/tasks.json new file mode 100644 index 00000000..244306f9 --- /dev/null +++ b/material/ngrx-signal-store/demo/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/material/ngrx-signal-store/demo/README.md b/material/ngrx-signal-store/demo/README.md new file mode 100644 index 00000000..63bf475a --- /dev/null +++ b/material/ngrx-signal-store/demo/README.md @@ -0,0 +1,19 @@ +# Beispiel-App: SignalStore + +Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil 3: SignalStore](../README.md)**. + +Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern und löschen (CRUD) mit `signalStore`, `withState`, `withComputed`, `withMethods`, `rxMethod` und `patchState`, inklusive Fehler-State. + +- **Angular 21** + **NgRx 21** (`@ngrx/signals`, `@ngrx/operators`) +- Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. + +## Befehle + +```bash +npm install +npm test # Vitest: Store-Tests (Laden, CRUD, Fehler) + Komponente +npm start # ng serve +npm run build +``` + +Die Tests beweisen den Store wie einen gewöhnlichen Service: Instanz beziehen, Methoden aufrufen, Signale auslesen – ganz ohne Store-Setup. diff --git a/material/ngrx-signal-store/demo/angular.json b/material/ngrx-signal-store/demo/angular.json new file mode 100644 index 00000000..3043937e --- /dev/null +++ b/material/ngrx-signal-store/demo/angular.json @@ -0,0 +1,73 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm" + }, + "newProjectRoot": "projects", + "projects": { + "demo": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.css" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "demo:build:production" + }, + "development": { + "buildTarget": "demo:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/material/ngrx-signal-store/demo/package-lock.json b/material/ngrx-signal-store/demo/package-lock.json new file mode 100644 index 00000000..28c73da2 --- /dev/null +++ b/material/ngrx-signal-store/demo/package-lock.json @@ -0,0 +1,8637 @@ +{ + "name": "demo", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "demo", + "version": "0.0.0", + "dependencies": { + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", + "@ngrx/operators": "^21.1.1", + "@ngrx/signals": "^21.1.1", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.2.7", + "@angular/cli": "^21.2.7", + "@angular/compiler-cli": "^21.2.0", + "jsdom": "^28.0.0", + "prettier": "^3.8.1", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/abtesting": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", + "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", + "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", + "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", + "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", + "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", + "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", + "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", + "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", + "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", + "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", + "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", + "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", + "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", + "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2102.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.15.tgz", + "integrity": "sha512-EZQXu6j7J7OUxmpxIO2mxd58NTlCb7HOAOfLLGdk7lRcwZeCxnjGRzb72tXlJgIEi3IoOYwcW0ft2cg7r/d6qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.15", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.15.tgz", + "integrity": "sha512-x7EwuQtMGHANVznHpwyEi/3lMo/kRoaxV2w7lXbRGjTezwv4SPaOBwhrIGCbAVFs5B1ziff4jZzors4owlCTgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.18.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.4", + "rxjs": "7.8.2", + "source-map": "0.7.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.15.tgz", + "integrity": "sha512-kHHV694KUPuXlItjvnxUspn+KYjlTKu8KINX/kT1qlogzLYojpnRwcUtZyRPUz1f/M1d3TUHVFeBKx+h5HoNcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.15", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.21", + "ora": "9.3.0", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/build": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.15.tgz", + "integrity": "sha512-APJ5v0/hL38CKSqSQDos5Y8/O2LSiSSqP9FagdcMT5j9JAlENXdhVaA3IxPMXDWcxtzD5T1IlN8Z9aFxDXxHyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2102.15", + "@babel/core": "7.29.0", + "@babel/helper-annotate-as-pure": "7.27.3", + "@babel/helper-split-export-declaration": "7.24.7", + "@inquirer/confirm": "5.1.21", + "@vitejs/plugin-basic-ssl": "2.1.4", + "beasties": "0.4.1", + "browserslist": "^4.26.0", + "esbuild": "0.27.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "magic-string": "0.30.21", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "8.0.0", + "picomatch": "4.0.4", + "piscina": "5.1.4", + "rolldown": "1.0.0-rc.4", + "sass": "1.97.3", + "semver": "7.7.4", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.15", + "undici": "7.24.4", + "vite": "7.3.2", + "watchpack": "2.5.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.1" + }, + "peerDependencies": { + "@angular/compiler": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/localize": "^21.0.0", + "@angular/platform-browser": "^21.0.0", + "@angular/platform-server": "^21.0.0", + "@angular/service-worker": "^21.0.0", + "@angular/ssr": "^21.2.15", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^21.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.9 <6.0", + "vitest": "^4.0.8" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.15.tgz", + "integrity": "sha512-8CT1iST5CwdxHEzC0NVFUsMenAY7aE30ayTAw5PpA6MPs6bdHlL6QxTmkqThW7hoCj6PJ56YfQMzSQU+aYRLQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2102.15", + "@angular-devkit/core": "21.2.15", + "@angular-devkit/schematics": "21.2.15", + "@inquirer/prompts": "7.10.1", + "@listr2/prompt-adapter-inquirer": "3.0.5", + "@modelcontextprotocol/sdk": "1.26.0", + "@schematics/angular": "21.2.15", + "@yarnpkg/lockfile": "1.1.0", + "algoliasearch": "5.48.1", + "ini": "6.0.0", + "jsonc-parser": "3.3.1", + "listr2": "9.0.5", + "npm-package-arg": "13.0.2", + "pacote": "21.3.1", + "parse5-html-rewriting-stream": "8.0.0", + "semver": "7.7.4", + "yargs": "18.0.0", + "zod": "4.3.6" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.17.tgz", + "integrity": "sha512-hqAQxRfi5ldFE42suAXRcY+JCANrUh7fuSQ/DtZ7L896id5BT/exuv6dWNBC1PyAfQmRbpD5Pt6/pd+tNLyhDQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/core": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.17.tgz", + "integrity": "sha512-p+NdjYiwAz9Zmu2yul0LlMXaFjMISVVa24+/MVMoKFeQeI82QE8jDywPlnOSHQHvdCcQVpS7saeEriZzX3JuBQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.17.tgz", + "integrity": "sha512-KithZ3b0HBFH0NbUcswBcjpN9y09vLbarMD7qmGWTnGUBk4W8cn4sbT8zJyv9CRKg9ZcuUBeJYKUfUPn/u/5OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.29.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.17", + "typescript": ">=5.9 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/core": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.17.tgz", + "integrity": "sha512-wYHpwIdnUnjQFOJJNqRcGx7LS3u64jT+R9L0TnMR/ViBM9dQgGYImlSikkftg2yrFCNo5aKRxhG2LLskQurVdg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.17.tgz", + "integrity": "sha512-WKu8XeRSNZo+a+aDDZ3M5OtReF7KYqR/PmZ2l1lSf6N5EEAmc+Ky4aqbRhTL/mTSfHrO4+TDJ4C5A2tFmuwIeA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.17", + "@angular/core": "21.2.17", + "@angular/platform-browser": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.17.tgz", + "integrity": "sha512-ROdSliejY37g1EphYmweYdm5cHM8HY3X4tbWt4ubxmhTyYgfN3nxrxfGQ/n7Mz5tDY9VXVLIGDgjLOGYOo4uTQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/animations": "21.2.17", + "@angular/common": "21.2.17", + "@angular/core": "21.2.17" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/router": { + "version": "21.2.17", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.17.tgz", + "integrity": "sha512-RSCtK5ppAV6y6wfRLHSK2a9Wc/vm8j0wsC+/j9PH9yQmppWFVXDWsg5E39MKOIpnoYVx2+hI6eak6+wYtZTe1A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/common": "21.2.17", + "@angular/core": "21.2.17", + "@angular/platform-browser": "21.2.17", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.4.tgz", + "integrity": "sha512-yI8kNhHiOrLb8Rlulsk07DeQz0PwyT69FX9dkz5rAp7p9RUwFKEXnZpBGzURiLHgi66YqIWxOHn1nij8Lrg27Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", + "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8", + "listr2": "9.0.5" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", + "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", + "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", + "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", + "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", + "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", + "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", + "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@ngrx/operators": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-21.1.1.tgz", + "integrity": "sha512-2ChT1rVi6w8xHrmIAqRFuvY7YLAJiH7idfmbimjOYHzJ7pgaz0+4WVqyCp9pv2UiDEXuvpUOvs3afgS5m1LDNQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngrx/signals": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/@ngrx/signals/-/signals-21.1.1.tgz", + "integrity": "sha512-AXxsvO39cJ4gu2GMvLyUHwx+3qHi35Bn7OUqyIfJW0fffi/Af7ML6h0MPMWDHVROo55FBgxzJrYRswwzdew9VA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": "^21.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } + } + }, + "node_modules/@npmcli/agent": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-4.0.2.tgz", + "integrity": "sha512-EUEuWAxnL07Sp5/iC/1X6Xj+XThUvnbei9zfRWZdEXa7lss9RTHMhAHBeg+MZ5To9s/gGaSI+UwZTPdYMvKSeg==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-5.0.0.tgz", + "integrity": "sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-7.0.2.tgz", + "integrity": "sha512-oeolHDjExNAJAnlYP2qzNjMX/Xi9bmu78C9dIGr4xjobrSKbuMYCph8lTzn4vnW3NjIqVmw/f8BCfouqyJXlRg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-4.0.0.tgz", + "integrity": "sha512-yNyAdkBxB72gtZ4GrwXCM0ZUedo9nIbOMKfGjt6Cu6DXf0p8y1PViZAKDC8q8kv/fufx0WTjRBdSlyrvnP7hmA==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-5.0.0.tgz", + "integrity": "sha512-uuG5HZFXLfyFKqg8QypsmgLQW7smiRjVc45bqD/ofZZcR/uxEjgQU8qDPv0s9TEeMUiAAU/GC5bR6++UdTirIQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-7.0.5.tgz", + "integrity": "sha512-iVuTlG3ORq2iaVa1IWUxAO/jIp77tUKBhoMjuzYW2kL4MLN1bi/ofqkZ7D7OOwh8coAx1/S2ge0rMdGv8sLSOQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", + "integrity": "sha512-OLUaoqBuyxeTqUvjA3FZFiXUfYC1alp3Sa99gW3EUDz3tZ3CbXDdcZ7qWKBzicrJleIgucoWamWH1saAmH/l2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-4.0.0.tgz", + "integrity": "sha512-gOBg5YHMfZy+TfHArfVogwgfBeQnKbbGo3pSUyK/gSI0AVu+pEiDVcKlQb0D8Mg1LNRZILZ6XG8I5dJ4KuAd9Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-10.0.4.tgz", + "integrity": "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.113.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", + "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", + "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", + "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", + "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", + "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", + "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", + "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", + "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", + "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@schematics/angular": { + "version": "21.2.15", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.15.tgz", + "integrity": "sha512-xdTirIUUegMXtyEDsYabCRcAnlVatVYb6S8v77fgK2eqUhGalI2e/3+L51N3XF1+6K2vEhyDpmmhFZLsdhLYbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "21.2.15", + "@angular-devkit/schematics": "21.2.15", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz", + "integrity": "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/core": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-3.2.1.tgz", + "integrity": "sha512-qRsxPnCrbC/puegGxKuynfnxgLiHqWStrSjxkoB4YKqq3Z3s4cyZyj42ZdWFAEblNP65C+rBH8EuREHIXoi83g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-4.1.1.tgz", + "integrity": "sha512-Hf4xglukg0XXQ2RiD5vSoLjdPe8OBUPA8XeVjUObheuDcWdYWrnH/BNmxZCzkAy68MzmNCxXLeurJvs6hcP2OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-4.0.2.tgz", + "integrity": "sha512-TCAzTy0xzdP79EnxSjq9KQ3eaR7+FmudLC6eRKknVKZbV7ZNlGLClAAQb/HMNJ5n2OBNk2GT1tEmU0xuPr+SLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-3.1.1.tgz", + "integrity": "sha512-qv7+G3J2cc6wwFj3yKvXOamzqhMwSk1ogPGmhpS8iXllcPrJaIIBA+4HbttlHVu1pqWTdmaCH/WE7UOC51kdoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-4.1.0.tgz", + "integrity": "sha512-Y8cK9aggNRsqJVaKUlEYs4s7CvQ1b1ta2DVPyAimb0I2qhzjNk+A+mxvll/klL0RlfuIUei8BF7YWiua4kQqww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", + "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/algoliasearch": { + "version": "5.48.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", + "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.14.1", + "@algolia/client-abtesting": "5.48.1", + "@algolia/client-analytics": "5.48.1", + "@algolia/client-common": "5.48.1", + "@algolia/client-insights": "5.48.1", + "@algolia/client-personalization": "5.48.1", + "@algolia/client-query-suggestions": "5.48.1", + "@algolia/client-search": "5.48.1", + "@algolia/ingestion": "1.48.1", + "@algolia/monitoring": "1.48.1", + "@algolia/recommend": "5.48.1", + "@algolia/requester-browser-xhr": "5.48.1", + "@algolia/requester-fetch": "5.48.1", + "@algolia/requester-node-http": "5.48.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.37", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", + "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", + "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^6.0.0", + "css-what": "^7.0.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^10.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-20.0.4.tgz", + "integrity": "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssstyle": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.372", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", + "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-walk": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz", + "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/immutable": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", + "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", + "integrity": "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", + "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.1", + "@lmdb/lmdb-darwin-x64": "3.5.1", + "@lmdb/lmdb-linux-arm": "3.5.1", + "@lmdb/lmdb-linux-arm64": "3.5.1", + "@lmdb/lmdb-linux-x64": "3.5.1", + "@lmdb/lmdb-win32-arm64": "3.5.1", + "@lmdb/lmdb-win32-x64": "3.5.1" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-fetch-happen": { + "version": "15.0.6", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-15.0.6.tgz", + "integrity": "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-5.0.2.tgz", + "integrity": "sha512-2d0q2a8eCi2IRg/IGubCNRJoYbA1+YPXAzQVRFmB45gdGZafyivnZ5YSEfo3JikbjGxOdntGFvBQGqaSMXlAFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/undici": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.26.0.tgz", + "integrity": "sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-bundled": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", + "integrity": "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-install-checks": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-8.0.0.tgz", + "integrity": "sha512-ScAUdMpyzkbpxoNekQ3tNRdFI8SJ86wgKZSQZdUxT+bj0wVFpsEMWnkXP0twVe1gJyNF5apBWDJhhIbgrIViRA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-5.0.0.tgz", + "integrity": "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-package-arg": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-13.0.2.tgz", + "integrity": "sha512-IciCE3SY3uE84Ld8WZU23gAPPV9rIYod4F+rc+vJ7h7cwAJt9Vk6TVsK60ry7Uj3SRS3bqRRIGuTp9YVlk6WNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-packlist": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.4.tgz", + "integrity": "sha512-uMW73iajD8hiH4ZBxEV3HC+eTnppIqwakjOYuvgddnalIw2lJguKviK1pcUJDlIWm1wSJkchpDZDSVVsZEYRng==", + "dev": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-11.0.3.tgz", + "integrity": "sha512-buzyCfeoGY/PxKqmBqn1IUJrZnUi1VVJTdSSRPGI60tJdUhUoSQFhs0zycJokDdOznQentgrpf8LayEHyyYlqQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-19.1.1.tgz", + "integrity": "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", + "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.1", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", + "integrity": "sha512-QkCdPooczexPLiXIrbVOPYkR3VO3T6v2OyKRkR1Xbhpy7/LAVXwahnRCgRp78Oe/Ehf0C/HATAxfSr6eA1oX+w==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pacote": { + "version": "21.3.1", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", + "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", + "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", + "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", + "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.113.0", + "@rolldown/pluginutils": "1.0.0-rc.4" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", + "@rolldown/binding-darwin-x64": "1.0.0-rc.4", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.97.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", + "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-4.1.1.tgz", + "integrity": "sha512-endqECJkfhozrXMK5ngu/UAA0xVcVEFdnHJCElGaExypjW+HK5i6zu3NteLoaX/iFbRUbC3+DjttQs0GARr+5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ssri": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-13.0.1.tgz", + "integrity": "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tar": { + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.2.tgz", + "integrity": "sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.2" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.2.tgz", + "integrity": "sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tuf-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-4.1.0.tgz", + "integrity": "sha512-50QV99kCKH5P/Vs4E2Gzp7BopNV+KzTXqWeaxrfu5IQJBOULRsTIS9seSsOVT8ZnGXzCyx55nYWAi4qJzpZKEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", + "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/material/ngrx-signal-store/demo/package.json b/material/ngrx-signal-store/demo/package.json new file mode 100644 index 00000000..9e3ccaf6 --- /dev/null +++ b/material/ngrx-signal-store/demo/package.json @@ -0,0 +1,34 @@ +{ + "name": "demo", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "packageManager": "npm@10.9.4", + "dependencies": { + "@angular/common": "^21.2.0", + "@angular/compiler": "^21.2.0", + "@angular/core": "^21.2.0", + "@angular/forms": "^21.2.0", + "@angular/platform-browser": "^21.2.0", + "@angular/router": "^21.2.0", + "@ngrx/operators": "^21.1.1", + "@ngrx/signals": "^21.1.1", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^21.2.7", + "@angular/cli": "^21.2.7", + "@angular/compiler-cli": "^21.2.0", + "jsdom": "^28.0.0", + "prettier": "^3.8.1", + "typescript": "~5.9.2", + "vitest": "^4.0.8" + } +} diff --git a/material/ngrx-signal-store/demo/public/favicon.ico b/material/ngrx-signal-store/demo/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 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 Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/material/ngrx-signal-store/demo/src/app/app.config.ts b/material/ngrx-signal-store/demo/src/app/app.config.ts new file mode 100644 index 00000000..7f244ed8 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/app.config.ts @@ -0,0 +1,8 @@ +import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + + ] +}; diff --git a/material/ngrx-signal-store/demo/src/app/app.css b/material/ngrx-signal-store/demo/src/app/app.css new file mode 100644 index 00000000..e69de29b diff --git a/material/ngrx-signal-store/demo/src/app/app.html b/material/ngrx-signal-store/demo/src/app/app.html new file mode 100644 index 00000000..475128a5 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/app.html @@ -0,0 +1 @@ + diff --git a/material/ngrx-signal-store/demo/src/app/app.spec.ts b/material/ngrx-signal-store/demo/src/app/app.spec.ts new file mode 100644 index 00000000..b094eac8 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/app.spec.ts @@ -0,0 +1,21 @@ +import { TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; +import { App } from './app'; +import { BookStoreService } from './shared/book-store.service'; + +describe('App', () => { + it('erzeugt die App und zeigt die Buchliste an', async () => { + await TestBed.configureTestingModule({ + imports: [App], + providers: [ + { provide: BookStoreService, useValue: { getAll: () => of([]) } } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(App); + await fixture.whenStable(); + + const h1 = fixture.nativeElement.querySelector('h1') as HTMLElement; + expect(h1?.textContent).toContain('Books'); + }); +}); diff --git a/material/ngrx-signal-store/demo/src/app/app.ts b/material/ngrx-signal-store/demo/src/app/app.ts new file mode 100644 index 00000000..029de745 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/app.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { BookList } from './book-list/book-list'; + +@Component({ + selector: 'app-root', + imports: [BookList], + templateUrl: './app.html', + styleUrl: './app.css' +}) +export class App {} diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.html b/material/ngrx-signal-store/demo/src/app/book-list/book-list.html new file mode 100644 index 00000000..a3ce042f --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.html @@ -0,0 +1,21 @@ +

    Books ({{ store.booksCount() }})

    + +@if (store.loading()) { +

    Loading ...

    +} + +@if (store.error(); as error) { +

    + {{ error }} + +

    +} + +
      + @for (book of store.books(); track book.isbn) { +
    • + {{ book.title }} + +
    • + } +
    diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts b/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts new file mode 100644 index 00000000..450379b9 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts @@ -0,0 +1,19 @@ +import { Component, inject } from '@angular/core'; +import { BookStore } from '../book.store'; +import { Book } from '../shared/book'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.html' +}) +export class BookList { + protected store = inject(BookStore); + + deleteBook(isbn: string): void { + this.store.deleteBook(isbn); + } + + addBook(book: Book): void { + this.store.addBook(book); + } +} diff --git a/material/ngrx-signal-store/demo/src/app/book.store.spec.ts b/material/ngrx-signal-store/demo/src/app/book.store.spec.ts new file mode 100644 index 00000000..cf13c6f9 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/book.store.spec.ts @@ -0,0 +1,84 @@ +import { TestBed } from '@angular/core/testing'; +import { of, throwError } from 'rxjs'; + +import { BookStore } from './book.store'; +import { BookStoreService } from './shared/book-store.service'; +import { Book } from './shared/book'; + +const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); + +/** Erzeugt einen frischen Store mit gemocktem BookStoreService. */ +function createStore(mock: Partial) { + TestBed.configureTestingModule({ + providers: [{ provide: BookStoreService, useValue: mock }] + }); + return TestBed.inject(BookStore); +} + +describe('BookStore (SignalStore)', () => { + it('lädt beim Init die Bücher und zählt sie', () => { + const store = createStore({ getAll: () => of([b('1'), b('2'), b('3')]) }); + + expect(store.loading()).toBe(false); + expect(store.error()).toBeNull(); + expect(store.books().length).toBe(3); + expect(store.booksCount()).toBe(3); + }); + + it('addBook hängt ein Buch immutabel an die Liste an', () => { + const store = createStore({ + getAll: () => of([b('1')]), + create: book => of(book) + }); + expect(store.booksCount()).toBe(1); + + store.addBook(b('2', 'Neues Buch')); + + expect(store.booksCount()).toBe(2); + expect(store.books().map(x => x.isbn)).toEqual(['1', '2']); + }); + + it('updateBook ersetzt das passende Buch', () => { + const store = createStore({ + getAll: () => of([b('1', 'Alt'), b('2', 'Zwei')]), + update: book => of(book) + }); + + store.updateBook(b('1', 'Neu')); + + expect(store.books().find(x => x.isbn === '1')?.title).toBe('Neu'); + expect(store.booksCount()).toBe(2); + }); + + it('deleteBook entfernt das Buch anhand der ISBN', () => { + const store = createStore({ + getAll: () => of([b('1'), b('2')]), + remove: () => of(undefined) + }); + + store.deleteBook('1'); + + expect(store.booksCount()).toBe(1); + expect(store.books()[0].isbn).toBe('2'); + }); + + it('schreibt eine Fehlermeldung in den State, wenn das Laden fehlschlägt', () => { + const store = createStore({ + getAll: () => throwError(() => ({ message: 'Netzwerkfehler' })) + }); + + expect(store.error()).toBe('Netzwerkfehler'); + expect(store.loading()).toBe(false); + }); + + it('clearError setzt die Fehlermeldung zurück', () => { + const store = createStore({ + getAll: () => throwError(() => ({ message: 'Netzwerkfehler' })) + }); + expect(store.error()).toBe('Netzwerkfehler'); + + store.clearError(); + + expect(store.error()).toBeNull(); + }); +}); diff --git a/material/ngrx-signal-store/demo/src/app/book.store.ts b/material/ngrx-signal-store/demo/src/app/book.store.ts new file mode 100644 index 00000000..39e0d856 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/book.store.ts @@ -0,0 +1,106 @@ +import { computed, inject } from '@angular/core'; +import { concatMap, pipe, switchMap, tap } from 'rxjs'; +import { + patchState, + signalStore, + withComputed, + withHooks, + withMethods, + withState +} from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { tapResponse } from '@ngrx/operators'; + +import { Book } from './shared/book'; +import { BookStoreService } from './shared/book-store.service'; + +type BookState = { + books: Book[]; + loading: boolean; + error: string | null; +}; + +const initialState: BookState = { + books: [], + loading: false, + error: null +}; + +export const BookStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withComputed(({ books }) => ({ + booksCount: computed(() => books().length) + })), + withMethods((store, service = inject(BookStoreService)) => ({ + clearError(): void { + patchState(store, { error: null }); + }, + loadBooks: rxMethod( + pipe( + tap(() => patchState(store, { loading: true, error: null })), + switchMap(() => + service.getAll().pipe( + tapResponse({ + next: books => patchState(store, { books }), + error: (err: { message: string }) => + patchState(store, { error: err.message }), + finalize: () => patchState(store, { loading: false }) + }) + ) + ) + ) + ), + addBook: rxMethod( + pipe( + concatMap(book => + service.create(book).pipe( + tapResponse({ + next: created => + patchState(store, state => ({ books: [...state.books, created] })), + error: (err: { message: string }) => + patchState(store, { error: err.message }) + }) + ) + ) + ) + ), + updateBook: rxMethod( + pipe( + concatMap(book => + service.update(book).pipe( + tapResponse({ + next: updated => + patchState(store, state => ({ + books: state.books.map(b => (b.isbn === updated.isbn ? updated : b)) + })), + error: (err: { message: string }) => + patchState(store, { error: err.message }) + }) + ) + ) + ) + ), + deleteBook: rxMethod( + pipe( + concatMap(isbn => + service.remove(isbn).pipe( + tapResponse({ + next: () => + patchState(store, state => ({ + books: state.books.filter(b => b.isbn !== isbn) + })), + error: (err: { message: string }) => + patchState(store, { error: err.message }) + }) + ) + ) + ) + ) + })), + withHooks({ + onInit(store) { + store.loadBooks(); + } + }) +); diff --git a/material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts b/material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts new file mode 100644 index 00000000..8d1ebab3 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { Book } from './book'; + +/** + * In-Memory-Stand-in für ein echtes HTTP-Backend. + * Die Methoden geben – wie ein HttpClient-Service – Observables zurück, + * sodass der Store-Code identisch zum Artikel bleibt. + */ +@Injectable({ providedIn: 'root' }) +export class BookStoreService { + private books: Book[] = [ + { isbn: '978-3-86490-900-1', title: 'Angular', rating: 5 }, + { isbn: '978-3-86490-466-2', title: 'Angular für Einsteiger', rating: 4 }, + { isbn: '978-3-96009-187-4', title: 'Webentwicklung mit Angular', rating: 4 } + ]; + + getAll(): Observable { + return of([...this.books]); + } + + create(book: Book): Observable { + this.books = [...this.books, book]; + return of(book); + } + + update(book: Book): Observable { + this.books = this.books.map(b => (b.isbn === book.isbn ? book : b)); + return of(book); + } + + remove(isbn: string): Observable { + this.books = this.books.filter(b => b.isbn !== isbn); + return of(undefined); + } +} diff --git a/material/ngrx-signal-store/demo/src/app/shared/book.ts b/material/ngrx-signal-store/demo/src/app/shared/book.ts new file mode 100644 index 00000000..fda322a3 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/shared/book.ts @@ -0,0 +1,5 @@ +export interface Book { + isbn: string; + title: string; + rating?: number; +} diff --git a/material/ngrx-signal-store/demo/src/index.html b/material/ngrx-signal-store/demo/src/index.html new file mode 100644 index 00000000..f9f06e72 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/index.html @@ -0,0 +1,13 @@ + + + + + Demo + + + + + + + + diff --git a/material/ngrx-signal-store/demo/src/main.ts b/material/ngrx-signal-store/demo/src/main.ts new file mode 100644 index 00000000..5df75f9c --- /dev/null +++ b/material/ngrx-signal-store/demo/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig) + .catch((err) => console.error(err)); diff --git a/material/ngrx-signal-store/demo/src/styles.css b/material/ngrx-signal-store/demo/src/styles.css new file mode 100644 index 00000000..90d4ee00 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/material/ngrx-signal-store/demo/tsconfig.app.json b/material/ngrx-signal-store/demo/tsconfig.app.json new file mode 100644 index 00000000..264f459b --- /dev/null +++ b/material/ngrx-signal-store/demo/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/material/ngrx-signal-store/demo/tsconfig.json b/material/ngrx-signal-store/demo/tsconfig.json new file mode 100644 index 00000000..2ab74427 --- /dev/null +++ b/material/ngrx-signal-store/demo/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/material/ngrx-signal-store/demo/tsconfig.spec.json b/material/ngrx-signal-store/demo/tsconfig.spec.json new file mode 100644 index 00000000..d3837063 --- /dev/null +++ b/material/ngrx-signal-store/demo/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "vitest/globals" + ] + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.spec.ts" + ] +} From 0dfabe78581dec60fde61071e75240f240540506 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Sun, 14 Jun 2026 00:22:27 +0200 Subject: [PATCH 15/24] material(ngrx): Beispiel-Apps auf Angular 22 angehoben (NgRx 21 via legacy-peer-deps) - ng update @angular/{cli,core}@22 in beiden Demos (Signal Store + Global Store) - NgRx bleibt 21.1.1: laeuft unter Angular 22; .npmrc mit legacy-peer-deps=true sorgt fuer reibungsloses "npm install" (NgRx-21-Peer-Range ist noch ^21.0.0) - ChangeDetectionStrategy.Eager-Migration zurueckgenommen, damit die Demo deckungsgleich zum Artikel bleibt; tsconfig-Diagnostics der Migration uebernommen - Beweis auf Angular 22 (frischer npm install): Signal Store 7/7, Global Store 16/16 Vitest gruen, beide bauen (124 kB / 146 kB) - Demo-READMEs auf "Angular 22" aktualisiert inkl. Hinweis zur Peer-Range --- material/ngrx-global-store/demo/.npmrc | 5 + material/ngrx-global-store/demo/README.md | 4 +- .../ngrx-global-store/demo/package-lock.json | 2516 ++++++++++------- material/ngrx-global-store/demo/package.json | 20 +- .../ngrx-global-store/demo/tsconfig.app.json | 16 +- material/ngrx-signal-store/demo/.npmrc | 5 + material/ngrx-signal-store/demo/README.md | 4 +- .../ngrx-signal-store/demo/package-lock.json | 2516 ++++++++++------- material/ngrx-signal-store/demo/package.json | 20 +- .../ngrx-signal-store/demo/tsconfig.app.json | 16 +- 10 files changed, 2956 insertions(+), 2166 deletions(-) create mode 100644 material/ngrx-global-store/demo/.npmrc create mode 100644 material/ngrx-signal-store/demo/.npmrc diff --git a/material/ngrx-global-store/demo/.npmrc b/material/ngrx-global-store/demo/.npmrc new file mode 100644 index 00000000..a5518629 --- /dev/null +++ b/material/ngrx-global-store/demo/.npmrc @@ -0,0 +1,5 @@ +# NgRx 21 deklariert als Peer-Range noch "@angular/core: ^21.0.0". +# Die Bibliothek laeuft aber problemlos unter Angular 22 (PR ngrx/platform#5155). +# Damit ein einfaches "npm install" nicht an dieser Peer-Range scheitert, +# ignorieren wir die Peer-Dependency-Pruefung (entspricht "npm install --legacy-peer-deps"). +legacy-peer-deps=true diff --git a/material/ngrx-global-store/demo/README.md b/material/ngrx-global-store/demo/README.md index cf837f8c..55ccdd05 100644 --- a/material/ngrx-global-store/demo/README.md +++ b/material/ngrx-global-store/demo/README.md @@ -4,9 +4,11 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern und löschen (CRUD) mit Actions, Reducer, Selektoren und Effects, inklusive Fehler-State. -- **Angular 21** + **NgRx 21** (`@ngrx/store`, `@ngrx/effects`) +- **Angular 22** + **NgRx 21** (`@ngrx/store`, `@ngrx/effects`) - Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. +> **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. + ## Befehle ```bash diff --git a/material/ngrx-global-store/demo/package-lock.json b/material/ngrx-global-store/demo/package-lock.json index 31b657f6..3af3f21d 100644 --- a/material/ngrx-global-store/demo/package-lock.json +++ b/material/ngrx-global-store/demo/package-lock.json @@ -8,12 +8,12 @@ "name": "demo", "version": "0.0.0", "dependencies": { - "@angular/common": "^21.2.0", - "@angular/compiler": "^21.2.0", - "@angular/core": "^21.2.0", - "@angular/forms": "^21.2.0", - "@angular/platform-browser": "^21.2.0", - "@angular/router": "^21.2.0", + "@angular/common": "^22.0.1", + "@angular/compiler": "^22.0.1", + "@angular/core": "^22.0.1", + "@angular/forms": "^22.0.1", + "@angular/platform-browser": "^22.0.1", + "@angular/router": "^22.0.1", "@ngrx/effects": "^21.1.1", "@ngrx/operators": "^21.1.1", "@ngrx/store": "^21.1.1", @@ -21,12 +21,12 @@ "tslib": "^2.3.0" }, "devDependencies": { - "@angular/build": "^21.2.7", - "@angular/cli": "^21.2.7", - "@angular/compiler-cli": "^21.2.0", + "@angular/build": "^22.0.1", + "@angular/cli": "^22.0.1", + "@angular/compiler-cli": "^22.0.1", "jsdom": "^28.0.0", "prettier": "^3.8.1", - "typescript": "~5.9.2", + "typescript": "~6.0.3", "vitest": "^4.0.8" } }, @@ -38,57 +38,57 @@ "license": "MIT" }, "node_modules/@algolia/abtesting": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", - "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.0.tgz", + "integrity": "sha512-8siuLG+FIns1AjZ/g2SDVwHz9S+ObacDQISEJvS8XsNei1zl3FXqfqQrBpmrG7ACWCyesXHbicMJtvRbg00FEw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-abtesting": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", - "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.0.tgz", + "integrity": "sha512-wtwPgyPmO7b7sQPVgoK29c1VpfS08DnnJCmxX/oU1pV2DlMRJCzQcLN7JSloYpodyKHwM8+9wOzlAM0co3TDmA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", - "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.0.tgz", + "integrity": "sha512-9KY36bRl4AH7RjqSeDDOKnjsz4IxQFBEOB8/fWmEbdQe+Isbs5jGzVJu9NEPQ1Tgwxlf8Uf07Swj3jZyMNUZ2g==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", - "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.0.tgz", + "integrity": "sha512-3a/qM3dzJqqfTx7Yrw7uGQ98I3Q0rDfb4Vkv0wEzko96l7YQMxfBVz/VbLq2N+c59GweYv6Vhp8mPeqnWJSITw==", "dev": true, "license": "MIT", "engines": { @@ -96,151 +96,151 @@ } }, "node_modules/@algolia/client-insights": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", - "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.0.tgz", + "integrity": "sha512-Rki7ACbMcvbQW0BuM84x9dkGHY47ABmv4jU6tYssat2k02p3mIUms2YOLUAMeknhmnFsj6lb6ZzOXdMWMyc1sA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", - "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.0.tgz", + "integrity": "sha512-96s4Uzc3kk+/f4jJXIVVGWP5XlngOGNQ1x6hW9AT59pOixHlOs5tqJg+ZUS/GQ6h/iYP0ceQcmxDQeLyCLTaDQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", - "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.0.tgz", + "integrity": "sha512-lqeycNpSPe5Qa0OUWpejVvYQjQWV5nQuLT0a4aq7XzRAvCxprV/6Lf841EygdD2nrFnuS58ok7Au1uOtXzpnkg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", - "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.0.tgz", + "integrity": "sha512-ly1wETVGRo30cx61O7fetESN+ElL9c9K+bD/AVgnT1ar4c6v+/Yqjrhdtu6Fm4D0s4NZP081Isf6tunH1wUXHg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", - "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.0.tgz", + "integrity": "sha512-U4EeTvgmluRjj39ykZSAd5X+a6LD5m7/mcOWDmB7hqm1R6QY0yT8jLxpNVEjYhzgEN5hcDGW6X67EWQY8KiYGQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", - "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.0.tgz", + "integrity": "sha512-FCPnDcILfpTE94u7BVlV4DmnSV5wE3+j25EEF+3dYPrVzkVCSoAHs318oWDGxnxsAgiL4HpL12Jc4XHmw9shpA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", - "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.0.tgz", + "integrity": "sha512-br3DO7n4N8CXwTRbZS0MnB4WQ9YHfNjCwkCEzVR/wek/qNTDQKDb0nROmkFaNZ8ucUqUVKZi074dbwMwRDlK8Q==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", - "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.0.tgz", + "integrity": "sha512-b0T/Ca2c9KyEslKsVrGZvbe1UrrKKSdfXhBZ2pbpKahFUzJfziRZ0urbOm7V65O0tO/jwU+Lo/+bIiiyhzGt8w==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1" + "@algolia/client-common": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", - "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.0.tgz", + "integrity": "sha512-ozBT8J/mtD4H4IAojw8QPirlcL2gHrI1BGuZ4/ZXXO/rTE1yQ4VIPJj4mTTbwo4FbkS1MoJsD/DsrqLzhnc4/g==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1" + "@algolia/client-common": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", - "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.0.tgz", + "integrity": "sha512-gyyWcLD22tnabmoit4iukCXuoRc5HYJuUjPSEa8a0D/f/NlRafpWi52AlAaa4Uu/rsl7saHsJFTNjTptWbu2+A==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1" + "@algolia/client-common": "5.52.0" }, "engines": { "node": ">= 14.0.0" @@ -261,32 +261,32 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2102.15", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.15.tgz", - "integrity": "sha512-EZQXu6j7J7OUxmpxIO2mxd58NTlCb7HOAOfLLGdk7lRcwZeCxnjGRzb72tXlJgIEi3IoOYwcW0ft2cg7r/d6qA==", + "version": "0.2200.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2200.1.tgz", + "integrity": "sha512-Q3DfpgEIiHtG7uSUO8Tsm35rOeUbJfuxM9pi7cCyC8DvC/z1yNYm7/xEitlEYPzJmSLmks3eqlsaGnYhh0VLVg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.15", + "@angular-devkit/core": "22.0.1", "rxjs": "7.8.2" }, "bin": { "architect": "bin/cli.js" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular-devkit/core": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.15.tgz", - "integrity": "sha512-x7EwuQtMGHANVznHpwyEi/3lMo/kRoaxV2w7lXbRGjTezwv4SPaOBwhrIGCbAVFs5B1ziff4jZzors4owlCTgg==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.0.1.tgz", + "integrity": "sha512-77/WsCAbqGkumDfm/kkw2mFh/42DNF0hB02TvivlfiSC/KfK9DsHg7sKvTlNcuY14ZT/3iHhojLyNBc2HytuvQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.18.0", + "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", @@ -294,7 +294,7 @@ "source-map": "0.7.6" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -308,191 +308,1103 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.15.tgz", - "integrity": "sha512-kHHV694KUPuXlItjvnxUspn+KYjlTKu8KINX/kT1qlogzLYojpnRwcUtZyRPUz1f/M1d3TUHVFeBKx+h5HoNcA==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.0.1.tgz", + "integrity": "sha512-GWou5meX3vAvqQEkox7xYMT9tIrYBVl0StbP7rGH5yMrzngvi6eyikMiUYnmMvoEoBK9gFNnXaAKeeu2aWvb3Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.15", + "@angular-devkit/core": "22.0.1", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", - "ora": "9.3.0", + "ora": "9.4.0", "rxjs": "7.8.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular/build": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.15.tgz", - "integrity": "sha512-APJ5v0/hL38CKSqSQDos5Y8/O2LSiSSqP9FagdcMT5j9JAlENXdhVaA3IxPMXDWcxtzD5T1IlN8Z9aFxDXxHyg==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.0.1.tgz", + "integrity": "sha512-05oMhBuRy4qycmuhrBpz3y/OxaW0qeguKj7ArUdTFOJvi6Y1kthzcg6bF1cPPVz0TMGnoTwMf9OCHjoT2QHAKA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.15", + "@angular-devkit/architect": "0.2200.1", "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", - "@inquirer/confirm": "5.1.21", - "@vitejs/plugin-basic-ssl": "2.1.4", - "beasties": "0.4.1", + "@inquirer/confirm": "6.0.12", + "@vitejs/plugin-basic-ssl": "2.3.0", + "beasties": "0.4.2", "browserslist": "^4.26.0", - "esbuild": "0.27.3", - "https-proxy-agent": "7.0.6", - "istanbul-lib-instrument": "6.0.3", + "esbuild": "0.28.0", + "https-proxy-agent": "9.0.0", "jsonc-parser": "3.3.1", - "listr2": "9.0.5", + "listr2": "10.2.1", "magic-string": "0.30.21", "mrmime": "2.0.1", - "parse5-html-rewriting-stream": "8.0.0", + "parse5-html-rewriting-stream": "8.0.1", "picomatch": "4.0.4", "piscina": "5.1.4", - "rolldown": "1.0.0-rc.4", - "sass": "1.97.3", + "rollup": "4.60.2", + "sass": "1.99.0", "semver": "7.7.4", "source-map-support": "0.5.21", - "tinyglobby": "0.2.15", - "undici": "7.24.4", + "tinyglobby": "0.2.16", "vite": "7.3.2", "watchpack": "2.5.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "optionalDependencies": { - "lmdb": "3.5.1" + "lmdb": "3.5.4" }, "peerDependencies": { - "@angular/compiler": "^21.0.0", - "@angular/compiler-cli": "^21.0.0", - "@angular/core": "^21.0.0", - "@angular/localize": "^21.0.0", - "@angular/platform-browser": "^21.0.0", - "@angular/platform-server": "^21.0.0", - "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.15", + "@angular/compiler": "^22.0.0", + "@angular/compiler-cli": "^22.0.0", + "@angular/core": "^22.0.0", + "@angular/localize": "^22.0.0", + "@angular/platform-browser": "^22.0.0", + "@angular/platform-server": "^22.0.0", + "@angular/service-worker": "^22.0.0", + "@angular/ssr": "^22.0.1", + "istanbul-lib-instrument": "^6.0.0", "karma": "^6.4.0", "less": "^4.2.0", - "ng-packagr": "^21.0.0", + "ng-packagr": "^22.0.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", - "typescript": ">=5.9 <6.0", + "typescript": ">=6.0 <6.1", "vitest": "^4.0.8" }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - }, - "@angular/localize": { - "optional": true - }, - "@angular/platform-browser": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@angular/ssr": { - "optional": true - }, - "karma": { - "optional": true - }, - "less": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tailwindcss": { - "optional": true - }, - "vitest": { - "optional": true - } + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "istanbul-lib-instrument": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/build/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@angular/build/node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/@angular/build/node_modules/https-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", + "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@angular/build/node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" } }, "node_modules/@angular/cli": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.15.tgz", - "integrity": "sha512-8CT1iST5CwdxHEzC0NVFUsMenAY7aE30ayTAw5PpA6MPs6bdHlL6QxTmkqThW7hoCj6PJ56YfQMzSQU+aYRLQA==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.0.1.tgz", + "integrity": "sha512-E1b3yroIDkqKpRJ5M/ihQkmgrF+gTlrntLbLWkSE5XReMSGtkog16I3hewI1zV2K4TMdiDZ1lzJvkJ4CgG3wjA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2102.15", - "@angular-devkit/core": "21.2.15", - "@angular-devkit/schematics": "21.2.15", - "@inquirer/prompts": "7.10.1", - "@listr2/prompt-adapter-inquirer": "3.0.5", - "@modelcontextprotocol/sdk": "1.26.0", - "@schematics/angular": "21.2.15", + "@angular-devkit/architect": "0.2200.1", + "@angular-devkit/core": "22.0.1", + "@angular-devkit/schematics": "22.0.1", + "@inquirer/prompts": "8.4.2", + "@listr2/prompt-adapter-inquirer": "4.2.3", + "@modelcontextprotocol/sdk": "1.29.0", + "@schematics/angular": "22.0.1", "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.48.1", + "algoliasearch": "5.52.0", "ini": "6.0.0", "jsonc-parser": "3.3.1", - "listr2": "9.0.5", + "listr2": "10.2.1", "npm-package-arg": "13.0.2", - "pacote": "21.3.1", - "parse5-html-rewriting-stream": "8.0.0", + "pacote": "21.5.0", + "parse5-html-rewriting-stream": "8.0.1", "semver": "7.7.4", "yargs": "18.0.0", - "zod": "4.3.6" + "zod": "4.4.2" }, "bin": { "ng": "bin/ng.js" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular/common": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.17.tgz", - "integrity": "sha512-hqAQxRfi5ldFE42suAXRcY+JCANrUh7fuSQ/DtZ7L896id5BT/exuv6dWNBC1PyAfQmRbpD5Pt6/pd+tNLyhDQ==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.0.1.tgz", + "integrity": "sha512-EczDHu+ziop3x2CSh9+hdkegTSjblYvuH0y4aZ8biqh+pGFRni24Qu9kZiV5VrBprIJG5NeiCEA2rT6fJ7w/Nw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/core": "21.2.17", + "@angular/core": "22.0.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.17.tgz", - "integrity": "sha512-p+NdjYiwAz9Zmu2yul0LlMXaFjMISVVa24+/MVMoKFeQeI82QE8jDywPlnOSHQHvdCcQVpS7saeEriZzX3JuBQ==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.0.1.tgz", + "integrity": "sha512-J8lcmYXJCGZn1+CHx3LSgq6rtS5Efc1/Nafcd/v8VZN2NrsFIkIlTbnAIUxcwHwYxzZs4X5CGfX+5ZIB4X9rUw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" } }, "node_modules/@angular/compiler-cli": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.17.tgz", - "integrity": "sha512-KithZ3b0HBFH0NbUcswBcjpN9y09vLbarMD7qmGWTnGUBk4W8cn4sbT8zJyv9CRKg9ZcuUBeJYKUfUPn/u/5OQ==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.0.1.tgz", + "integrity": "sha512-fo/tXV3sqw92/qz898ejru6G4wMvstd9gBjb0HRDJv5rR1XnSWHVKJuhrgrHd6tVkNdc8y6odBB9GKecdqqnGg==", "dev": true, "license": "MIT", "dependencies": { @@ -510,11 +1422,11 @@ "ngc": "bundles/src/bin/ngc.js" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.17", - "typescript": ">=5.9 <6.1" + "@angular/compiler": "22.0.1", + "typescript": ">=6.0 <6.1" }, "peerDependenciesMeta": { "typescript": { @@ -523,18 +1435,18 @@ } }, "node_modules/@angular/core": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.17.tgz", - "integrity": "sha512-wYHpwIdnUnjQFOJJNqRcGx7LS3u64jT+R9L0TnMR/ViBM9dQgGYImlSikkftg2yrFCNo5aKRxhG2LLskQurVdg==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.0.1.tgz", + "integrity": "sha512-Sk0fz+LR2q6QhJJtCV9ElN1GzoEX6lOB4difMqpOC0yRh/ue+9iKd+x3RRiL4p+dnAiRLQYAKqsXkYlqUm2SMg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.17", + "@angular/compiler": "22.0.1", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -548,39 +1460,40 @@ } }, "node_modules/@angular/forms": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.17.tgz", - "integrity": "sha512-WKu8XeRSNZo+a+aDDZ3M5OtReF7KYqR/PmZ2l1lSf6N5EEAmc+Ky4aqbRhTL/mTSfHrO4+TDJ4C5A2tFmuwIeA==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.0.1.tgz", + "integrity": "sha512-9an74j0DtSeY4XMp5crkAdlxmhdRNq6cKlN9dxGch6udEr2SjHDP7hYoNXJ6y5yna5FEa/t+1SltWF0OVPIVxw==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "zod": "^4.0.10" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "21.2.17", - "@angular/core": "21.2.17", - "@angular/platform-browser": "21.2.17", + "@angular/common": "22.0.1", + "@angular/core": "22.0.1", + "@angular/platform-browser": "22.0.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.17.tgz", - "integrity": "sha512-ROdSliejY37g1EphYmweYdm5cHM8HY3X4tbWt4ubxmhTyYgfN3nxrxfGQ/n7Mz5tDY9VXVLIGDgjLOGYOo4uTQ==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.1.tgz", + "integrity": "sha512-wbj/ddrMIOHKrONcFlDmHfJKUZq4dX8pzcxsLFTQ6sppUKtzWMrkxtCVkSPJLEzs6OG3OupRrc1yHbL/V+ffsw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/animations": "21.2.17", - "@angular/common": "21.2.17", - "@angular/core": "21.2.17" + "@angular/animations": "22.0.1", + "@angular/common": "22.0.1", + "@angular/core": "22.0.1" }, "peerDependenciesMeta": { "@angular/animations": { @@ -589,20 +1502,20 @@ } }, "node_modules/@angular/router": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.17.tgz", - "integrity": "sha512-RSCtK5ppAV6y6wfRLHSK2a9Wc/vm8j0wsC+/j9PH9yQmppWFVXDWsg5E39MKOIpnoYVx2+hI6eak6+wYtZTe1A==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.0.1.tgz", + "integrity": "sha512-NHMkRAgBMKrE7g5lSe1H/espe0NX8axVBy6TtzEh/l7yxTnZiqUIjmRnIDb+LFGPRQVgDu3FxR/fvwQaosOn0w==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "21.2.17", - "@angular/core": "21.2.17", - "@angular/platform-browser": "21.2.17", + "@angular/common": "22.0.1", + "@angular/core": "22.0.1", + "@angular/platform-browser": "22.0.1", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1103,48 +2016,11 @@ { "type": "opencollective", "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, + } + ], "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" + "engines": { + "node": ">=20.19.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -1639,30 +2515,29 @@ } }, "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/checkbox": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", - "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1674,17 +2549,17 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1696,23 +2571,22 @@ } }, "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1724,18 +2598,18 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.23", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", - "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/external-editor": "^1.0.3", - "@inquirer/type": "^3.0.10" + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1747,18 +2621,17 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", - "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1770,17 +2643,17 @@ } }, "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", "dev": true, "license": "MIT", "dependencies": { "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" + "iconv-lite": "^0.7.2" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1792,27 +2665,27 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/input": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", - "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1824,17 +2697,17 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", - "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1846,18 +2719,18 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", - "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1869,25 +2742,25 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", - "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.2.tgz", + "integrity": "sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.3.2", - "@inquirer/confirm": "^5.1.21", - "@inquirer/editor": "^4.2.23", - "@inquirer/expand": "^4.0.23", - "@inquirer/input": "^4.3.1", - "@inquirer/number": "^3.0.23", - "@inquirer/password": "^4.0.23", - "@inquirer/rawlist": "^4.1.11", - "@inquirer/search": "^3.2.2", - "@inquirer/select": "^4.4.2" + "@inquirer/checkbox": "^5.1.4", + "@inquirer/confirm": "^6.0.12", + "@inquirer/editor": "^5.1.1", + "@inquirer/expand": "^5.0.13", + "@inquirer/input": "^5.0.12", + "@inquirer/number": "^4.0.12", + "@inquirer/password": "^5.0.12", + "@inquirer/rawlist": "^5.2.8", + "@inquirer/search": "^4.1.8", + "@inquirer/select": "^5.1.4" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1899,18 +2772,17 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", - "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1922,19 +2794,18 @@ } }, "node_modules/@inquirer/search": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", - "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1946,20 +2817,19 @@ } }, "node_modules/@inquirer/select": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", - "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1971,13 +2841,13 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -2001,16 +2871,6 @@ "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2062,26 +2922,26 @@ } }, "node_modules/@listr2/prompt-adapter-inquirer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", - "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-4.2.3.tgz", + "integrity": "sha512-Co9U3AJ3LW0J8XBHjVoNKA79dMAyFt8EZH3OaKTMcDTj8r+6kG3vSUPq/eGLHT7P0iK3uLaFfhdFYd3033P24g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/type": "^3.0.8" + "@inquirer/type": "^4.0.5" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.13.0" }, "peerDependencies": { - "@inquirer/prompts": ">= 3 < 8", - "listr2": "9.0.5" + "@inquirer/prompts": ">= 3 < 9", + "listr2": "10.2.1" } }, "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", - "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.4.tgz", + "integrity": "sha512-Kk4Kz3iyu1QiLsLZBS9Af1eSKUC8VR2T+/jyE2iAyuGw2VwK08pp5iTbZnXn6sWu0LogO/RFktMxOjiDA2sS3w==", "cpu": [ "arm64" ], @@ -2093,9 +2953,9 @@ ] }, "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", - "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.4.tgz", + "integrity": "sha512-BEe5Rp3trn26oxoXOVL5HVDoiYmjUDwr8NRPkBOdUdCSBEorKI+7JrZLRKAdxO+G6cGQLgseXk0gR7qIQa7aGw==", "cpu": [ "x64" ], @@ -2107,9 +2967,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", - "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.4.tgz", + "integrity": "sha512-SGbFR7816uBcTHc2ZY4S6WyOkl9bICnzqTQd2Mv4V/j24cfds88xx2nC6cm/y8zGQL7Ds31YF/5NGxjgcdM5Hw==", "cpu": [ "arm" ], @@ -2121,9 +2981,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", - "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.4.tgz", + "integrity": "sha512-cUXEengO8o60v1SWerJTH4/RH4U3+9jC0/4njp2Z9NdmvaGzhKsbRM2wpXuRYrN8tytsoJCg0SvWEWwHAwLbCA==", "cpu": [ "arm64" ], @@ -2135,9 +2995,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", - "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.4.tgz", + "integrity": "sha512-Gxq8jpgOWXwd0PUR+c9R2Ik1/uBnGd5GMIIzRRDqABCkvmjtC3KWcyhesV9jSPCz759isl0NlbsstZ2oyvk8lA==", "cpu": [ "x64" ], @@ -2149,9 +3009,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-arm64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", - "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.4.tgz", + "integrity": "sha512-pKv1DJ1bPZAaHkdFsSz5IDfUG8x9vntgquXF9/Dm2xuupcIe/EkLzylpoBxppFVK5vzbV561Dq26jNY2fIMA7g==", "cpu": [ "arm64" ], @@ -2163,9 +3023,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", - "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.4.tgz", + "integrity": "sha512-JF1BmLCm9kGEVZgYmJq43zeQVdHVgAJnTi/NURWEsy6L1ZrrlSmdltS+D17QN4LODwf+1LMXAA9auIZVXtWwzw==", "cpu": [ "x64" ], @@ -2177,9 +3037,9 @@ ] }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2624,25 +3484,6 @@ "node": ">= 10" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@ngrx/effects": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-21.1.1.tgz", @@ -2890,16 +3731,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@oxc-project/types": { - "version": "0.113.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", - "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -3140,241 +3971,17 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", - "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", - "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", - "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", - "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", - "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", - "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", - "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", - "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", - "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", - "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -3382,35 +3989,22 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", - "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "node": ">= 10.0.0" }, - "engines": { - "node": ">=14.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", - "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", @@ -3419,13 +4013,17 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", - "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -3436,15 +4034,20 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", - "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.0", @@ -3797,18 +4400,19 @@ ] }, "node_modules/@schematics/angular": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.15.tgz", - "integrity": "sha512-xdTirIUUegMXtyEDsYabCRcAnlVatVYb6S8v77fgK2eqUhGalI2e/3+L51N3XF1+6K2vEhyDpmmhFZLsdhLYbw==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.0.1.tgz", + "integrity": "sha512-JRtJ9x0CaYIBLdPERr7B66ZSSLy4phkb7KtFIcD8RC2nAmnL/elevL2wg2Miih7ww0zmhiblS3LDE/abqSLRAA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.15", - "@angular-devkit/schematics": "21.2.15", - "jsonc-parser": "3.3.1" + "@angular-devkit/core": "22.0.1", + "@angular-devkit/schematics": "22.0.1", + "jsonc-parser": "3.3.1", + "typescript": "6.0.3" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } @@ -3923,17 +4527,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -3960,16 +4553,16 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", - "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", + "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==", "dev": true, "license": "MIT", "engines": { "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "peerDependencies": { - "vite": "^6.0.0 || ^7.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@vitest/expect": { @@ -4134,9 +4727,9 @@ } }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -4169,26 +4762,26 @@ } }, "node_modules/algoliasearch": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", - "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.0.tgz", + "integrity": "sha512-0ZzY9mjqV7gop/AH8pIBiAS8giXP7WcSiUfoFYIzYAK9QC5c37E4SIVtJVBMwlURc0/uNt2o4RcNRvdHa4CJ5w==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/abtesting": "1.14.1", - "@algolia/client-abtesting": "5.48.1", - "@algolia/client-analytics": "5.48.1", - "@algolia/client-common": "5.48.1", - "@algolia/client-insights": "5.48.1", - "@algolia/client-personalization": "5.48.1", - "@algolia/client-query-suggestions": "5.48.1", - "@algolia/client-search": "5.48.1", - "@algolia/ingestion": "1.48.1", - "@algolia/monitoring": "1.48.1", - "@algolia/recommend": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/abtesting": "1.18.0", + "@algolia/client-abtesting": "5.52.0", + "@algolia/client-analytics": "5.52.0", + "@algolia/client-common": "5.52.0", + "@algolia/client-insights": "5.52.0", + "@algolia/client-personalization": "5.52.0", + "@algolia/client-query-suggestions": "5.52.0", + "@algolia/client-search": "5.52.0", + "@algolia/ingestion": "1.52.0", + "@algolia/monitoring": "1.52.0", + "@algolia/recommend": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" @@ -4270,9 +4863,9 @@ } }, "node_modules/beasties": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", - "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.2.tgz", + "integrity": "sha512-NvcGjG/7AVUAfRbvrJmHunDQS9uHnE6Q/7AkaPr8oKE8HjOlpjRG5075z/th2Tmlezk3VlaaS8+X9I1RwHJMQw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4643,33 +5236,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -5025,13 +5591,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5268,6 +5827,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", @@ -5285,6 +5861,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5823,33 +6409,6 @@ "dev": true, "license": "ISC" }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jose": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", @@ -5976,63 +6535,26 @@ "license": "MIT" }, "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "wrap-ansi": "^10.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=22.13.0" } }, "node_modules/lmdb": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", - "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.4.tgz", + "integrity": "sha512-9FKQA6G1MMtqNxfxvSBNXD/axeG2QRjYbNh0/ykRL5xYcRbCm2vXq7B9bhc7nSuKdHzr8/BHIwfPuYYH1UsXXw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6049,13 +6571,13 @@ "download-lmdb-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.5.1", - "@lmdb/lmdb-darwin-x64": "3.5.1", - "@lmdb/lmdb-linux-arm": "3.5.1", - "@lmdb/lmdb-linux-arm64": "3.5.1", - "@lmdb/lmdb-linux-x64": "3.5.1", - "@lmdb/lmdb-win32-arm64": "3.5.1", - "@lmdb/lmdb-win32-x64": "3.5.1" + "@lmdb/lmdb-darwin-arm64": "3.5.4", + "@lmdb/lmdb-darwin-x64": "3.5.4", + "@lmdb/lmdb-linux-arm": "3.5.4", + "@lmdb/lmdb-linux-arm64": "3.5.4", + "@lmdb/lmdb-linux-x64": "3.5.4", + "@lmdb/lmdb-win32-arm64": "3.5.4", + "@lmdb/lmdb-win32-x64": "3.5.4" } }, "node_modules/log-symbols": { @@ -6473,13 +6995,13 @@ } }, "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nanoid": { @@ -6814,9 +7336,9 @@ } }, "node_modules/ora": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", - "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", + "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6826,7 +7348,7 @@ "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.1", + "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" }, "engines": { @@ -6858,12 +7380,13 @@ } }, "node_modules/pacote": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", - "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", + "version": "21.5.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.0.tgz", + "integrity": "sha512-VtZ0SB8mb5Tzw3dXDfVAIjhyVKUHZkS/ZH9/5mpKenwC9sFOXNI0JI7kEF7IMkwOnsWMFrvAZHzx1T5fmrp9FQ==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", @@ -6877,7 +7400,6 @@ "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" @@ -6903,13 +7425,13 @@ } }, "node_modules/parse5-html-rewriting-stream": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", - "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.1.tgz", + "integrity": "sha512-NaRku2aMpUN1Sh1Gyk1KWUh2A7EJx2c6qYzvwsPtqhoHoaURshdrceYK3LunVCm3WHhm6FS7Vcczbvdh3/UIVw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^6.0.0", + "entities": "^8.0.0", "parse5": "^8.0.0", "parse5-sax-parser": "^8.0.0" }, @@ -6918,13 +7440,13 @@ } }, "node_modules/parse5-html-rewriting-stream/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -7153,20 +7675,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7281,16 +7789,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -7298,38 +7796,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rolldown": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", - "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.113.0", - "@rolldown/pluginutils": "1.0.0-rc.4" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.4", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", - "@rolldown/binding-darwin-x64": "1.0.0-rc.4", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" - } - }, "node_modules/rollup": { "version": "4.62.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", @@ -7409,14 +7875,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.97.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", - "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -7935,14 +8401,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -8072,9 +8538,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8425,89 +8891,21 @@ } }, "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrappy": { @@ -8610,24 +9008,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", + "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/material/ngrx-global-store/demo/package.json b/material/ngrx-global-store/demo/package.json index 10b8ba66..4310d2ca 100644 --- a/material/ngrx-global-store/demo/package.json +++ b/material/ngrx-global-store/demo/package.json @@ -11,12 +11,12 @@ "private": true, "packageManager": "npm@10.9.4", "dependencies": { - "@angular/common": "^21.2.0", - "@angular/compiler": "^21.2.0", - "@angular/core": "^21.2.0", - "@angular/forms": "^21.2.0", - "@angular/platform-browser": "^21.2.0", - "@angular/router": "^21.2.0", + "@angular/common": "^22.0.1", + "@angular/compiler": "^22.0.1", + "@angular/core": "^22.0.1", + "@angular/forms": "^22.0.1", + "@angular/platform-browser": "^22.0.1", + "@angular/router": "^22.0.1", "@ngrx/effects": "^21.1.1", "@ngrx/operators": "^21.1.1", "@ngrx/store": "^21.1.1", @@ -24,12 +24,12 @@ "tslib": "^2.3.0" }, "devDependencies": { - "@angular/build": "^21.2.7", - "@angular/cli": "^21.2.7", - "@angular/compiler-cli": "^21.2.0", + "@angular/build": "^22.0.1", + "@angular/cli": "^22.0.1", + "@angular/compiler-cli": "^22.0.1", "jsdom": "^28.0.0", "prettier": "^3.8.1", - "typescript": "~5.9.2", + "typescript": "~6.0.3", "vitest": "^4.0.8" } } diff --git a/material/ngrx-global-store/demo/tsconfig.app.json b/material/ngrx-global-store/demo/tsconfig.app.json index 264f459b..8356da16 100644 --- a/material/ngrx-global-store/demo/tsconfig.app.json +++ b/material/ngrx-global-store/demo/tsconfig.app.json @@ -6,10 +6,14 @@ "outDir": "./out-tsc/app", "types": [] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"], + "angularCompilerOptions": { + "extendedDiagnostics": { + "checks": { + "nullishCoalescingNotNullable": "suppress", + "optionalChainNotNullable": "suppress" + } + } + } } diff --git a/material/ngrx-signal-store/demo/.npmrc b/material/ngrx-signal-store/demo/.npmrc new file mode 100644 index 00000000..a5518629 --- /dev/null +++ b/material/ngrx-signal-store/demo/.npmrc @@ -0,0 +1,5 @@ +# NgRx 21 deklariert als Peer-Range noch "@angular/core: ^21.0.0". +# Die Bibliothek laeuft aber problemlos unter Angular 22 (PR ngrx/platform#5155). +# Damit ein einfaches "npm install" nicht an dieser Peer-Range scheitert, +# ignorieren wir die Peer-Dependency-Pruefung (entspricht "npm install --legacy-peer-deps"). +legacy-peer-deps=true diff --git a/material/ngrx-signal-store/demo/README.md b/material/ngrx-signal-store/demo/README.md index 63bf475a..d12bc878 100644 --- a/material/ngrx-signal-store/demo/README.md +++ b/material/ngrx-signal-store/demo/README.md @@ -4,9 +4,11 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern und löschen (CRUD) mit `signalStore`, `withState`, `withComputed`, `withMethods`, `rxMethod` und `patchState`, inklusive Fehler-State. -- **Angular 21** + **NgRx 21** (`@ngrx/signals`, `@ngrx/operators`) +- **Angular 22** + **NgRx 21** (`@ngrx/signals`, `@ngrx/operators`) - Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. +> **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. + ## Befehle ```bash diff --git a/material/ngrx-signal-store/demo/package-lock.json b/material/ngrx-signal-store/demo/package-lock.json index 28c73da2..11d4547e 100644 --- a/material/ngrx-signal-store/demo/package-lock.json +++ b/material/ngrx-signal-store/demo/package-lock.json @@ -8,24 +8,24 @@ "name": "demo", "version": "0.0.0", "dependencies": { - "@angular/common": "^21.2.0", - "@angular/compiler": "^21.2.0", - "@angular/core": "^21.2.0", - "@angular/forms": "^21.2.0", - "@angular/platform-browser": "^21.2.0", - "@angular/router": "^21.2.0", + "@angular/common": "^22.0.1", + "@angular/compiler": "^22.0.1", + "@angular/core": "^22.0.1", + "@angular/forms": "^22.0.1", + "@angular/platform-browser": "^22.0.1", + "@angular/router": "^22.0.1", "@ngrx/operators": "^21.1.1", "@ngrx/signals": "^21.1.1", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, "devDependencies": { - "@angular/build": "^21.2.7", - "@angular/cli": "^21.2.7", - "@angular/compiler-cli": "^21.2.0", + "@angular/build": "^22.0.1", + "@angular/cli": "^22.0.1", + "@angular/compiler-cli": "^22.0.1", "jsdom": "^28.0.0", "prettier": "^3.8.1", - "typescript": "~5.9.2", + "typescript": "~6.0.3", "vitest": "^4.0.8" } }, @@ -37,57 +37,57 @@ "license": "MIT" }, "node_modules/@algolia/abtesting": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.14.1.tgz", - "integrity": "sha512-Dkj0BgPiLAaim9sbQ97UKDFHJE/880wgStAM18U++NaJ/2Cws34J5731ovJifr6E3Pv4T2CqvMXf8qLCC417Ew==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.0.tgz", + "integrity": "sha512-8siuLG+FIns1AjZ/g2SDVwHz9S+ObacDQISEJvS8XsNei1zl3FXqfqQrBpmrG7ACWCyesXHbicMJtvRbg00FEw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-abtesting": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.48.1.tgz", - "integrity": "sha512-LV5qCJdj+/m9I+Aj91o+glYszrzd7CX6NgKaYdTOj4+tUYfbS62pwYgUfZprYNayhkQpVFcrW8x8ZlIHpS23Vw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.0.tgz", + "integrity": "sha512-wtwPgyPmO7b7sQPVgoK29c1VpfS08DnnJCmxX/oU1pV2DlMRJCzQcLN7JSloYpodyKHwM8+9wOzlAM0co3TDmA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.48.1.tgz", - "integrity": "sha512-/AVoMqHhPm14CcHq7mwB+bUJbfCv+jrxlNvRjXAuO+TQa+V37N8k1b0ijaRBPdmSjULMd8KtJbQyUyabXOu6Kg==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.0.tgz", + "integrity": "sha512-9KY36bRl4AH7RjqSeDDOKnjsz4IxQFBEOB8/fWmEbdQe+Isbs5jGzVJu9NEPQ1Tgwxlf8Uf07Swj3jZyMNUZ2g==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.48.1.tgz", - "integrity": "sha512-VXO+qu2Ep6ota28ktvBm3sG53wUHS2n7bgLWmce5jTskdlCD0/JrV4tnBm1l7qpla1CeoQb8D7ShFhad+UoSOw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.0.tgz", + "integrity": "sha512-3a/qM3dzJqqfTx7Yrw7uGQ98I3Q0rDfb4Vkv0wEzko96l7YQMxfBVz/VbLq2N+c59GweYv6Vhp8mPeqnWJSITw==", "dev": true, "license": "MIT", "engines": { @@ -95,151 +95,151 @@ } }, "node_modules/@algolia/client-insights": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.48.1.tgz", - "integrity": "sha512-zl+Qyb0nLg+Y5YvKp1Ij+u9OaPaKg2/EPzTwKNiVyOHnQJlFxmXyUZL1EInczAZsEY8hVpPCLtNfhMhfxluXKQ==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.0.tgz", + "integrity": "sha512-Rki7ACbMcvbQW0BuM84x9dkGHY47ABmv4jU6tYssat2k02p3mIUms2YOLUAMeknhmnFsj6lb6ZzOXdMWMyc1sA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.48.1.tgz", - "integrity": "sha512-r89Qf9Oo9mKWQXumRu/1LtvVJAmEDpn8mHZMc485pRfQUMAwSSrsnaw1tQ3sszqzEgAr1c7rw6fjBI+zrAXTOw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.0.tgz", + "integrity": "sha512-96s4Uzc3kk+/f4jJXIVVGWP5XlngOGNQ1x6hW9AT59pOixHlOs5tqJg+ZUS/GQ6h/iYP0ceQcmxDQeLyCLTaDQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.48.1.tgz", - "integrity": "sha512-TPKNPKfghKG/bMSc7mQYD9HxHRUkBZA4q1PEmHgICaSeHQscGqL4wBrKkhfPlDV1uYBKW02pbFMUhsOt7p4ZpA==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.0.tgz", + "integrity": "sha512-lqeycNpSPe5Qa0OUWpejVvYQjQWV5nQuLT0a4aq7XzRAvCxprV/6Lf841EygdD2nrFnuS58ok7Au1uOtXzpnkg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.48.1.tgz", - "integrity": "sha512-4Fu7dnzQyQmMFknYwTiN/HxPbH4DyxvQ1m+IxpPp5oslOgz8m6PG5qhiGbqJzH4HiT1I58ecDiCAC716UyVA8Q==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.0.tgz", + "integrity": "sha512-ly1wETVGRo30cx61O7fetESN+ElL9c9K+bD/AVgnT1ar4c6v+/Yqjrhdtu6Fm4D0s4NZP081Isf6tunH1wUXHg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.48.1.tgz", - "integrity": "sha512-/RFq3TqtXDUUawwic/A9xylA2P3LDMO8dNhphHAUOU51b1ZLHrmZ6YYJm3df1APz7xLY1aht6okCQf+/vmrV9w==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.0.tgz", + "integrity": "sha512-U4EeTvgmluRjj39ykZSAd5X+a6LD5m7/mcOWDmB7hqm1R6QY0yT8jLxpNVEjYhzgEN5hcDGW6X67EWQY8KiYGQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.48.1", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.48.1.tgz", - "integrity": "sha512-Of0jTeAZRyRhC7XzDSjJef0aBkgRcvRAaw0ooYRlOw57APii7lZdq+layuNdeL72BRq1snaJhoMMwkmLIpJScw==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.0.tgz", + "integrity": "sha512-FCPnDcILfpTE94u7BVlV4DmnSV5wE3+j25EEF+3dYPrVzkVCSoAHs318oWDGxnxsAgiL4HpL12Jc4XHmw9shpA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.48.1.tgz", - "integrity": "sha512-bE7JcpFXzxF5zHwj/vkl2eiCBvyR1zQ7aoUdO+GDXxGp0DGw7nI0p8Xj6u8VmRQ+RDuPcICFQcCwRIJT5tDJFw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.0.tgz", + "integrity": "sha512-br3DO7n4N8CXwTRbZS0MnB4WQ9YHfNjCwkCEzVR/wek/qNTDQKDb0nROmkFaNZ8ucUqUVKZi074dbwMwRDlK8Q==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/client-common": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.48.1.tgz", - "integrity": "sha512-MK3wZ2koLDnvH/AmqIF1EKbJlhRS5j74OZGkLpxI4rYvNi9Jn/C7vb5DytBnQ4KUWts7QsmbdwHkxY5txQHXVw==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.0.tgz", + "integrity": "sha512-b0T/Ca2c9KyEslKsVrGZvbe1UrrKKSdfXhBZ2pbpKahFUzJfziRZ0urbOm7V65O0tO/jwU+Lo/+bIiiyhzGt8w==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1" + "@algolia/client-common": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.48.1.tgz", - "integrity": "sha512-2oDT43Y5HWRSIQMPQI4tA/W+TN/N2tjggZCUsqQV440kxzzoPGsvv9QP1GhQ4CoDa+yn6ygUsGp6Dr+a9sPPSg==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.0.tgz", + "integrity": "sha512-ozBT8J/mtD4H4IAojw8QPirlcL2gHrI1BGuZ4/ZXXO/rTE1yQ4VIPJj4mTTbwo4FbkS1MoJsD/DsrqLzhnc4/g==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1" + "@algolia/client-common": "5.52.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.48.1.tgz", - "integrity": "sha512-xcaCqbhupVWhuBP1nwbk1XNvwrGljozutEiLx06mvqDf3o8cHyEgQSHS4fKJM+UAggaWVnnFW+Nne5aQ8SUJXg==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.0.tgz", + "integrity": "sha512-gyyWcLD22tnabmoit4iukCXuoRc5HYJuUjPSEa8a0D/f/NlRafpWi52AlAaa4Uu/rsl7saHsJFTNjTptWbu2+A==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.48.1" + "@algolia/client-common": "5.52.0" }, "engines": { "node": ">= 14.0.0" @@ -260,32 +260,32 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2102.15", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.15.tgz", - "integrity": "sha512-EZQXu6j7J7OUxmpxIO2mxd58NTlCb7HOAOfLLGdk7lRcwZeCxnjGRzb72tXlJgIEi3IoOYwcW0ft2cg7r/d6qA==", + "version": "0.2200.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2200.1.tgz", + "integrity": "sha512-Q3DfpgEIiHtG7uSUO8Tsm35rOeUbJfuxM9pi7cCyC8DvC/z1yNYm7/xEitlEYPzJmSLmks3eqlsaGnYhh0VLVg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.15", + "@angular-devkit/core": "22.0.1", "rxjs": "7.8.2" }, "bin": { "architect": "bin/cli.js" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular-devkit/core": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.15.tgz", - "integrity": "sha512-x7EwuQtMGHANVznHpwyEi/3lMo/kRoaxV2w7lXbRGjTezwv4SPaOBwhrIGCbAVFs5B1ziff4jZzors4owlCTgg==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.0.1.tgz", + "integrity": "sha512-77/WsCAbqGkumDfm/kkw2mFh/42DNF0hB02TvivlfiSC/KfK9DsHg7sKvTlNcuY14ZT/3iHhojLyNBc2HytuvQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.18.0", + "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", @@ -293,7 +293,7 @@ "source-map": "0.7.6" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -307,191 +307,1103 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.15.tgz", - "integrity": "sha512-kHHV694KUPuXlItjvnxUspn+KYjlTKu8KINX/kT1qlogzLYojpnRwcUtZyRPUz1f/M1d3TUHVFeBKx+h5HoNcA==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.0.1.tgz", + "integrity": "sha512-GWou5meX3vAvqQEkox7xYMT9tIrYBVl0StbP7rGH5yMrzngvi6eyikMiUYnmMvoEoBK9gFNnXaAKeeu2aWvb3Q==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.15", + "@angular-devkit/core": "22.0.1", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", - "ora": "9.3.0", + "ora": "9.4.0", "rxjs": "7.8.2" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular/build": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.15.tgz", - "integrity": "sha512-APJ5v0/hL38CKSqSQDos5Y8/O2LSiSSqP9FagdcMT5j9JAlENXdhVaA3IxPMXDWcxtzD5T1IlN8Z9aFxDXxHyg==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.0.1.tgz", + "integrity": "sha512-05oMhBuRy4qycmuhrBpz3y/OxaW0qeguKj7ArUdTFOJvi6Y1kthzcg6bF1cPPVz0TMGnoTwMf9OCHjoT2QHAKA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.15", + "@angular-devkit/architect": "0.2200.1", "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", - "@inquirer/confirm": "5.1.21", - "@vitejs/plugin-basic-ssl": "2.1.4", - "beasties": "0.4.1", + "@inquirer/confirm": "6.0.12", + "@vitejs/plugin-basic-ssl": "2.3.0", + "beasties": "0.4.2", "browserslist": "^4.26.0", - "esbuild": "0.27.3", - "https-proxy-agent": "7.0.6", - "istanbul-lib-instrument": "6.0.3", + "esbuild": "0.28.0", + "https-proxy-agent": "9.0.0", "jsonc-parser": "3.3.1", - "listr2": "9.0.5", + "listr2": "10.2.1", "magic-string": "0.30.21", "mrmime": "2.0.1", - "parse5-html-rewriting-stream": "8.0.0", + "parse5-html-rewriting-stream": "8.0.1", "picomatch": "4.0.4", "piscina": "5.1.4", - "rolldown": "1.0.0-rc.4", - "sass": "1.97.3", + "rollup": "4.60.2", + "sass": "1.99.0", "semver": "7.7.4", "source-map-support": "0.5.21", - "tinyglobby": "0.2.15", - "undici": "7.24.4", + "tinyglobby": "0.2.16", "vite": "7.3.2", "watchpack": "2.5.1" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "optionalDependencies": { - "lmdb": "3.5.1" + "lmdb": "3.5.4" }, "peerDependencies": { - "@angular/compiler": "^21.0.0", - "@angular/compiler-cli": "^21.0.0", - "@angular/core": "^21.0.0", - "@angular/localize": "^21.0.0", - "@angular/platform-browser": "^21.0.0", - "@angular/platform-server": "^21.0.0", - "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.15", + "@angular/compiler": "^22.0.0", + "@angular/compiler-cli": "^22.0.0", + "@angular/core": "^22.0.0", + "@angular/localize": "^22.0.0", + "@angular/platform-browser": "^22.0.0", + "@angular/platform-server": "^22.0.0", + "@angular/service-worker": "^22.0.0", + "@angular/ssr": "^22.0.1", + "istanbul-lib-instrument": "^6.0.0", "karma": "^6.4.0", "less": "^4.2.0", - "ng-packagr": "^21.0.0", + "ng-packagr": "^22.0.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", - "typescript": ">=5.9 <6.0", + "typescript": ">=6.0 <6.1", "vitest": "^4.0.8" }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - }, - "@angular/localize": { - "optional": true - }, - "@angular/platform-browser": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@angular/ssr": { - "optional": true - }, - "karma": { - "optional": true - }, - "less": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tailwindcss": { - "optional": true - }, - "vitest": { - "optional": true - } + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "istanbul-lib-instrument": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@angular/build/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/build/node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@angular/build/node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "node_modules/@angular/build/node_modules/https-proxy-agent": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz", + "integrity": "sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@angular/build/node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" } }, "node_modules/@angular/cli": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.15.tgz", - "integrity": "sha512-8CT1iST5CwdxHEzC0NVFUsMenAY7aE30ayTAw5PpA6MPs6bdHlL6QxTmkqThW7hoCj6PJ56YfQMzSQU+aYRLQA==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.0.1.tgz", + "integrity": "sha512-E1b3yroIDkqKpRJ5M/ihQkmgrF+gTlrntLbLWkSE5XReMSGtkog16I3hewI1zV2K4TMdiDZ1lzJvkJ4CgG3wjA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2102.15", - "@angular-devkit/core": "21.2.15", - "@angular-devkit/schematics": "21.2.15", - "@inquirer/prompts": "7.10.1", - "@listr2/prompt-adapter-inquirer": "3.0.5", - "@modelcontextprotocol/sdk": "1.26.0", - "@schematics/angular": "21.2.15", + "@angular-devkit/architect": "0.2200.1", + "@angular-devkit/core": "22.0.1", + "@angular-devkit/schematics": "22.0.1", + "@inquirer/prompts": "8.4.2", + "@listr2/prompt-adapter-inquirer": "4.2.3", + "@modelcontextprotocol/sdk": "1.29.0", + "@schematics/angular": "22.0.1", "@yarnpkg/lockfile": "1.1.0", - "algoliasearch": "5.48.1", + "algoliasearch": "5.52.0", "ini": "6.0.0", "jsonc-parser": "3.3.1", - "listr2": "9.0.5", + "listr2": "10.2.1", "npm-package-arg": "13.0.2", - "pacote": "21.3.1", - "parse5-html-rewriting-stream": "8.0.0", + "pacote": "21.5.0", + "parse5-html-rewriting-stream": "8.0.1", "semver": "7.7.4", "yargs": "18.0.0", - "zod": "4.3.6" + "zod": "4.4.2" }, "bin": { "ng": "bin/ng.js" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "node_modules/@angular/common": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.17.tgz", - "integrity": "sha512-hqAQxRfi5ldFE42suAXRcY+JCANrUh7fuSQ/DtZ7L896id5BT/exuv6dWNBC1PyAfQmRbpD5Pt6/pd+tNLyhDQ==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.0.1.tgz", + "integrity": "sha512-EczDHu+ziop3x2CSh9+hdkegTSjblYvuH0y4aZ8biqh+pGFRni24Qu9kZiV5VrBprIJG5NeiCEA2rT6fJ7w/Nw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/core": "21.2.17", + "@angular/core": "22.0.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.17.tgz", - "integrity": "sha512-p+NdjYiwAz9Zmu2yul0LlMXaFjMISVVa24+/MVMoKFeQeI82QE8jDywPlnOSHQHvdCcQVpS7saeEriZzX3JuBQ==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.0.1.tgz", + "integrity": "sha512-J8lcmYXJCGZn1+CHx3LSgq6rtS5Efc1/Nafcd/v8VZN2NrsFIkIlTbnAIUxcwHwYxzZs4X5CGfX+5ZIB4X9rUw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" } }, "node_modules/@angular/compiler-cli": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.17.tgz", - "integrity": "sha512-KithZ3b0HBFH0NbUcswBcjpN9y09vLbarMD7qmGWTnGUBk4W8cn4sbT8zJyv9CRKg9ZcuUBeJYKUfUPn/u/5OQ==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.0.1.tgz", + "integrity": "sha512-fo/tXV3sqw92/qz898ejru6G4wMvstd9gBjb0HRDJv5rR1XnSWHVKJuhrgrHd6tVkNdc8y6odBB9GKecdqqnGg==", "dev": true, "license": "MIT", "dependencies": { @@ -509,11 +1421,11 @@ "ngc": "bundles/src/bin/ngc.js" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.17", - "typescript": ">=5.9 <6.1" + "@angular/compiler": "22.0.1", + "typescript": ">=6.0 <6.1" }, "peerDependenciesMeta": { "typescript": { @@ -522,18 +1434,18 @@ } }, "node_modules/@angular/core": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.17.tgz", - "integrity": "sha512-wYHpwIdnUnjQFOJJNqRcGx7LS3u64jT+R9L0TnMR/ViBM9dQgGYImlSikkftg2yrFCNo5aKRxhG2LLskQurVdg==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.0.1.tgz", + "integrity": "sha512-Sk0fz+LR2q6QhJJtCV9ElN1GzoEX6lOB4difMqpOC0yRh/ue+9iKd+x3RRiL4p+dnAiRLQYAKqsXkYlqUm2SMg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.17", + "@angular/compiler": "22.0.1", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -547,39 +1459,40 @@ } }, "node_modules/@angular/forms": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.17.tgz", - "integrity": "sha512-WKu8XeRSNZo+a+aDDZ3M5OtReF7KYqR/PmZ2l1lSf6N5EEAmc+Ky4aqbRhTL/mTSfHrO4+TDJ4C5A2tFmuwIeA==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.0.1.tgz", + "integrity": "sha512-9an74j0DtSeY4XMp5crkAdlxmhdRNq6cKlN9dxGch6udEr2SjHDP7hYoNXJ6y5yna5FEa/t+1SltWF0OVPIVxw==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "zod": "^4.0.10" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "21.2.17", - "@angular/core": "21.2.17", - "@angular/platform-browser": "21.2.17", + "@angular/common": "22.0.1", + "@angular/core": "22.0.1", + "@angular/platform-browser": "22.0.1", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.17.tgz", - "integrity": "sha512-ROdSliejY37g1EphYmweYdm5cHM8HY3X4tbWt4ubxmhTyYgfN3nxrxfGQ/n7Mz5tDY9VXVLIGDgjLOGYOo4uTQ==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.1.tgz", + "integrity": "sha512-wbj/ddrMIOHKrONcFlDmHfJKUZq4dX8pzcxsLFTQ6sppUKtzWMrkxtCVkSPJLEzs6OG3OupRrc1yHbL/V+ffsw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/animations": "21.2.17", - "@angular/common": "21.2.17", - "@angular/core": "21.2.17" + "@angular/animations": "22.0.1", + "@angular/common": "22.0.1", + "@angular/core": "22.0.1" }, "peerDependenciesMeta": { "@angular/animations": { @@ -588,20 +1501,20 @@ } }, "node_modules/@angular/router": { - "version": "21.2.17", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.17.tgz", - "integrity": "sha512-RSCtK5ppAV6y6wfRLHSK2a9Wc/vm8j0wsC+/j9PH9yQmppWFVXDWsg5E39MKOIpnoYVx2+hI6eak6+wYtZTe1A==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.0.1.tgz", + "integrity": "sha512-NHMkRAgBMKrE7g5lSe1H/espe0NX8axVBy6TtzEh/l7yxTnZiqUIjmRnIDb+LFGPRQVgDu3FxR/fvwQaosOn0w==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "21.2.17", - "@angular/core": "21.2.17", - "@angular/platform-browser": "21.2.17", + "@angular/common": "22.0.1", + "@angular/core": "22.0.1", + "@angular/platform-browser": "22.0.1", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1102,48 +2015,11 @@ { "type": "opencollective", "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, + } + ], "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" + "engines": { + "node": ">=20.19.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -1638,30 +2514,29 @@ } }, "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/checkbox": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", - "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1673,17 +2548,17 @@ } }, "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.12.tgz", + "integrity": "sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@inquirer/core": "^11.1.9", + "@inquirer/type": "^4.0.5" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1695,23 +2570,22 @@ } }, "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1723,18 +2597,18 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.23", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", - "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/external-editor": "^1.0.3", - "@inquirer/type": "^3.0.10" + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1746,18 +2620,17 @@ } }, "node_modules/@inquirer/expand": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", - "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1769,17 +2642,17 @@ } }, "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", "dev": true, "license": "MIT", "dependencies": { "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" + "iconv-lite": "^0.7.2" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1791,27 +2664,27 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" } }, "node_modules/@inquirer/input": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", - "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1823,17 +2696,17 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", - "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1845,18 +2718,18 @@ } }, "node_modules/@inquirer/password": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", - "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1868,25 +2741,25 @@ } }, "node_modules/@inquirer/prompts": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", - "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.2.tgz", + "integrity": "sha512-XJmn/wY4AX56l1BRU+ZjDrFtg9+2uBEi4JvJQj82kwJDQKiPgSn4CEsbfGGygS4Gw6rkL4W18oATjfVfaqub2Q==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.3.2", - "@inquirer/confirm": "^5.1.21", - "@inquirer/editor": "^4.2.23", - "@inquirer/expand": "^4.0.23", - "@inquirer/input": "^4.3.1", - "@inquirer/number": "^3.0.23", - "@inquirer/password": "^4.0.23", - "@inquirer/rawlist": "^4.1.11", - "@inquirer/search": "^3.2.2", - "@inquirer/select": "^4.4.2" + "@inquirer/checkbox": "^5.1.4", + "@inquirer/confirm": "^6.0.12", + "@inquirer/editor": "^5.1.1", + "@inquirer/expand": "^5.0.13", + "@inquirer/input": "^5.0.12", + "@inquirer/number": "^4.0.12", + "@inquirer/password": "^5.0.12", + "@inquirer/rawlist": "^5.2.8", + "@inquirer/search": "^4.1.8", + "@inquirer/select": "^5.1.4" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1898,18 +2771,17 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", - "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1921,19 +2793,18 @@ } }, "node_modules/@inquirer/search": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", - "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1945,20 +2816,19 @@ } }, "node_modules/@inquirer/select": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", - "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" }, "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -1970,13 +2840,13 @@ } }, "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" }, "peerDependencies": { "@types/node": ">=18" @@ -2000,16 +2870,6 @@ "node": ">=18.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2061,26 +2921,26 @@ } }, "node_modules/@listr2/prompt-adapter-inquirer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", - "integrity": "sha512-WELs+hj6xcilkloBXYf9XXK8tYEnKsgLj01Xl5ONUJpKjmT5hGVUzNUS5tooUxs7pGMrw+jFD/41WpqW4V3LDA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-4.2.3.tgz", + "integrity": "sha512-Co9U3AJ3LW0J8XBHjVoNKA79dMAyFt8EZH3OaKTMcDTj8r+6kG3vSUPq/eGLHT7P0iK3uLaFfhdFYd3033P24g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/type": "^3.0.8" + "@inquirer/type": "^4.0.5" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.13.0" }, "peerDependencies": { - "@inquirer/prompts": ">= 3 < 8", - "listr2": "9.0.5" + "@inquirer/prompts": ">= 3 < 9", + "listr2": "10.2.1" } }, "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.1.tgz", - "integrity": "sha512-tpfN4kKrrMpQ+If1l8bhmoNkECJi0iOu6AEdrTJvWVC+32sLxTARX5Rsu579mPImRP9YFWfWgeRQ5oav7zApQQ==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.4.tgz", + "integrity": "sha512-Kk4Kz3iyu1QiLsLZBS9Af1eSKUC8VR2T+/jyE2iAyuGw2VwK08pp5iTbZnXn6sWu0LogO/RFktMxOjiDA2sS3w==", "cpu": [ "arm64" ], @@ -2092,9 +2952,9 @@ ] }, "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.1.tgz", - "integrity": "sha512-+a2tTfc3rmWhLAolFUWRgJtpSuu+Fw/yjn4rF406NMxhfjbMuiOUTDRvRlMFV+DzyjkwnokisskHbCWkS3Ly5w==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.4.tgz", + "integrity": "sha512-BEe5Rp3trn26oxoXOVL5HVDoiYmjUDwr8NRPkBOdUdCSBEorKI+7JrZLRKAdxO+G6cGQLgseXk0gR7qIQa7aGw==", "cpu": [ "x64" ], @@ -2106,9 +2966,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.1.tgz", - "integrity": "sha512-0EgcE6reYr8InjD7V37EgXcYrloqpxVPINy3ig1MwDSbl6LF/vXTYRH9OE1Ti1D8YZnB35ZH9aTcdfSb5lql2A==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.4.tgz", + "integrity": "sha512-SGbFR7816uBcTHc2ZY4S6WyOkl9bICnzqTQd2Mv4V/j24cfds88xx2nC6cm/y8zGQL7Ds31YF/5NGxjgcdM5Hw==", "cpu": [ "arm" ], @@ -2120,9 +2980,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.1.tgz", - "integrity": "sha512-aoERa5B6ywXdyFeYGQ1gbQpkMkDbEo45qVoXE5QpIRavqjnyPwjOulMkmkypkmsbJ5z4Wi0TBztON8agCTG0Vg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.4.tgz", + "integrity": "sha512-cUXEengO8o60v1SWerJTH4/RH4U3+9jC0/4njp2Z9NdmvaGzhKsbRM2wpXuRYrN8tytsoJCg0SvWEWwHAwLbCA==", "cpu": [ "arm64" ], @@ -2134,9 +2994,9 @@ ] }, "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.1.tgz", - "integrity": "sha512-SqNDY1+vpji7bh0sFH5wlWyFTOzjbDOl0/kB5RLLYDAFyd/uw3n7wyrmas3rYPpAW7z18lMOi1yKlTPv967E3g==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.4.tgz", + "integrity": "sha512-Gxq8jpgOWXwd0PUR+c9R2Ik1/uBnGd5GMIIzRRDqABCkvmjtC3KWcyhesV9jSPCz759isl0NlbsstZ2oyvk8lA==", "cpu": [ "x64" ], @@ -2148,9 +3008,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-arm64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.1.tgz", - "integrity": "sha512-50v0O1Lt37cwrmR9vWZK5hRW0Aw+KEmxJJ75fge/zIYdvNKB/0bSMSVR5Uc2OV9JhosIUyklOmrEvavwNJ8D6w==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.4.tgz", + "integrity": "sha512-pKv1DJ1bPZAaHkdFsSz5IDfUG8x9vntgquXF9/Dm2xuupcIe/EkLzylpoBxppFVK5vzbV561Dq26jNY2fIMA7g==", "cpu": [ "arm64" ], @@ -2162,9 +3022,9 @@ ] }, "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.1.tgz", - "integrity": "sha512-qwosvPyl+zpUlp3gRb7UcJ3H8S28XHCzkv0Y0EgQToXjQP91ZD67EHSCDmaLjtKhe+GVIW5om1KUpzVLA0l6pg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.4.tgz", + "integrity": "sha512-JF1BmLCm9kGEVZgYmJq43zeQVdHVgAJnTi/NURWEsy6L1ZrrlSmdltS+D17QN4LODwf+1LMXAA9auIZVXtWwzw==", "cpu": [ "x64" ], @@ -2176,9 +3036,9 @@ ] }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2623,25 +3483,6 @@ "node": ">= 10" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@ngrx/operators": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-21.1.1.tgz", @@ -2880,16 +3721,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@oxc-project/types": { - "version": "0.113.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.113.0.tgz", - "integrity": "sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.6", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", @@ -3130,241 +3961,17 @@ "linux" ], "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher/node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz", - "integrity": "sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.4.tgz", - "integrity": "sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.4.tgz", - "integrity": "sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.4.tgz", - "integrity": "sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.4.tgz", - "integrity": "sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.4.tgz", - "integrity": "sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.4.tgz", - "integrity": "sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.4.tgz", - "integrity": "sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.4.tgz", - "integrity": "sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.4.tgz", - "integrity": "sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -3372,35 +3979,22 @@ "license": "MIT", "optional": true, "os": [ - "openharmony" + "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.4.tgz", - "integrity": "sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "node": ">= 10.0.0" }, - "engines": { - "node": ">=14.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.4.tgz", - "integrity": "sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", @@ -3409,13 +4003,17 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.4.tgz", - "integrity": "sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -3426,15 +4024,20 @@ "win32" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.4.tgz", - "integrity": "sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==", + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.0", @@ -3787,18 +4390,19 @@ ] }, "node_modules/@schematics/angular": { - "version": "21.2.15", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.15.tgz", - "integrity": "sha512-xdTirIUUegMXtyEDsYabCRcAnlVatVYb6S8v77fgK2eqUhGalI2e/3+L51N3XF1+6K2vEhyDpmmhFZLsdhLYbw==", + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.0.1.tgz", + "integrity": "sha512-JRtJ9x0CaYIBLdPERr7B66ZSSLy4phkb7KtFIcD8RC2nAmnL/elevL2wg2Miih7ww0zmhiblS3LDE/abqSLRAA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.15", - "@angular-devkit/schematics": "21.2.15", - "jsonc-parser": "3.3.1" + "@angular-devkit/core": "22.0.1", + "@angular-devkit/schematics": "22.0.1", + "jsonc-parser": "3.3.1", + "typescript": "6.0.3" }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } @@ -3913,17 +4517,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -3950,16 +4543,16 @@ "license": "MIT" }, "node_modules/@vitejs/plugin-basic-ssl": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.4.tgz", - "integrity": "sha512-HXciTXN/sDBYWgeAD4V4s0DN0g72x5mlxQhHxtYu3Tt8BLa6MzcJZUyDVFCdtjNs3bfENVHVzOsmooTVuNgAAw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", + "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==", "dev": true, "license": "MIT", "engines": { "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "peerDependencies": { - "vite": "^6.0.0 || ^7.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/@vitest/expect": { @@ -4124,9 +4717,9 @@ } }, "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -4159,26 +4752,26 @@ } }, "node_modules/algoliasearch": { - "version": "5.48.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.48.1.tgz", - "integrity": "sha512-Rf7xmeuIo7nb6S4mp4abW2faW8DauZyE2faBIKFaUfP3wnpOvNSbiI5AwVhqBNj0jPgBWEvhyCu0sLjN2q77Rg==", + "version": "5.52.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.0.tgz", + "integrity": "sha512-0ZzY9mjqV7gop/AH8pIBiAS8giXP7WcSiUfoFYIzYAK9QC5c37E4SIVtJVBMwlURc0/uNt2o4RcNRvdHa4CJ5w==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/abtesting": "1.14.1", - "@algolia/client-abtesting": "5.48.1", - "@algolia/client-analytics": "5.48.1", - "@algolia/client-common": "5.48.1", - "@algolia/client-insights": "5.48.1", - "@algolia/client-personalization": "5.48.1", - "@algolia/client-query-suggestions": "5.48.1", - "@algolia/client-search": "5.48.1", - "@algolia/ingestion": "1.48.1", - "@algolia/monitoring": "1.48.1", - "@algolia/recommend": "5.48.1", - "@algolia/requester-browser-xhr": "5.48.1", - "@algolia/requester-fetch": "5.48.1", - "@algolia/requester-node-http": "5.48.1" + "@algolia/abtesting": "1.18.0", + "@algolia/client-abtesting": "5.52.0", + "@algolia/client-analytics": "5.52.0", + "@algolia/client-common": "5.52.0", + "@algolia/client-insights": "5.52.0", + "@algolia/client-personalization": "5.52.0", + "@algolia/client-query-suggestions": "5.52.0", + "@algolia/client-search": "5.52.0", + "@algolia/ingestion": "1.52.0", + "@algolia/monitoring": "1.52.0", + "@algolia/recommend": "5.52.0", + "@algolia/requester-browser-xhr": "5.52.0", + "@algolia/requester-fetch": "5.52.0", + "@algolia/requester-node-http": "5.52.0" }, "engines": { "node": ">= 14.0.0" @@ -4260,9 +4853,9 @@ } }, "node_modules/beasties": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.1.tgz", - "integrity": "sha512-2Imdcw3LznDuxAbJM26RHniOLAzE6WgrK8OuvVXCQtNBS8rsnD9zsSEa3fHl4hHpUY7BYTlrpvtPVbvu9G6neg==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.4.2.tgz", + "integrity": "sha512-NvcGjG/7AVUAfRbvrJmHunDQS9uHnE6Q/7AkaPr8oKE8HjOlpjRG5075z/th2Tmlezk3VlaaS8+X9I1RwHJMQw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4633,33 +5226,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -5015,13 +5581,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -5258,6 +5817,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", @@ -5275,6 +5851,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5813,33 +6399,6 @@ "dev": true, "license": "ISC" }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jose": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", @@ -5966,63 +6525,26 @@ "license": "MIT" }, "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "wrap-ansi": "^10.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=22.13.0" } }, "node_modules/lmdb": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.1.tgz", - "integrity": "sha512-NYHA0MRPjvNX+vSw8Xxg6FLKxzAG+e7Pt8RqAQA/EehzHVXq9SxDqJIN3JL1hK0dweb884y8kIh6rkWvPyg9Wg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.4.tgz", + "integrity": "sha512-9FKQA6G1MMtqNxfxvSBNXD/axeG2QRjYbNh0/ykRL5xYcRbCm2vXq7B9bhc7nSuKdHzr8/BHIwfPuYYH1UsXXw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6039,13 +6561,13 @@ "download-lmdb-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.5.1", - "@lmdb/lmdb-darwin-x64": "3.5.1", - "@lmdb/lmdb-linux-arm": "3.5.1", - "@lmdb/lmdb-linux-arm64": "3.5.1", - "@lmdb/lmdb-linux-x64": "3.5.1", - "@lmdb/lmdb-win32-arm64": "3.5.1", - "@lmdb/lmdb-win32-x64": "3.5.1" + "@lmdb/lmdb-darwin-arm64": "3.5.4", + "@lmdb/lmdb-darwin-x64": "3.5.4", + "@lmdb/lmdb-linux-arm": "3.5.4", + "@lmdb/lmdb-linux-arm64": "3.5.4", + "@lmdb/lmdb-linux-x64": "3.5.4", + "@lmdb/lmdb-win32-arm64": "3.5.4", + "@lmdb/lmdb-win32-x64": "3.5.4" } }, "node_modules/log-symbols": { @@ -6463,13 +6985,13 @@ } }, "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", "dev": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/nanoid": { @@ -6804,9 +7326,9 @@ } }, "node_modules/ora": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", - "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.0.tgz", + "integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6816,7 +7338,7 @@ "is-interactive": "^2.0.0", "is-unicode-supported": "^2.1.0", "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.1", + "stdin-discarder": "^0.3.2", "string-width": "^8.1.0" }, "engines": { @@ -6848,12 +7370,13 @@ } }, "node_modules/pacote": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.3.1.tgz", - "integrity": "sha512-O0EDXi85LF4AzdjG74GUwEArhdvawi/YOHcsW6IijKNj7wm8IvEWNF5GnfuxNpQ/ZpO3L37+v8hqdVh8GgWYhg==", + "version": "21.5.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.5.0.tgz", + "integrity": "sha512-VtZ0SB8mb5Tzw3dXDfVAIjhyVKUHZkS/ZH9/5mpKenwC9sFOXNI0JI7kEF7IMkwOnsWMFrvAZHzx1T5fmrp9FQ==", "dev": true, "license": "ISC", "dependencies": { + "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", @@ -6867,7 +7390,6 @@ "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" @@ -6893,13 +7415,13 @@ } }, "node_modules/parse5-html-rewriting-stream": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.0.tgz", - "integrity": "sha512-wzh11mj8KKkno1pZEu+l2EVeWsuKDfR5KNWZOTsslfUX8lPDZx77m9T0kIoAVkFtD1nx6YF8oh4BnPHvxMtNMw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.1.tgz", + "integrity": "sha512-NaRku2aMpUN1Sh1Gyk1KWUh2A7EJx2c6qYzvwsPtqhoHoaURshdrceYK3LunVCm3WHhm6FS7Vcczbvdh3/UIVw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^6.0.0", + "entities": "^8.0.0", "parse5": "^8.0.0", "parse5-sax-parser": "^8.0.0" }, @@ -6908,13 +7430,13 @@ } }, "node_modules/parse5-html-rewriting-stream/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=0.12" + "node": ">=20.19.0" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -7143,20 +7665,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7271,16 +7779,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -7288,38 +7786,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rolldown": { - "version": "1.0.0-rc.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.4.tgz", - "integrity": "sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.113.0", - "@rolldown/pluginutils": "1.0.0-rc.4" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.4", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.4", - "@rolldown/binding-darwin-x64": "1.0.0-rc.4", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.4", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.4", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.4", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.4", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.4", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.4", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.4", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.4", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.4" - } - }, "node_modules/rollup": { "version": "4.62.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", @@ -7399,14 +7865,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.97.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", - "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -7925,14 +8391,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -8062,9 +8528,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8415,89 +8881,21 @@ } }, "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrappy": { @@ -8600,24 +8998,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", + "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/material/ngrx-signal-store/demo/package.json b/material/ngrx-signal-store/demo/package.json index 9e3ccaf6..a6fcc69a 100644 --- a/material/ngrx-signal-store/demo/package.json +++ b/material/ngrx-signal-store/demo/package.json @@ -11,24 +11,24 @@ "private": true, "packageManager": "npm@10.9.4", "dependencies": { - "@angular/common": "^21.2.0", - "@angular/compiler": "^21.2.0", - "@angular/core": "^21.2.0", - "@angular/forms": "^21.2.0", - "@angular/platform-browser": "^21.2.0", - "@angular/router": "^21.2.0", + "@angular/common": "^22.0.1", + "@angular/compiler": "^22.0.1", + "@angular/core": "^22.0.1", + "@angular/forms": "^22.0.1", + "@angular/platform-browser": "^22.0.1", + "@angular/router": "^22.0.1", "@ngrx/operators": "^21.1.1", "@ngrx/signals": "^21.1.1", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, "devDependencies": { - "@angular/build": "^21.2.7", - "@angular/cli": "^21.2.7", - "@angular/compiler-cli": "^21.2.0", + "@angular/build": "^22.0.1", + "@angular/cli": "^22.0.1", + "@angular/compiler-cli": "^22.0.1", "jsdom": "^28.0.0", "prettier": "^3.8.1", - "typescript": "~5.9.2", + "typescript": "~6.0.3", "vitest": "^4.0.8" } } diff --git a/material/ngrx-signal-store/demo/tsconfig.app.json b/material/ngrx-signal-store/demo/tsconfig.app.json index 264f459b..8356da16 100644 --- a/material/ngrx-signal-store/demo/tsconfig.app.json +++ b/material/ngrx-signal-store/demo/tsconfig.app.json @@ -6,10 +6,14 @@ "outDir": "./out-tsc/app", "types": [] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"], + "angularCompilerOptions": { + "extendedDiagnostics": { + "checks": { + "nullishCoalescingNotNullable": "suppress", + "optionalChainNotNullable": "suppress" + } + } + } } From cb1d7e773c862ac6b5d22bb3ff1b29065554cca7 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Sun, 14 Jun 2026 15:30:41 +0200 Subject: [PATCH 16/24] material(ngrx): Audit-Fixes Texte + Code (alle drei Teile + beide Demos) Faktische Fehler: - Buchauflage korrigiert: "(4. Auflage)" -> "(1. Auflage)" in allen drei Teilen - NgRx richtig aufgeloest: "Reactive State for Angular" statt "Reactive Extensions ..." - Entity-Reducer-Bug: createBookSuccess + action.book (war createBook/action.data) Korrektheit & Konsistenz (Artikel an bewiesene Demos angeglichen): - Ehrliche Fehler-Typisierung: unknown + toMessage()-Helfer statt {message:string} - Schreibvorgaenge setzen alte Fehlermeldung zurueck (Reducer/SignalStore + Artikel) - Teil-2-State-Illustration und Test-Stubs um error-Feld ergaenzt - Phantom-Import ./test-helper raus -> inline-Helfer (b()/book()), String-ISBNs - "Propertys" -> "Properties"; Teil 1 "Kapitel" -> "Artikel"; scan()-Aussage entschaerft Demos: vollstaendiges CRUD jetzt sichtbar - Anlege-Formular + "Bewertung +1" (update) + Loeschen verdrahtet; rating wird genutzt - OnPush ergaenzt; leere app.css entfernt; .error/.add-form-Styles; app.config aufgeraeumt - Global-Demo: ungenutztes @ngrx/operators entfernt - In-Memory-Service wirft bei doppelter ISBN -> Fehleranzeige ohne Server ausloesbar Tests deutlich erweitert (Beweis auf Angular 22 / NgRx 21): - Global: updateBook$-Effect + alle Failure-Pfade + Komponententest (16 -> 27) - Signal: Write-Failure, loading-Transition, error-reset, Komponententest (7 -> 12) Hygiene & Didaktik: - .vscode/mcp.json (npx-basiert) aus der Versionierung entfernt - Teil 3: async/Promise-Aussage entschaerft, HttpClient-Service-Hinweis, Test ohne doppeltes loadBooks() (onInit), store.select()-Erklaerung in Effects (Teil 2) - Demo-READMEs: CRUD-Beschreibung, Testumfang, Hinweis auf abgeflachte Struktur - lastModified der Artikel auf 2026-06-14 Beweis: Global 27/27, Signal 12/12 Vitest gruen; beide bauen (148 kB / 126 kB). --- material/ngrx-global-store/README.md | 134 ++++++++++++------ material/ngrx-global-store/demo/.gitignore | 1 - .../ngrx-global-store/demo/.vscode/mcp.json | 9 -- material/ngrx-global-store/demo/README.md | 10 +- .../ngrx-global-store/demo/package-lock.json | 13 -- material/ngrx-global-store/demo/package.json | 1 - .../ngrx-global-store/demo/src/app/app.css | 0 .../ngrx-global-store/demo/src/app/app.ts | 4 +- .../demo/src/app/book-list/book-list.html | 17 ++- .../demo/src/app/book-list/book-list.spec.ts | 68 +++++++++ .../demo/src/app/book-list/book-list.ts | 21 ++- .../demo/src/app/shared/book-store.service.ts | 18 ++- .../demo/src/app/shared/error-message.ts | 12 ++ .../demo/src/app/store/book.effects.spec.ts | 39 ++++- .../demo/src/app/store/book.effects.ts | 9 +- .../demo/src/app/store/book.reducer.spec.ts | 9 +- .../demo/src/app/store/book.reducer.ts | 8 ++ .../demo/src/app/store/book.selectors.spec.ts | 12 +- .../ngrx-global-store/demo/src/styles.css | 15 ++ material/ngrx-intro/README.md | 14 +- material/ngrx-signal-store/README.md | 104 +++++++++----- material/ngrx-signal-store/demo/.gitignore | 1 - .../ngrx-signal-store/demo/.vscode/mcp.json | 9 -- material/ngrx-signal-store/demo/README.md | 6 +- .../demo/src/app/app.config.ts | 5 +- .../ngrx-signal-store/demo/src/app/app.css | 0 .../ngrx-signal-store/demo/src/app/app.ts | 4 +- .../demo/src/app/book-list/book-list.html | 19 ++- .../demo/src/app/book-list/book-list.spec.ts | 48 +++++++ .../demo/src/app/book-list/book-list.ts | 21 ++- .../demo/src/app/book.store.spec.ts | 43 +++++- .../demo/src/app/book.store.ts | 16 +-- .../demo/src/app/shared/book-store.service.ts | 18 ++- .../demo/src/app/shared/error-message.ts | 12 ++ .../ngrx-signal-store/demo/src/styles.css | 15 ++ 35 files changed, 546 insertions(+), 189 deletions(-) delete mode 100644 material/ngrx-global-store/demo/.vscode/mcp.json delete mode 100644 material/ngrx-global-store/demo/src/app/app.css create mode 100644 material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts create mode 100644 material/ngrx-global-store/demo/src/app/shared/error-message.ts delete mode 100644 material/ngrx-signal-store/demo/.vscode/mcp.json delete mode 100644 material/ngrx-signal-store/demo/src/app/app.css create mode 100644 material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts create mode 100644 material/ngrx-signal-store/demo/src/app/shared/error-message.ts diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index b02b52c4..7dc042bd 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -1,11 +1,11 @@ --- title: "State Management mit NgRx – Teil 2: Global Store mit NgRx" published: "2026-06-11" -lastModified: "2026-06-13" +lastModified: "2026-06-14" hidden: true --- -**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (4. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** +**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (1. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** Dieser Artikel ist **Teil 2** einer dreiteiligen Serie zum Thema State Management mit NgRx. @@ -19,9 +19,9 @@ Im ersten Teil haben wir uns Schritt für Schritt ein eigenes Modell für zentra > **Hinweis zur Version:** Wir verwenden in diesem Artikel **Angular 22** mit der dazu passenden NgRx-Version (NgRx folgt dem Major-Versionsschema von Angular). Wir setzen durchgängig auf Standalone APIs und Signals: Den Store richten wir ausschließlich über die `provide…`-Funktionen ein und lesen ihn mit `selectSignal()` als Signal aus. Konstruktor-Injection brauchen wir nicht mehr – auch das offizielle Effect-Schematic erzeugt seine Abhängigkeiten inzwischen mit `inject()`. -## NgRx: Reactive Extensions for Angular +## NgRx: Reactive State for Angular -Das Framework *Reactive Extensions for Angular (NgRx)* ist eine der populärsten Implementierungen für State Management mit Angular. Durch die gezielte Ausrichtung auf Angular fügt sich der Code gut in die Strukturen und Lebenszyklen einer Angular-Anwendung ein. NgRx setzt stark auf die Möglichkeiten der reaktiven Programmierung mit RxJS, ist also an vielen Stellen von Observables und Datenströmen geprägt. Über `selectSignal()` integriert es sich außerdem nahtlos in die signal-basierte Welt von modernem Angular. Die große Community und eine Reihe von verwandten Projekten machen NgRx zum wohl bekanntesten Werkzeug für Zustandsverwaltung mit Angular. +Das Framework *Reactive State for Angular (NgRx)* ist eine der populärsten Implementierungen für State Management mit Angular. Durch die gezielte Ausrichtung auf Angular fügt sich der Code gut in die Strukturen und Lebenszyklen einer Angular-Anwendung ein. NgRx setzt stark auf die Möglichkeiten der reaktiven Programmierung mit RxJS, ist also an vielen Stellen von Observables und Datenströmen geprägt. Über `selectSignal()` integriert es sich außerdem nahtlos in die signal-basierte Welt von modernem Angular. Die große Community und eine Reihe von verwandten Projekten machen NgRx zum wohl bekanntesten Werkzeug für Zustandsverwaltung mit Angular. Wir wollen in diesem Abschnitt die Struktur und die Bausteine in der Welt von NgRx genauer besprechen. Außerdem wollen wir im BookMonkey einen Aspekt mithilfe von NgRx umsetzen, um so alle Bausteine auch praktisch zu üben. @@ -162,7 +162,8 @@ Damit haben wir die Struktur unseres Feature-States definiert. Die restlichen In { book: { books: [], - loading: false + loading: false, + error: null } } ``` @@ -225,7 +226,7 @@ export const loadBooksSuccess = createAction( ); ``` -Der generische Typparameter der Funktion `props()` gibt an, welche zusätzlichen Eigenschaften die Action enthalten soll. Wir haben hier den generischen Namen `data` gewählt, die Payload-Propertys können wir allerdings nach Belieben benennen. Das erzeugte Action-Objekt hat den folgenden Aufbau: +Der generische Typparameter der Funktion `props()` gibt an, welche zusätzlichen Eigenschaften die Action enthalten soll. Wir haben hier den generischen Namen `data` gewählt, die Payload-Properties können wir allerdings nach Belieben benennen. Das erzeugte Action-Objekt hat den folgenden Aufbau: ```ts { @@ -331,7 +332,7 @@ Wir wollen nun auch für den BookMonkey passende Reducers entwickeln. Dazu über - `loadBooksSuccess`: Buchliste einfügen und Ladeindikator auf `false` setzen - `loadBooksFailure`: Ladeindikator auf `false` setzen -Die Implementierung bringen wir in der Datei `books/store/book.reducer.ts` unter, in der das Grundgerüst der Reducer-Funktion bereits vorbereitet ist. Statt *switch/case* wird hier die Funktion `createReducer()` genutzt. Für jede Fallunterscheidung existiert ein Block, der in ein `on()` gekapselt ist. Als erstes Argument geben wir hier immer die Action an, die behandelt werden soll. Im zweiten Argument ist die Reducer-Funktion notiert, die schließlich anhand der eingehenden Action den neuen State generiert. Für die einzelnen Reducer-Funktionen sollten wir den Body in geschweiften Klammern definieren und von dort das State-Objekt direkt zurückgeben. Außerdem sollte der Rückgabetyp der Funktion immer explizit mit `State` definiert sein. +Die Implementierung bringen wir in der Datei `books/store/book.reducer.ts` unter, in der das Grundgerüst der Reducer-Funktion bereits vorbereitet ist. Statt *switch/case* wird hier die Funktion `createReducer()` genutzt. Für jede Fallunterscheidung existiert ein Block, der in ein `on()` gekapselt ist. Als erstes Argument geben wir hier immer die Action an, die behandelt werden soll. Im zweiten Argument ist die Reducer-Funktion notiert, die schließlich anhand der eingehenden Action den neuen State generiert. Für die einzelnen Reducer-Funktionen geben wir stets ein neues State-Objekt zurück und definieren den Rückgabetyp immer explizit mit `State`. Ob wir dafür die ausführliche Form mit `return` oder die kompakte Pfeilschreibweise (`(state): State => ({ ... })`) wählen, ist eine Frage des Stils – in der lauffähigen Beispiel-App verwenden wir die kompakte Variante. ```ts // books/store/book.reducer.ts @@ -339,7 +340,7 @@ export const reducer = createReducer( initialState, on(BookActions.loadBooks, (state): State => { - return { ...state, loading: true }; + return { ...state, loading: true, error: null }; }), on(BookActions.loadBooksSuccess, (state, action): State => { @@ -423,7 +424,7 @@ Das resultierende Signal aktualisiert sich nur dann, wenn sich der selektierte W ![Diagramm: Der Store gibt bei jeder Änderung den gesamten State aus (loading wechselt false, true, true, false). Der Selektor store.selectSignal(state => state.book.loading) gibt darunter nur dann einen neuen Wert aus, wenn sich das loading-Flag tatsächlich ändert (false, true, false).](./selectors-gap.svg "Selektor mit store.selectSignal() verwenden") -Wir legen die Signale direkt als Propertys in der Komponentenklasse ab. Wenn wir unsere Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. Da wir die Buchliste nun aus dem Store beziehen, benötigen wir den `BookStoreService` in der Komponente nicht mehr. +Wir legen die Signale direkt als Properties in der Komponentenklasse ab. Wenn wir unsere Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. Da wir die Buchliste nun aus dem Store beziehen, benötigen wir den `BookStoreService` in der Komponente nicht mehr. ```ts // books/book-list/book-list.component.ts @@ -484,7 +485,7 @@ Technisch ist ein Effect also immer ein `Observable`. Alle so erzeugten ![Diagramm: Wie der einfache Redux-Fluss, aber erweitert um Effects. Eine Action triggert nicht nur den Reducer, sondern löst auch einen Effect aus. Der Effect ruft einen Service auf, der mit einer API kommuniziert, und löst seinerseits eine neue Action aus, die wieder in den Reducer fließt.](./ngrx-flow-full.svg "Datenfluss in NgRx mit Effects") -Die Klasse `BookEffects` befindet sich in der Datei `books/store/book.effects.ts`. Ein Effect wird als ein Property in dieser Klasse definiert. Der Name des Propertys spielt keine Rolle, sollte aber passend zur Aufgabe benannt werden. Das Ziel ist es, hier ein Observable zu entwickeln, das Actions ausgibt. Jeden Effect kapseln wir mit der Funktion `createEffect()`, sodass er automatisch in den Lebenszyklus von NgRx integriert wird. Ein erster Effect zum Laden von Daten wurde bereits automatisch generiert, weil wir beim Anlegen die Einstellung `--api` verwendet haben. Dieses Grundgerüst wollen wir vervollständigen, um die Buchliste per HTTP zu laden. +Die Klasse `BookEffects` befindet sich in der Datei `books/store/book.effects.ts`. Ein Effect wird als ein Property in dieser Klasse definiert. Der Name des Properties spielt keine Rolle, sollte aber passend zur Aufgabe benannt werden. Das Ziel ist es, hier ein Observable zu entwickeln, das Actions ausgibt. Jeden Effect kapseln wir mit der Funktion `createEffect()`, sodass er automatisch in den Lebenszyklus von NgRx integriert wird. Ein erster Effect zum Laden von Daten wurde bereits automatisch generiert, weil wir beim Anlegen die Einstellung `--api` verwendet haben. Dieses Grundgerüst wollen wir vervollständigen, um die Buchliste per HTTP zu laden. In die Klasse holen wir mit `inject()` zwei Abhängigkeiten: den `BookStoreService`, der die HTTP-Kommunikation kapselt, und den Service `Actions`. Über `Actions` erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. Dieser Datenstrom ist meist die Grundlage für unsere Effects. (Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: [5 useful NgRx effects that don't rely on actions](https://angular.schule/blog/2018-06-5-useful-effects-without-actions).) @@ -499,6 +500,7 @@ import { of } from 'rxjs'; import * as BookActions from './book.actions'; import { BookStoreService } from '../../shared/book-store.service'; +import { toMessage } from '../../shared/error-message'; @Injectable() export class BookEffects { @@ -511,7 +513,8 @@ export class BookEffects { switchMap(() => this.service.getAll().pipe( map(data => BookActions.loadBooksSuccess({ data })), - catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) + catchError((error: unknown) => + of(BookActions.loadBooksFailure({ error: toMessage(error) }))) ) ) ); @@ -525,7 +528,14 @@ Für jede dieser Actions wollen wir nun einen HTTP-Request ausführen und die Er Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookStoreService` mit der Methode `getAll()` einsetzen, die ein Observable zurückgibt. Unser Effect ist nun ein Observable, das ein Array von Büchern liefert. Damit die Buchliste im Store verarbeitet werden kann, müssen wir sie in eine Action verpacken. Wir nutzen dazu den Operator `map()` und wandeln damit die Buchliste um in eine Action `loadBooksSuccess`, die die Liste enthält. -Ähnlich gehen wir für den Fehlerfall vor: Hier nutzen wir den Operator `catchError()`, um einen Fehler in der Ausführung abzufangen. Wir werfen den Fehler allerdings an der Stelle nicht weiter, sondern kapseln ihn in eine Action `loadBooksFailure`, die wir als reguläres Element des Datenstroms weitergeben. Dabei verwenden wir das Property `message` auf dem Fehlerobjekt, um die Meldung auszulesen. +Ähnlich gehen wir für den Fehlerfall vor: Hier nutzen wir den Operator `catchError()`, um einen Fehler in der Ausführung abzufangen. Wir werfen den Fehler allerdings an der Stelle nicht weiter, sondern kapseln ihn in eine Action `loadBooksFailure`, die wir als reguläres Element des Datenstroms weitergeben. Der Fehler im `catchError()` ist als `unknown` typisiert – an dieser Stelle kann schließlich alles Mögliche ankommen (ein `Error`, bei einem echten Backend eine `HttpErrorResponse`, im Extremfall ein String). Statt blind auf eine Eigenschaft wie `message` zuzugreifen, prüfen wir die Form des Fehlers in einer kleinen Hilfsfunktion `toMessage()`: + +```ts +// shared/error-message.ts +export function toMessage(error: unknown): string { + return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; +} +``` Auf diese Weise erzeugen wir einen Datenstrom, der nacheinander verschiedene Actions ausgeben kann. Die Funktion `createEffect()` sorgt dafür, dass dieser Datenstrom automatisch abonniert wird. Die resultierenden Actions werden in den Store dispatcht und durchlaufen dort die Reducers. In einem Effect sollten wir also niemals selbst die Methode `store.dispatch()` aufrufen. @@ -563,6 +573,7 @@ import { of } from 'rxjs'; import * as BookActions from './book.actions'; import { BookStoreService } from '../../shared/book-store.service'; +import { toMessage } from '../../shared/error-message'; export const loadBooks$ = createEffect( (actions$ = inject(Actions), service = inject(BookStoreService)) => { @@ -571,7 +582,8 @@ export const loadBooks$ = createEffect( switchMap(() => service.getAll().pipe( map(data => BookActions.loadBooksSuccess({ data })), - catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) + catchError((error: unknown) => + of(BookActions.loadBooksFailure({ error: toMessage(error) }))) ) ) ); @@ -625,7 +637,8 @@ createBook$ = createEffect(() => { concatMap(({ book }) => this.service.create(book).pipe( map(created => BookActions.createBookSuccess({ book: created })), - catchError(error => of(BookActions.createBookFailure({ error: error.message }))) + catchError((error: unknown) => + of(BookActions.createBookFailure({ error: toMessage(error) }))) ) ) ); @@ -637,7 +650,8 @@ updateBook$ = createEffect(() => { concatMap(({ book }) => this.service.update(book).pipe( map(updated => BookActions.updateBookSuccess({ book: updated })), - catchError(error => of(BookActions.updateBookFailure({ error: error.message }))) + catchError((error: unknown) => + of(BookActions.updateBookFailure({ error: toMessage(error) }))) ) ) ); @@ -649,17 +663,27 @@ deleteBook$ = createEffect(() => { concatMap(({ isbn }) => this.service.remove(isbn).pipe( map(() => BookActions.deleteBookSuccess({ isbn })), - catchError(error => of(BookActions.deleteBookFailure({ error: error.message }))) + catchError((error: unknown) => + of(BookActions.deleteBookFailure({ error: toMessage(error) }))) ) ) ); }); ``` -**Reducer.** Im Reducer passen wir den State immutabel an: anhängen mit dem Spread-Operator, ersetzen mit `map()`, entfernen mit `filter()`. Auf die drei Fehlerfälle reagieren wir mit *einem* `on()` gleichzeitig (das ist möglich, weil ein Reducer für jede Action durchlaufen wird): +**Reducer.** Im Reducer passen wir den State immutabel an: anhängen mit dem Spread-Operator, ersetzen mit `map()`, entfernen mit `filter()`. Beim Auslösen eines Schreibvorgangs setzen wir – wie schon beim Laden – eine alte Fehlermeldung zurück. Auf die drei Fehlerfälle reagieren wir mit *einem* `on()` gleichzeitig (das ist möglich, weil ein Reducer für jede Action durchlaufen wird): ```ts // books/store/book.reducer.ts (Ergänzung in createReducer) + +// Beim Auslösen eines Schreibvorgangs eine alte Fehlermeldung zurücksetzen: +on( + BookActions.createBook, + BookActions.updateBook, + BookActions.deleteBook, + (state): State => ({ ...state, error: null }) +), + on(BookActions.createBookSuccess, (state, action): State => ({ ...state, books: [...state.books, action.book] @@ -714,17 +738,25 @@ export class BookListComponent { this.store.dispatch(BookActions.loadBooks()); } - deleteBook(isbn: string): void { - this.store.dispatch(BookActions.deleteBook({ isbn })); + addBook(isbn: string, title: string): void { + if (!isbn || !title) { + return; + } + this.store.dispatch(BookActions.createBook({ book: { isbn, title, rating: 0 } })); } - createBook(book: Book): void { - this.store.dispatch(BookActions.createBook({ book })); + rateUp(book: Book): void { + const rating = Math.min((book.rating ?? 0) + 1, 5); + this.store.dispatch(BookActions.updateBook({ book: { ...book, rating } })); + } + + deleteBook(isbn: string): void { + this.store.dispatch(BookActions.deleteBook({ isbn })); } } ``` -Im Template zeigen wir die Fehlermeldung an und lösen das Löschen über einen Button aus: +Im Template zeigen wir die Fehlermeldung an, bieten ein kleines Formular zum Anlegen und lösen Ändern (Bewertung erhöhen) sowie Löschen über Buttons aus. Die Eingabefelder lesen wir bewusst ohne `FormsModule` über lokale Template-Referenzen (`#isbnEl`) aus, um den Fokus auf NgRx zu behalten: ```html @@ -732,10 +764,19 @@ Im Template zeigen wir die Fehlermeldung an und lösen das Löschen über einen
    {{ error }}
    } +
    + + + +
    +
      @for (book of books(); track book.isbn) {
    • - {{ book.title }} + {{ book.title }} – ★ {{ book.rating ?? 0 }} +
    • } @@ -822,7 +863,7 @@ export const BookActions = createActionGroup({ }); ``` -Die Besonderheit an diesem Ansatz ist, dass wir die Variablennamen nicht mehr von Hand notieren müssen. Wir können die Actions z. B. als `BookActions.loadBooks()` verwenden – der Name des Propertys wird automatisch aus dem Action Type ermittelt. Auf diese Weise sparen wir uns viel Tipparbeit. +Die Besonderheit an diesem Ansatz ist, dass wir die Variablennamen nicht mehr von Hand notieren müssen. Wir können die Actions z. B. als `BookActions.loadBooks()` verwenden – der Name des Properties wird automatisch aus dem Action Type ermittelt. Auf diese Weise sparen wir uns viel Tipparbeit. Um die Actions zu verwenden, importieren wir nun immer das gesamte Objekt `BookActions`, nicht die einzelnen Actions. In den Komponenten, Reducers und Effects sind also auch Anpassungen nötig, um diesen Ansatz zu nutzen. @@ -836,9 +877,9 @@ export const bookFeature = createFeature({ name: 'book', reducer: createReducer( initialState, - on(BookActions.loadBooks, (state): State => ({ ...state, loading: true })), + on(BookActions.loadBooks, (state): State => ({ ...state, loading: true, error: null })), on(BookActions.loadBooksSuccess, (state, { data }): State => ({ ...state, books: data, loading: false })), - on(BookActions.loadBooksFailure, (state): State => ({ ...state, loading: false })) + on(BookActions.loadBooksFailure, (state, { error }): State => ({ ...state, loading: false, error })) ) }); @@ -943,8 +984,8 @@ Alle Funktionen erhalten den aktuellen State und die zu verarbeitenden Werte als export const reducer = createReducer( initialState, - on(BookActions.createBook, (state, action) => { - return adapter.addOne(action.data, state); + on(BookActions.createBookSuccess, (state, action) => { + return adapter.addOne(action.book, state); }), on(BookActions.loadBooksSuccess, (state, action) => { @@ -977,6 +1018,8 @@ Auf diese Weise können wir die Verwaltung von Entitäten im State sehr effizien Alle auf Grundlage von NgRx entwickelten Bausteine sollten auch getestet werden. Dafür möchten wir in diesem Abschnitt einige Hinweise geben. Grundsätzlich werden bei der Initialisierung mit den Schematics von NgRx bereits Grundgerüste für die Unit-Tests angelegt – wir können also direkt loslegen. +> **Hinweis:** Die folgenden Listings zeigen die grundsätzlichen Testtechniken. Die lauffähige Beispiel-App im Ordner `demo/` enthält vollständige, mit **Vitest** umgesetzte Tests für Reducer, Selektoren, Effects und Komponente. Die `hot`/`cold`-Marbles und `spyOn()` aus den folgenden Beispielen setzen ein Jasmine-Setup voraus; mit Vitest stehen die äquivalenten Helfer unter `vi` bzw. über `vitest-marbles` bereit. + #### Actions Die Action Creators beinhalten keine spezifische Logik, die getestet werden muss. In manchen Blogartikeln wird vorgeschlagen, die korrekte Zusammensetzung der Objekte zu prüfen, damit eine Action stets den richtigen `type` besitzt. Wir sind der Meinung, dass solche trivialen Tests keinen Mehrwert liefern und deshalb nicht notwendig sind. @@ -994,7 +1037,8 @@ describe('Book Reducer', () => { it('should enable the loading flag for loadBooks', () => { const state = { books: [], - loading: false + loading: false, + error: null }; const action = loadBooks(); @@ -1013,13 +1057,16 @@ Die Hilfsfunktion `book()` haben wir selbst definiert. Sie generiert einfache Bu ```ts // books/store/book.selectors.spec.ts import { selectAllBooks } from './book.selectors'; -import { book } from './test-helper'; +import { Book } from '../../shared/book'; + +// kleine Hilfsfunktion, um schnell Testdaten zu erzeugen +const book = (isbn: string): Book => ({ isbn, title: `Titel ${isbn}`, rating: 0 }); describe('Book Selectors', () => { it('should select all books', () => { - const books = [book(1), book(2), book(3)]; + const books = [book('1'), book('2'), book('3')]; const state = { - book: { books, loading: false } + book: { books, loading: false, error: null } }; const result = selectAllBooks(state); @@ -1034,8 +1081,8 @@ Bei komplexeren Selektoren, die mehrere andere Selektoren als Grundlage verwende // books/store/book.selectors.spec.ts describe('Book Selectors', () => { it('should select all books', () => { - const books = [book(1), book(2), book(3)]; - const bookState = { books, loading: false }; + const books = [book('1'), book('2'), book('3')]; + const bookState = { books, loading: false, error: null }; const result = selectAllBooks.projector(bookState); expect(result).toEqual(books); @@ -1094,7 +1141,7 @@ describe('BookEffects', () => { }); it('should fire loadBooksSuccess for loadBooks', () => { - const books = [book(1), book(2), book(3)]; + const books = [book('1'), book('2'), book('3')]; // Implementierung von getAll() ersetzen const bs = TestBed.inject(BookStoreService); @@ -1134,7 +1181,7 @@ describe('BookEffects', () => { beforeEach(() => { /* ... */ }); it('should fire loadBooksSuccess for loadBooks', () => { - const books = [book(1), book(2), book(3)]; + const books = [book('1'), book('2'), book('3')]; const bs = TestBed.inject(BookStoreService); spyOn(bs, 'getAll').and.callFake(() => of(books)); @@ -1153,7 +1200,7 @@ Komponenten und Effects greifen häufig auf den Store zu. Unter anderem werden A Als Beispiel wollen wir den bekannten Effect zum Laden der Bücher so erweitern, dass er nur dann Bücher lädt, wenn die Buchliste im State noch leer ist. Hat das Array bereits Einträge, soll nichts passieren. -Es ist für dieses Szenario notwendig, dass wir den bestehenden State im Effect berücksichtigen. Dies können wir mit dem Operator `concatLatestFrom()` realisieren: Wir übergeben ein anderes Observable als Argument, und der Operator reichert den Hauptdatenstrom mit dem jeweils letzten Element aus diesem Observable an. Das bedeutet also, dass uns in den Effects neben dem Payload aus den Actions auch zusätzlich Daten aus dem Store zur Verfügung stehen. Wir verwenden hier direkt unseren Selektor `selectAllBooks`, um die aktuelle Buchliste aus dem Store zu erhalten. Anhand der Buchliste können wir dann mit dem Operator `filter()` entscheiden, ob die Bücher neu heruntergeladen werden sollen oder nicht. +Es ist für dieses Szenario notwendig, dass wir den bestehenden State im Effect berücksichtigen. Dies können wir mit dem Operator `concatLatestFrom()` realisieren: Wir übergeben ein anderes Observable als Argument, und der Operator reichert den Hauptdatenstrom mit dem jeweils letzten Element aus diesem Observable an. Das bedeutet also, dass uns in den Effects neben dem Payload aus den Actions auch zusätzlich Daten aus dem Store zur Verfügung stehen. Wir verwenden hier direkt unseren Selektor `selectAllBooks`, um die aktuelle Buchliste aus dem Store zu erhalten. Anhand der Buchliste können wir dann mit dem Operator `filter()` entscheiden, ob die Bücher neu heruntergeladen werden sollen oder nicht. In Effects arbeiten wir im Observable-Kontext – deshalb nutzen wir hier `store.select()` (liefert ein Observable) und nicht `selectSignal()`. > **Hinweis:** Seit NgRx 18 importieren wir den Operator `concatLatestFrom()` aus dem Paket `@ngrx/operators`. Zuvor war er Teil von `@ngrx/effects` (dort seit NgRx 17 als veraltet markiert). @@ -1180,7 +1227,8 @@ export class BookEffects { switchMap(() => this.service.getAll().pipe( map(data => BookActions.loadBooksSuccess({ data })), - catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) + catchError((error: unknown) => + of(BookActions.loadBooksFailure({ error: toMessage(error) }))) ) ) ); @@ -1231,7 +1279,7 @@ describe('BookEffects', () => { const store = TestBed.inject(MockStore); store.setState({ book: { - books: [book(1)], + books: [book('1')], loading: false } }); @@ -1289,11 +1337,11 @@ Um den Anwendungszustand mit NgRx zu verwalten, kommunizieren die Komponenten mi Damit sind die Komponenten stets abhängig von den Bausteinen des Frameworks NgRx. Das erschwert insbesondere das Testing und die Wiederverwendbarkeit. Außerdem müssen wir uns für das State Management auf eine spezifische Lösung festlegen. Jeglicher State wird global im Store abgelegt, und ein Mischbetrieb mit anderen Ansätzen führt potenziell zu unsauberem Code in der Komponente. -Um diese Unschönheiten zu lösen, können wir eine *Facade* verwenden: Dafür erstellen wir einen Service, der eine Schnittstelle zwischen der Komponente und dem Store bildet. Die Komponente kommuniziert über Methoden und Propertys mit der Facade. Das Framework NgRx bleibt dabei verborgen, und wir halten die Komponente frei von frameworkspezifischem Code. +Um diese Unschönheiten zu lösen, können wir eine *Facade* verwenden: Dafür erstellen wir einen Service, der eine Schnittstelle zwischen der Komponente und dem Store bildet. Die Komponente kommuniziert über Methoden und Properties mit der Facade. Das Framework NgRx bleibt dabei verborgen, und wir halten die Komponente frei von frameworkspezifischem Code. ![Diagramm: Zwischen Component und Store liegt eine Facade. Die Component ruft Methoden der Facade auf (doThings()) und liest Daten über ein Signal (data). Die Facade kommuniziert per dispatch() und selectSignal() mit dem Store, sodass die Component selbst keinen direkten Kontakt zu NgRx hat.](./facade.svg "NgRx Flow mit Facade") -Zur Implementierung nutzen wir einen einfachen Service, der mit dem Store kommuniziert. Als öffentliche Schnittstelle bieten wir gut lesbare Methoden und Propertys an. +Zur Implementierung nutzen wir einen einfachen Service, der mit dem Store kommuniziert. Als öffentliche Schnittstelle bieten wir gut lesbare Methoden und Properties an. ```ts @Injectable() @@ -1308,7 +1356,7 @@ export class BooksFacade { } ``` -In der Komponente nutzen wir die Facade, um die Daten aus den Propertys zu lesen und Aktionen mithilfe der angebotenen Methoden auszulösen. +In der Komponente nutzen wir die Facade, um die Daten aus den Properties zu lesen und Aktionen mithilfe der angebotenen Methoden auszulösen. ```ts @Component({ /* ... */ }) @@ -1323,7 +1371,7 @@ export class BookListComponent { } ``` -Im Unit-Test können wir einen solchen Service leicht durch einen Stub ersetzen. Die Propertys und Methoden können dabei in einem einfachen Objekt nachgebildet werden, ohne die Schnittstelle des Stores ausmocken zu müssen. +Im Unit-Test können wir einen solchen Service leicht durch einen Stub ersetzen. Die Properties und Methoden können dabei in einem einfachen Objekt nachgebildet werden, ohne die Schnittstelle des Stores ausmocken zu müssen. ```ts import { signal } from '@angular/core'; diff --git a/material/ngrx-global-store/demo/.gitignore b/material/ngrx-global-store/demo/.gitignore index 854acd5f..b1d225e2 100644 --- a/material/ngrx-global-store/demo/.gitignore +++ b/material/ngrx-global-store/demo/.gitignore @@ -26,7 +26,6 @@ yarn-error.log !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -!.vscode/mcp.json .history/* # Miscellaneous diff --git a/material/ngrx-global-store/demo/.vscode/mcp.json b/material/ngrx-global-store/demo/.vscode/mcp.json deleted file mode 100644 index 956af8c6..00000000 --- a/material/ngrx-global-store/demo/.vscode/mcp.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - // For more information, visit: https://angular.dev/ai/mcp - "servers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp"] - } - } -} diff --git a/material/ngrx-global-store/demo/README.md b/material/ngrx-global-store/demo/README.md index 55ccdd05..34269df9 100644 --- a/material/ngrx-global-store/demo/README.md +++ b/material/ngrx-global-store/demo/README.md @@ -2,20 +2,22 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil 2: Global Store](../README.md)**. -Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern und löschen (CRUD) mit Actions, Reducer, Selektoren und Effects, inklusive Fehler-State. +Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern (Bewertung erhöhen) und löschen (CRUD) mit Actions, Reducer, Selektoren und Effects – inklusive Lade- und Fehleranzeige. - **Angular 22** + **NgRx 21** (`@ngrx/store`, `@ngrx/effects`) -- Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. +- Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/store/…`); hier liegen die Dateien direkt unter `src/app/store/…`. + ## Befehle ```bash npm install -npm test # Vitest: Reducer-, Selektor- und Effect-Tests +npm test # Vitest: Reducer-, Selektor-, Effect- und Komponententests npm start # ng serve npm run build ``` -Die Tests beweisen das State Management isoliert (Reducer als Pure Functions, Effects mit `provideMockActions`, Selektoren, Komponente mit `provideMockStore`). +Die Tests beweisen das State Management isoliert: Reducer als Pure Functions, Selektoren, Effects mit `provideMockActions` und die Komponente mit `provideMockStore`. diff --git a/material/ngrx-global-store/demo/package-lock.json b/material/ngrx-global-store/demo/package-lock.json index 3af3f21d..1ffdceb8 100644 --- a/material/ngrx-global-store/demo/package-lock.json +++ b/material/ngrx-global-store/demo/package-lock.json @@ -15,7 +15,6 @@ "@angular/platform-browser": "^22.0.1", "@angular/router": "^22.0.1", "@ngrx/effects": "^21.1.1", - "@ngrx/operators": "^21.1.1", "@ngrx/store": "^21.1.1", "rxjs": "~7.8.0", "tslib": "^2.3.0" @@ -3498,18 +3497,6 @@ "rxjs": "^6.5.3 || ^7.5.0" } }, - "node_modules/@ngrx/operators": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-21.1.1.tgz", - "integrity": "sha512-2ChT1rVi6w8xHrmIAqRFuvY7YLAJiH7idfmbimjOYHzJ7pgaz0+4WVqyCp9pv2UiDEXuvpUOvs3afgS5m1LDNQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "peerDependencies": { - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@ngrx/store": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-21.1.1.tgz", diff --git a/material/ngrx-global-store/demo/package.json b/material/ngrx-global-store/demo/package.json index 4310d2ca..3060e811 100644 --- a/material/ngrx-global-store/demo/package.json +++ b/material/ngrx-global-store/demo/package.json @@ -18,7 +18,6 @@ "@angular/platform-browser": "^22.0.1", "@angular/router": "^22.0.1", "@ngrx/effects": "^21.1.1", - "@ngrx/operators": "^21.1.1", "@ngrx/store": "^21.1.1", "rxjs": "~7.8.0", "tslib": "^2.3.0" diff --git a/material/ngrx-global-store/demo/src/app/app.css b/material/ngrx-global-store/demo/src/app/app.css deleted file mode 100644 index e69de29b..00000000 diff --git a/material/ngrx-global-store/demo/src/app/app.ts b/material/ngrx-global-store/demo/src/app/app.ts index 029de745..54511afa 100644 --- a/material/ngrx-global-store/demo/src/app/app.ts +++ b/material/ngrx-global-store/demo/src/app/app.ts @@ -1,10 +1,10 @@ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { BookList } from './book-list/book-list'; @Component({ selector: 'app-root', imports: [BookList], templateUrl: './app.html', - styleUrl: './app.css' + changeDetection: ChangeDetectionStrategy.OnPush }) export class App {} diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.html b/material/ngrx-global-store/demo/src/app/book-list/book-list.html index 624d28b6..f4913e18 100644 --- a/material/ngrx-global-store/demo/src/app/book-list/book-list.html +++ b/material/ngrx-global-store/demo/src/app/book-list/book-list.html @@ -1,17 +1,26 @@

      Books ({{ books().length }})

      @if (loading()) { -

      Loading ...

      +
      Loading ...
      } @if (error(); as error) { -

      {{ error }}

      +
      {{ error }}
      } -
        +
        + + + +
        + +
          @for (book of books(); track book.isbn) {
        • - {{ book.title }} + {{ book.title }} – ★ {{ book.rating ?? 0 }} +
        • } diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts b/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts new file mode 100644 index 00000000..d974f83f --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts @@ -0,0 +1,68 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import type { MockInstance } from 'vitest'; + +import { BookList } from './book-list'; +import * as BookActions from '../store/book.actions'; +import { selectAllBooks, selectBooksError, selectBooksLoading } from '../store/book.selectors'; +import { Book } from '../shared/book'; + +const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title, rating: 0 }); + +interface Setup { + fixture: ComponentFixture; + dispatch: MockInstance<(action: unknown) => void>; +} + +describe('BookList', () => { + async function setup(books: Book[] = [], loading = false, error: string | null = null): Promise { + await TestBed.configureTestingModule({ + imports: [BookList], + providers: [ + provideMockStore({ + selectors: [ + { selector: selectAllBooks, value: books }, + { selector: selectBooksLoading, value: loading }, + { selector: selectBooksError, value: error } + ] + }) + ] + }).compileComponents(); + + const store = TestBed.inject(MockStore); + const dispatch = vi.spyOn(store, 'dispatch'); + const fixture = TestBed.createComponent(BookList); + await fixture.whenStable(); + return { fixture, dispatch }; + } + + it('dispatcht loadBooks beim Erzeugen', async () => { + const { dispatch } = await setup(); + expect(dispatch).toHaveBeenCalledWith(BookActions.loadBooks()); + }); + + it('rendert die Buchliste', async () => { + const { fixture } = await setup([b('1', 'Eins'), b('2', 'Zwei')]); + const items = fixture.nativeElement.querySelectorAll('li'); + expect(items.length).toBe(2); + expect(items[0].textContent).toContain('Eins'); + }); + + it('zeigt den Ladeindikator', async () => { + const { fixture } = await setup([], true); + expect(fixture.nativeElement.querySelector('.loader')).toBeTruthy(); + }); + + it('zeigt eine Fehlermeldung', async () => { + const { fixture } = await setup([], false, 'Kaputt'); + expect(fixture.nativeElement.querySelector('.error')?.textContent).toContain('Kaputt'); + }); + + it('dispatcht deleteBook beim Klick auf Löschen', async () => { + const { fixture, dispatch } = await setup([b('1')]); + dispatch.mockClear(); + const deleteButton = fixture.nativeElement.querySelector('li button:last-child') as HTMLButtonElement; + deleteButton.click(); + expect(dispatch).toHaveBeenCalledWith(BookActions.deleteBook({ isbn: '1' })); + }); +}); diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.ts b/material/ngrx-global-store/demo/src/app/book-list/book-list.ts index e06a29e3..c34098ce 100644 --- a/material/ngrx-global-store/demo/src/app/book-list/book-list.ts +++ b/material/ngrx-global-store/demo/src/app/book-list/book-list.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; import * as BookActions from '../store/book.actions'; @@ -7,7 +7,8 @@ import { Book } from '../shared/book'; @Component({ selector: 'app-book-list', - templateUrl: './book-list.html' + templateUrl: './book-list.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class BookList { private store = inject(Store); @@ -20,11 +21,19 @@ export class BookList { this.store.dispatch(BookActions.loadBooks()); } - deleteBook(isbn: string): void { - this.store.dispatch(BookActions.deleteBook({ isbn })); + addBook(isbn: string, title: string): void { + if (!isbn || !title) { + return; + } + this.store.dispatch(BookActions.createBook({ book: { isbn, title, rating: 0 } })); + } + + rateUp(book: Book): void { + const rating = Math.min((book.rating ?? 0) + 1, 5); + this.store.dispatch(BookActions.updateBook({ book: { ...book, rating } })); } - addBook(book: Book): void { - this.store.dispatch(BookActions.createBook({ book })); + deleteBook(isbn: string): void { + this.store.dispatch(BookActions.deleteBook({ isbn })); } } diff --git a/material/ngrx-global-store/demo/src/app/shared/book-store.service.ts b/material/ngrx-global-store/demo/src/app/shared/book-store.service.ts index 0d4157fb..b43b0e6b 100644 --- a/material/ngrx-global-store/demo/src/app/shared/book-store.service.ts +++ b/material/ngrx-global-store/demo/src/app/shared/book-store.service.ts @@ -1,11 +1,14 @@ import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { Book } from './book'; /** * In-Memory-Stand-in für ein echtes HTTP-Backend. * Die Methoden geben – wie ein HttpClient-Service – Observables zurück, * sodass der Effect-Code identisch zum Artikel bleibt. + * + * `create()` wirft bewusst einen Fehler, wenn die ISBN bereits existiert – + * so lässt sich die Fehleranzeige der Anwendung auch ohne Server auslösen. */ @Injectable({ providedIn: 'root' }) export class BookStoreService { @@ -16,17 +19,20 @@ export class BookStoreService { ]; getAll(): Observable { - return of([...this.books]); + return of(this.books.map(book => ({ ...book }))); } create(book: Book): Observable { - this.books = [...this.books, book]; - return of(book); + if (this.books.some(b => b.isbn === book.isbn)) { + return throwError(() => new Error(`Ein Buch mit der ISBN ${book.isbn} existiert bereits.`)); + } + this.books = [...this.books, { ...book }]; + return of({ ...book }); } update(book: Book): Observable { - this.books = this.books.map(b => (b.isbn === book.isbn ? book : b)); - return of(book); + this.books = this.books.map(b => (b.isbn === book.isbn ? { ...book } : b)); + return of({ ...book }); } remove(isbn: string): Observable { diff --git a/material/ngrx-global-store/demo/src/app/shared/error-message.ts b/material/ngrx-global-store/demo/src/app/shared/error-message.ts new file mode 100644 index 00000000..0021b504 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/shared/error-message.ts @@ -0,0 +1,12 @@ +/** + * Wandelt einen unbekannten Fehler (z. B. aus `catchError` oder `tapResponse`) + * sicher in eine lesbare Meldung um. Der Fehler ist als `unknown` typisiert – + * wir prüfen die Form, statt sie blind anzunehmen. + * + * Bei einem echten HttpClient-Backend wäre der Fehler in der Regel eine + * `HttpErrorResponse`, die ebenfalls eine `message`-Eigenschaft trägt; die + * Prüfung ließe sich dafür leicht erweitern. + */ +export function toMessage(error: unknown): string { + return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; +} diff --git a/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts index 21d381fb..d54b7c93 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts @@ -35,7 +35,7 @@ describe('BookEffects', () => { }); it('loadBooks$ feuert loadBooksFailure bei einem Fehler', () => { - const effects = setup({ getAll: () => throwError(() => ({ message: 'Netzwerkfehler' })) }); + const effects = setup({ getAll: () => throwError(() => new Error('Netzwerkfehler')) }); actions$ = of(BookActions.loadBooks()); let result: Action | undefined; @@ -53,6 +53,34 @@ describe('BookEffects', () => { expect(result).toEqual(BookActions.createBookSuccess({ book })); }); + it('createBook$ feuert createBookFailure bei einem Fehler', () => { + const effects = setup({ create: () => throwError(() => new Error('Doppelte ISBN')) }); + actions$ = of(BookActions.createBook({ book: b('3', 'Neu') })); + + let result: Action | undefined; + effects.createBook$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.createBookFailure({ error: 'Doppelte ISBN' })); + }); + + it('updateBook$ feuert updateBookSuccess', () => { + const book = b('1', 'Geändert'); + const effects = setup({ update: x => of(x) }); + actions$ = of(BookActions.updateBook({ book })); + + let result: Action | undefined; + effects.updateBook$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.updateBookSuccess({ book })); + }); + + it('updateBook$ feuert updateBookFailure bei einem Fehler', () => { + const effects = setup({ update: () => throwError(() => new Error('Speicherfehler')) }); + actions$ = of(BookActions.updateBook({ book: b('1') })); + + let result: Action | undefined; + effects.updateBook$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.updateBookFailure({ error: 'Speicherfehler' })); + }); + it('deleteBook$ feuert deleteBookSuccess mit der ISBN', () => { const effects = setup({ remove: () => of(undefined) }); actions$ = of(BookActions.deleteBook({ isbn: '1' })); @@ -61,4 +89,13 @@ describe('BookEffects', () => { effects.deleteBook$.subscribe(action => (result = action)); expect(result).toEqual(BookActions.deleteBookSuccess({ isbn: '1' })); }); + + it('deleteBook$ feuert deleteBookFailure bei einem Fehler', () => { + const effects = setup({ remove: () => throwError(() => new Error('Löschfehler')) }); + actions$ = of(BookActions.deleteBook({ isbn: '1' })); + + let result: Action | undefined; + effects.deleteBook$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.deleteBookFailure({ error: 'Löschfehler' })); + }); }); diff --git a/material/ngrx-global-store/demo/src/app/store/book.effects.ts b/material/ngrx-global-store/demo/src/app/store/book.effects.ts index a1b4e49c..729e5554 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.effects.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.effects.ts @@ -5,6 +5,7 @@ import { of } from 'rxjs'; import * as BookActions from './book.actions'; import { BookStoreService } from '../shared/book-store.service'; +import { toMessage } from '../shared/error-message'; @Injectable() export class BookEffects { @@ -18,7 +19,7 @@ export class BookEffects { switchMap(() => this.service.getAll().pipe( map(data => BookActions.loadBooksSuccess({ data })), - catchError(error => of(BookActions.loadBooksFailure({ error: error.message }))) + catchError((error: unknown) => of(BookActions.loadBooksFailure({ error: toMessage(error) }))) ) ) ) @@ -31,7 +32,7 @@ export class BookEffects { concatMap(({ book }) => this.service.create(book).pipe( map(created => BookActions.createBookSuccess({ book: created })), - catchError(error => of(BookActions.createBookFailure({ error: error.message }))) + catchError((error: unknown) => of(BookActions.createBookFailure({ error: toMessage(error) }))) ) ) ) @@ -43,7 +44,7 @@ export class BookEffects { concatMap(({ book }) => this.service.update(book).pipe( map(updated => BookActions.updateBookSuccess({ book: updated })), - catchError(error => of(BookActions.updateBookFailure({ error: error.message }))) + catchError((error: unknown) => of(BookActions.updateBookFailure({ error: toMessage(error) }))) ) ) ) @@ -55,7 +56,7 @@ export class BookEffects { concatMap(({ isbn }) => this.service.remove(isbn).pipe( map(() => BookActions.deleteBookSuccess({ isbn })), - catchError(error => of(BookActions.deleteBookFailure({ error: error.message }))) + catchError((error: unknown) => of(BookActions.deleteBookFailure({ error: toMessage(error) }))) ) ) ) diff --git a/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts index cff9d9f5..dc7250d8 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts @@ -1,3 +1,5 @@ +import { Action } from '@ngrx/store'; + import { initialState, reducer, State } from './book.reducer'; import * as BookActions from './book.actions'; import { Book } from '../shared/book'; @@ -43,6 +45,11 @@ describe('Book Reducer', () => { expect(state.books.map(x => x.isbn)).toEqual(['2']); }); + it('ein Schreibvorgang (Auslöser-Action) setzt eine alte Fehlermeldung zurück', () => { + const state = reducer({ ...initialState, error: 'alt' }, BookActions.createBook({ book: b('9') })); + expect(state.error).toBeNull(); + }); + it('alle Failure-Actions setzen error (Mehrfach-on)', () => { expect(reducer(initialState, BookActions.createBookFailure({ error: 'c' })).error).toBe('c'); expect(reducer(initialState, BookActions.updateBookFailure({ error: 'u' })).error).toBe('u'); @@ -50,7 +57,7 @@ describe('Book Reducer', () => { }); it('lässt den State bei unbekannter Action unverändert', () => { - const state = reducer(initialState, { type: 'unbekannt' } as never); + const state = reducer(initialState, { type: 'unbekannt' } as Action); expect(state).toBe(initialState); }); }); diff --git a/material/ngrx-global-store/demo/src/app/store/book.reducer.ts b/material/ngrx-global-store/demo/src/app/store/book.reducer.ts index 7b6929bb..35b72ea7 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.reducer.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.reducer.ts @@ -31,6 +31,14 @@ export const reducer = createReducer( error: action.error })), + // Schreibvorgänge: beim Auslösen eine alte Fehlermeldung zurücksetzen. + on( + BookActions.createBook, + BookActions.updateBook, + BookActions.deleteBook, + (state): State => ({ ...state, error: null }) + ), + on(BookActions.createBookSuccess, (state, action): State => ({ ...state, books: [...state.books, action.book] diff --git a/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts index 7472bd87..9b5c48da 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts @@ -1,12 +1,16 @@ -import { selectAllBooks, selectBooksError, selectBooksLoading } from './book.selectors'; +import { selectAllBooks, selectBookState, selectBooksError, selectBooksLoading } from './book.selectors'; +import { bookFeatureKey, State } from './book.reducer'; import { Book } from '../shared/book'; const b = (isbn: string): Book => ({ isbn, title: `Title ${isbn}` }); describe('Book Selectors', () => { - const rootState = { - book: { books: [b('1'), b('2')], loading: true, error: 'x' } - } as never; + const bookState: State = { books: [b('1'), b('2')], loading: true, error: 'x' }; + const rootState: Record = { [bookFeatureKey]: bookState }; + + it('selectBookState liefert den Feature-State', () => { + expect(selectBookState(rootState)).toBe(bookState); + }); it('selectAllBooks liefert die Buchliste', () => { expect(selectAllBooks(rootState)).toEqual([b('1'), b('2')]); diff --git a/material/ngrx-global-store/demo/src/styles.css b/material/ngrx-global-store/demo/src/styles.css index 90d4ee00..b9b0344e 100644 --- a/material/ngrx-global-store/demo/src/styles.css +++ b/material/ngrx-global-store/demo/src/styles.css @@ -1 +1,16 @@ /* You can add global styles to this file, and also import other style files */ + +.loader { + font-style: italic; + color: #555; +} + +.error { + color: #b00020; +} + +.add-form { + display: flex; + gap: 0.5rem; + margin: 1rem 0; +} diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md index 2b35d040..b15ff901 100644 --- a/material/ngrx-intro/README.md +++ b/material/ngrx-intro/README.md @@ -1,11 +1,11 @@ --- title: "State Management mit NgRx – Teil 1: Wie kommen wir zu zentralem State Management?" published: "2026-06-11" -lastModified: "2026-06-13" +lastModified: "2026-06-14" hidden: true --- -**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (4. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** +**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (1. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** Dieser Artikel ist **Teil 1** einer dreiteiligen Serie zum Thema State Management mit NgRx. @@ -30,9 +30,9 @@ Mit wachsender Größe der Anwendung ergeben sich immer wieder folgende Fragen: - Wie reagieren wir an verschiedenen Stellen auf Ereignisse, die in der Anwendung auftreten? - Wie verwalten wir die Daten, die über die gesamte Anwendung verteilt sind? -Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookStoreService`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter treiben: Bislang waren Komponenten die "Hüterinnen" der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine abgeschottete Einheit zu den anderen Komponenten. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. +Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookStoreService`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter treiben: Bislang waren Komponenten die "Hüterinnen" der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine gegenüber den anderen Komponenten abgeschottete Einheit. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. -Wir wollen in diesem Kapitel besprechen, wie eine solche zentrale Zustandsverwaltung (engl. *State Management*) realisiert werden kann. Dabei lernen wir das Architekturmuster *Redux* kennen und nutzen das populäre Framework *Reactive Extensions for Angular (NgRx)*, um den Anwendungszustand zu verwalten und unsere Prozesse zu koordinieren. +Wir wollen in diesem Artikel besprechen, wie eine solche zentrale Zustandsverwaltung (engl. *State Management*) realisiert werden kann. Dabei lernen wir das Architekturmuster *Redux* kennen und nutzen das populäre Framework *Reactive State for Angular (NgRx)*, um den Anwendungszustand zu verwalten und unsere Prozesse zu koordinieren. ## Ein Modell für zentrales State Management @@ -319,7 +319,7 @@ Redux ist ein populäres Pattern zur Zustandsverwaltung in Webanwendungen. Die I Der zentrale Bestandteil der Architektur ist ein *Store*, in dem der gesamte Anwendungszustand als eine einzige große verschachtelte Datenstruktur hinterlegt ist. Der Store ist die *Single Source of Truth* für die Anwendung und enthält alle Zustände: vom Server heruntergeladene Daten, gesetzte Einstellungen, die aktuell geladene Route oder Infos zum eigenen Account – alles, was sich zur Laufzeit in der Anwendung verändert und den Zustand beschreibt. -Das State-Objekt im Store hat zwei elementare Eigenschaften: Es ist *immutable* und *read-only*. Wir können die Daten aus dem State nicht verändern, sondern ausschließlich lesend darauf zugreifen. Möchten wir den State "verändern", so muss das existierende Objekt durch eine Kopie ausgetauscht werden, die die Änderungen enthält. Solche Änderungen am State werden durch Nachrichten ausgelöst, die aus der Anwendung in den Store gesendet werden. Die Grundidee dieser Architektur haben wir bereits in der Einleitung zu diesem Kapitel gemeinsam entwickelt. +Das State-Objekt im Store hat zwei elementare Eigenschaften: Es ist *immutable* und *read-only*. Wir können die Daten aus dem State nicht verändern, sondern ausschließlich lesend darauf zugreifen. Möchten wir den State "verändern", so muss das existierende Objekt durch eine Kopie ausgetauscht werden, die die Änderungen enthält. Solche Änderungen am State werden durch Nachrichten ausgelöst, die aus der Anwendung in den Store gesendet werden. Die Grundidee dieser Architektur haben wir bereits in der Einleitung zu diesem Artikel gemeinsam entwickelt. > **Hinweis:** Das State-Objekt ist theoretisch mutierbar, wir werden es aber stets immutable behandeln. NgRx setzt also auf die Pseudo-Immutability. @@ -327,13 +327,13 @@ Neben dem zentralen Store mit dem State-Objekt verwendet Redux zwei weitere wese ![Diagramm: Die Component löst eine Action aus, die in den Store fließt. Dort triggert die Action einen Reducer, der einen neuen State erzeugt. Der State wird zurück an die Component gepusht. Die Daten fließen so im Kreis in eine Richtung.](./redux-flow-simple.svg "Datenfluss in Redux") -Bringt man diese Bausteine in den Kontext des einführenden Beispiels, so entspricht der zentrale Service dem Store von Redux. Die gesendeten Nachrichten entsprechen den Actions. Die Funktion `calculateState()`, die wir zur Veranschaulichung verwendet haben, ist genauso aufgebaut wie die Reducers von Redux. Der Operator `scan()` ist tatsächlich auch die technische Grundlage des Frameworks NgRx, das wir in diesem Kapitel für das State Management nutzen werden. +Bringt man diese Bausteine in den Kontext des einführenden Beispiels, so entspricht der zentrale Service dem Store von Redux. Die gesendeten Nachrichten entsprechen den Actions. Die Funktion `calculateState()`, die wir zur Veranschaulichung verwendet haben, ist genauso aufgebaut wie die Reducers von Redux. Dasselbe Prinzip – das Aufsummieren eines Stroms von Nachrichten zu einem Zustand – steckt auch im Kern von NgRx, das wir in diesem Artikel für das State Management nutzen werden. ### Redux und Angular Die originale Implementierung von Redux stammt aus der Welt von React. Alle enthaltenen Ideen können aber problemlos auch auf die Architektur einer Angular-Anwendung übertragen werden. Es existieren verschiedene Frameworks und Bibliotheken, die ein zentrales State Management für Angular ermöglichen. Sie alle folgen der grundsätzlichen Idee von Redux: -- Reactive Extensions for Angular (NgRx) +- Reactive State for Angular (NgRx) - NGXS - Akita - Elf diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index 4b8da4b5..1b6c1496 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -1,11 +1,11 @@ --- title: "State Management mit NgRx – Teil 3: SignalStore" published: "2026-06-11" -lastModified: "2026-06-13" +lastModified: "2026-06-14" hidden: true --- -**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (4. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** +**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (1. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** Dieser Artikel ist **Teil 3** einer dreiteiligen Serie zum Thema State Management mit NgRx. @@ -200,7 +200,7 @@ Hier liegt der zentrale Unterschied zum Global Store: Es gibt keine Action und k ### Asynchrone Seiteneffekte mit `rxMethod`: Daten laden -Für das Laden der Bücher brauchen wir einen asynchronen Seiteneffekt – die Aufgabe, die im Global Store ein Effect übernommen hat. Einfache Abläufe lassen sich direkt mit einer `async`-Methode und einem Promise erledigen. Für reaktive Abläufe mit RxJS bietet das Interop-Plugin die Funktion `rxMethod()` aus `@ngrx/signals/rxjs-interop`. Sie nimmt eine Kette von RxJS-Operatoren entgegen und gibt eine reaktive Methode zurück. +Für das Laden der Bücher brauchen wir einen asynchronen Seiteneffekt – die Aufgabe, die im Global Store ein Effect übernommen hat. Eine Store-Methode darf eine ganz gewöhnliche Methode sein; einfache Abläufe ließen sich daher auch mit einer `async`-Methode und einem Promise erledigen, die das Ergebnis am Ende per `patchState()` in den State schreibt. Wir zeigen hier aber den reaktiven Weg mit RxJS, weil er sich nahtlos mit einem HttpClient kombinieren lässt: Dafür bietet das Interop-Plugin die Funktion `rxMethod()` aus `@ngrx/signals/rxjs-interop`. Sie nimmt eine Kette von RxJS-Operatoren entgegen und gibt eine reaktive Methode zurück. Zum sicheren Verarbeiten der HTTP-Antwort nutzen wir den Operator `tapResponse` aus `@ngrx/operators`. Er ruft je nach Ausgang den `next`-, `error`- oder `finalize`-Callback auf – und ein Fehler im Service-Aufruf beendet dadurch nicht den reaktiven Strom der Methode: @@ -211,6 +211,7 @@ import { patchState, withMethods } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { BookStoreService } from '../shared/book-store.service'; +import { toMessage } from '../shared/error-message'; // innerhalb von signalStore(...): withMethods((store, service = inject(BookStoreService)) => ({ @@ -221,8 +222,7 @@ withMethods((store, service = inject(BookStoreService)) => ({ service.getAll().pipe( tapResponse({ next: books => patchState(store, { books }), - error: (err: { message: string }) => - patchState(store, { error: err.message }), + error: (err: unknown) => patchState(store, { error: toMessage(err) }), finalize: () => patchState(store, { loading: false }) }) ) @@ -232,29 +232,38 @@ withMethods((store, service = inject(BookStoreService)) => ({ })) ``` +Den Fehler behandeln wir wie in Teil 2 mit der kleinen Hilfsfunktion `toMessage()`. Der `error`-Callback von `tapResponse` liefert den Fehler als `unknown` – wir prüfen also die Form, statt blind auf eine Eigenschaft zuzugreifen: + +```ts +// shared/error-message.ts +export function toMessage(error: unknown): string { + return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; +} +``` + Die so erzeugte Methode rufen wir später einfach als `store.loadBooks()` auf. Wir haben hier `switchMap()` gewählt: Wird während eines laufenden Ladevorgangs erneut geladen, soll nur die letzte Anfrage zählen. ### Schreiben: Bücher anlegen, ändern und löschen -Jetzt zum Kern jeder echten Anwendung: Daten verändern. Wir definieren je eine Methode zum Anlegen, Ändern und Löschen. Jede löst einen HTTP-Request aus (über den `BookStoreService`, der `create()`, `update()` und `remove()` anbietet) und schreibt das Ergebnis anschließend immutabel in den State. +Jetzt zum Kern jeder echten Anwendung: Daten verändern. Wir definieren je eine Methode zum Anlegen, Ändern und Löschen. Jede löst einen HTTP-Request aus – über den `BookStoreService`, der wie schon in Teil 2 die HTTP-Aufrufe mit dem `HttpClient` kapselt und `create()`, `update()` und `remove()` anbietet (`provideHttpClient()` gehört dafür in die `app.config.ts`) – und schreibt das Ergebnis anschließend immutabel in den State. Ein wichtiger Unterschied zum Laden betrifft den Flattening-Operator: Beim Laden ist `switchMap()` richtig (eine neue Anfrage macht die alte überflüssig). Bei **schreibenden** Operationen wollen wir laufende Requests aber *nicht* abbrechen – sonst ginge womöglich ein Speichervorgang verloren. Hier ist `concatMap()` die sichere Wahl: Die Anfragen werden der Reihe nach abgearbeitet. ```ts -import { concatMap, pipe } from 'rxjs'; +import { concatMap, pipe, tap } from 'rxjs'; // innerhalb von withMethods((store, service = inject(BookStoreService)) => ({ ... })): // Anlegen: an die Liste anhängen addBook: rxMethod( pipe( + tap(() => patchState(store, { error: null })), concatMap(book => service.create(book).pipe( tapResponse({ next: created => patchState(store, state => ({ books: [...state.books, created] })), - error: (err: { message: string }) => - patchState(store, { error: err.message }) + error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) ) ) @@ -264,6 +273,7 @@ addBook: rxMethod( // Ändern: das passende Buch in der Liste ersetzen updateBook: rxMethod( pipe( + tap(() => patchState(store, { error: null })), concatMap(book => service.update(book).pipe( tapResponse({ @@ -271,8 +281,7 @@ updateBook: rxMethod( patchState(store, state => ({ books: state.books.map(b => b.isbn === updated.isbn ? updated : b) })), - error: (err: { message: string }) => - patchState(store, { error: err.message }) + error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) ) ) @@ -282,6 +291,7 @@ updateBook: rxMethod( // Löschen: das Buch anhand seiner ISBN aus der Liste filtern deleteBook: rxMethod( pipe( + tap(() => patchState(store, { error: null })), concatMap(isbn => service.remove(isbn).pipe( tapResponse({ @@ -289,8 +299,7 @@ deleteBook: rxMethod( patchState(store, state => ({ books: state.books.filter(b => b.isbn !== isbn) })), - error: (err: { message: string }) => - patchState(store, { error: err.message }) + error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) ) ) @@ -298,7 +307,7 @@ deleteBook: rxMethod( ) ``` -An diesen drei Methoden sieht man das Muster für jede Mutation: HTTP-Request auslösen, bei Erfolg den State **immutabel** anpassen (anhängen mit Spread, ersetzen mit `map()`, entfernen mit `filter()`), bei Fehler die Meldung in `error` schreiben. Genau diese immutable Logik ist auch das Herz eines Reducers im Global Store – nur dass wir sie hier direkt in der Methode notieren, ohne Action und Reducer dazwischen. +An diesen drei Methoden sieht man das Muster für jede Mutation: zu Beginn eine alte Fehlermeldung zurücksetzen, den HTTP-Request auslösen, bei Erfolg den State **immutabel** anpassen (anhängen mit Spread, ersetzen mit `map()`, entfernen mit `filter()`) und bei Fehler die Meldung in `error` schreiben. Genau diese immutable Logik ist auch das Herz eines Reducers im Global Store – nur dass wir sie hier direkt in der Methode notieren, ohne Action und Reducer dazwischen. ### Das vollständige Beispiel @@ -321,6 +330,7 @@ import { tapResponse } from '@ngrx/operators'; import { Book } from '../shared/book'; import { BookStoreService } from '../shared/book-store.service'; +import { toMessage } from '../shared/error-message'; type BookState = { books: Book[]; @@ -351,8 +361,7 @@ export const BookStore = signalStore( service.getAll().pipe( tapResponse({ next: books => patchState(store, { books }), - error: (err: { message: string }) => - patchState(store, { error: err.message }), + error: (err: unknown) => patchState(store, { error: toMessage(err) }), finalize: () => patchState(store, { loading: false }) }) ) @@ -361,13 +370,13 @@ export const BookStore = signalStore( ), addBook: rxMethod( pipe( + tap(() => patchState(store, { error: null })), concatMap(book => service.create(book).pipe( tapResponse({ next: created => patchState(store, state => ({ books: [...state.books, created] })), - error: (err: { message: string }) => - patchState(store, { error: err.message }) + error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) ) ) @@ -375,15 +384,15 @@ export const BookStore = signalStore( ), updateBook: rxMethod( pipe( + tap(() => patchState(store, { error: null })), concatMap(book => service.update(book).pipe( tapResponse({ next: updated => patchState(store, state => ({ - books: state.books.map(b => b.isbn === updated.isbn ? updated : b) + books: state.books.map(b => (b.isbn === updated.isbn ? updated : b)) })), - error: (err: { message: string }) => - patchState(store, { error: err.message }) + error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) ) ) @@ -391,6 +400,7 @@ export const BookStore = signalStore( ), deleteBook: rxMethod( pipe( + tap(() => patchState(store, { error: null })), concatMap(isbn => service.remove(isbn).pipe( tapResponse({ @@ -398,8 +408,7 @@ export const BookStore = signalStore( patchState(store, state => ({ books: state.books.filter(b => b.isbn !== isbn) })), - error: (err: { message: string }) => - patchState(store, { error: err.message }) + error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) ) ) @@ -422,28 +431,37 @@ Die Komponente injiziert den Store, liest die Signale im Template und ruft für ```ts // books/book-list/book-list.component.ts -import { Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { BookStore } from '../book.store'; import { Book } from '../shared/book'; @Component({ selector: 'bm-book-list', - templateUrl: './book-list.component.html' + templateUrl: './book-list.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class BookListComponent { protected store = inject(BookStore); - deleteBook(isbn: string): void { - this.store.deleteBook(isbn); + addBook(isbn: string, title: string): void { + if (!isbn || !title) { + return; + } + this.store.addBook({ isbn, title, rating: 0 }); } - createBook(book: Book): void { - this.store.addBook(book); + rateUp(book: Book): void { + const rating = Math.min((book.rating ?? 0) + 1, 5); + this.store.updateBook({ ...book, rating }); + } + + deleteBook(isbn: string): void { + this.store.deleteBook(isbn); } } ``` -Im Template lesen wir die Signale und lösen über Events die Methoden aus – ein Löschen-Button pro Buch, eine Fehlermeldung aus dem `error`-Signal: +Im Template lesen wir die Signale und lösen über Events die Methoden aus: ein kleines Formular zum Anlegen, pro Buch ein Button zum Erhöhen der Bewertung und ein Löschen-Button sowie eine Fehlermeldung aus dem `error`-Signal: ```html @@ -460,17 +478,26 @@ Im Template lesen wir die Signale und lösen über Events die Methoden aus – e } +
          + + + +
          +
            @for (book of store.books(); track book.isbn) {
          • - {{ book.title }} + {{ book.title }} – ★ {{ book.rating ?? 0 }} +
          • }
          ``` -Ein Formular zum Anlegen würde im `(ngSubmit)` einfach `createBook(neuesBuch)` aufrufen. Weil der Store global (`providedIn: 'root'`) bereitsteht, teilen sich alle Komponenten dieselbe Instanz: Eine Detail- oder Formularkomponente, die `store.addBook()` aufruft, verändert denselben State, den die Liste anzeigt – die Oberfläche aktualisiert sich dank der Signale automatisch. +Die Eingabefelder lesen wir bewusst ohne `FormsModule` über lokale Template-Referenzen (`#isbnEl`) aus, um den Fokus auf den SignalStore zu behalten. Weil der Store global (`providedIn: 'root'`) bereitsteht, teilen sich alle Komponenten dieselbe Instanz: Eine separate Detail- oder Formularkomponente, die `store.addBook()` aufruft, verändert denselben State, den die Liste anzeigt – die Oberfläche aktualisiert sich dank der Signale automatisch. ### Weniger Boilerplate: Entitäten mit `withEntities` @@ -574,7 +601,7 @@ Dieses Feature lässt sich anschließend in beliebig vielen Stores einsetzen: `s ### Testing -Da ein SignalStore ein gewöhnlicher Service ist, testen wir ihn auch wie einen Service: Instanz beziehen, Methoden aufrufen, Signale auslesen. Ein vollständiges Store-Setup wie beim Global Store ist nicht nötig. Den HTTP-Service ersetzen wir wie gewohnt per `{ provide: …, useValue: … }`: +Da ein SignalStore ein gewöhnlicher Service ist, testen wir ihn auch wie einen Service: Instanz beziehen, Methoden aufrufen, Signale auslesen. Ein vollständiges Store-Setup wie beim Global Store ist nicht nötig. Den HTTP-Service ersetzen wir wie gewohnt per `{ provide: …, useValue: … }`. Weil unser Store sich über `onInit` beim Injizieren selbst lädt, müssen wir `loadBooks()` im Test nicht eigens aufrufen: ```ts // books/book.store.spec.ts @@ -582,7 +609,10 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { BookStore } from './book.store'; import { BookStoreService } from '../shared/book-store.service'; -import { book } from './test-helper'; +import { Book } from '../shared/book'; + +// kleine Hilfsfunktion, um schnell Testdaten zu erzeugen +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ isbn, title }); describe('BookStore', () => { it('lädt Bücher und zählt sie', () => { @@ -590,13 +620,13 @@ describe('BookStore', () => { providers: [ { provide: BookStoreService, - useValue: { getAll: () => of([book(1), book(2), book(3)]) } + useValue: { getAll: () => of([b('1'), b('2'), b('3')]) } } ] }); + // onInit löst loadBooks() bereits beim Injizieren aus: const store = TestBed.inject(BookStore); - store.loadBooks(); expect(store.booksCount()).toBe(3); }); @@ -623,7 +653,7 @@ Beide Bausteine stammen aus dem NgRx-Projekt und lösen dieselbe Aufgabe, setzen | Lesen | Selektoren + `selectSignal()` | State *ist* direkt ein Signal: `store.books()` | | Schreiben | Action `dispatch()` → Reducer | Methode aufrufen → `patchState()` | | Indirektion | hoch (Action/Reducer/Effect getrennt) | gering (Methode ändert State direkt) | -| Seiteneffekte | Effects (`@ngrx/effects`) | Methoden (`async`) oder `rxMethod` | +| Seiteneffekte | Effects (`@ngrx/effects`) | `rxMethod` (oder einfache `async`-Methoden) | | Boilerplate | viel (Actions, Reducer, Selektoren, Effects) | wenig (`with…`-Features in einer Datei) | | Entitäten | `@ngrx/entity` | `withEntities` (`@ngrx/signals/entities`) | | DevTools / Time Travel | ja (Redux DevTools) | nicht offiziell (Angular DevTools / Community) | diff --git a/material/ngrx-signal-store/demo/.gitignore b/material/ngrx-signal-store/demo/.gitignore index 854acd5f..b1d225e2 100644 --- a/material/ngrx-signal-store/demo/.gitignore +++ b/material/ngrx-signal-store/demo/.gitignore @@ -26,7 +26,6 @@ yarn-error.log !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -!.vscode/mcp.json .history/* # Miscellaneous diff --git a/material/ngrx-signal-store/demo/.vscode/mcp.json b/material/ngrx-signal-store/demo/.vscode/mcp.json deleted file mode 100644 index 956af8c6..00000000 --- a/material/ngrx-signal-store/demo/.vscode/mcp.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - // For more information, visit: https://angular.dev/ai/mcp - "servers": { - "angular-cli": { - "command": "npx", - "args": ["-y", "@angular/cli", "mcp"] - } - } -} diff --git a/material/ngrx-signal-store/demo/README.md b/material/ngrx-signal-store/demo/README.md index d12bc878..7f01c26c 100644 --- a/material/ngrx-signal-store/demo/README.md +++ b/material/ngrx-signal-store/demo/README.md @@ -2,13 +2,15 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil 3: SignalStore](../README.md)**. -Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern und löschen (CRUD) mit `signalStore`, `withState`, `withComputed`, `withMethods`, `rxMethod` und `patchState`, inklusive Fehler-State. +Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern (Bewertung erhöhen) und löschen (CRUD) mit `signalStore`, `withState`, `withComputed`, `withMethods`, `rxMethod` und `patchState` – inklusive Lade- und Fehleranzeige. - **Angular 22** + **NgRx 21** (`@ngrx/signals`, `@ngrx/operators`) -- Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. +- Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/…`); hier liegt der Store direkt unter `src/app/book.store.ts`. + ## Befehle ```bash diff --git a/material/ngrx-signal-store/demo/src/app/app.config.ts b/material/ngrx-signal-store/demo/src/app/app.config.ts index 7f244ed8..cb999ab9 100644 --- a/material/ngrx-signal-store/demo/src/app/app.config.ts +++ b/material/ngrx-signal-store/demo/src/app/app.config.ts @@ -1,8 +1,5 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; export const appConfig: ApplicationConfig = { - providers: [ - provideBrowserGlobalErrorListeners(), - - ] + providers: [provideBrowserGlobalErrorListeners()] }; diff --git a/material/ngrx-signal-store/demo/src/app/app.css b/material/ngrx-signal-store/demo/src/app/app.css deleted file mode 100644 index e69de29b..00000000 diff --git a/material/ngrx-signal-store/demo/src/app/app.ts b/material/ngrx-signal-store/demo/src/app/app.ts index 029de745..54511afa 100644 --- a/material/ngrx-signal-store/demo/src/app/app.ts +++ b/material/ngrx-signal-store/demo/src/app/app.ts @@ -1,10 +1,10 @@ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { BookList } from './book-list/book-list'; @Component({ selector: 'app-root', imports: [BookList], templateUrl: './app.html', - styleUrl: './app.css' + changeDetection: ChangeDetectionStrategy.OnPush }) export class App {} diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.html b/material/ngrx-signal-store/demo/src/app/book-list/book-list.html index a3ce042f..a9003530 100644 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.html +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.html @@ -1,20 +1,29 @@

          Books ({{ store.booksCount() }})

          @if (store.loading()) { -

          Loading ...

          +
          Loading ...
          } @if (store.error(); as error) { -

          +

          {{ error }} -

          +
          } -
            +
            + + + +
            + +
              @for (book of store.books(); track book.isbn) {
            • - {{ book.title }} + {{ book.title }} – ★ {{ book.rating ?? 0 }} +
            • } diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts b/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts new file mode 100644 index 00000000..d93e7c52 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of } from 'rxjs'; + +import { BookList } from './book-list'; +import { BookStoreService } from '../shared/book-store.service'; +import { Book } from '../shared/book'; + +const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title, rating: 0 }); + +describe('BookList (SignalStore)', () => { + async function setup(books: Book[] = []): Promise> { + await TestBed.configureTestingModule({ + imports: [BookList], + providers: [ + { + provide: BookStoreService, + useValue: { + getAll: () => of(books), + create: (book: Book) => of(book), + update: (book: Book) => of(book), + remove: () => of(undefined) + } + } + ] + }).compileComponents(); + + const fixture = TestBed.createComponent(BookList); + await fixture.whenStable(); + return fixture; + } + + it('rendert die vom Store geladenen Bücher', async () => { + const fixture = await setup([b('1', 'Eins'), b('2', 'Zwei')]); + const items = fixture.nativeElement.querySelectorAll('li'); + expect(items.length).toBe(2); + expect(items[0].textContent).toContain('Eins'); + }); + + it('entfernt ein Buch beim Klick auf Löschen', async () => { + const fixture = await setup([b('1', 'Eins')]); + const deleteButton = fixture.nativeElement.querySelector('li button:last-child') as HTMLButtonElement; + + deleteButton.click(); + await fixture.whenStable(); + + expect(fixture.nativeElement.querySelectorAll('li').length).toBe(0); + }); +}); diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts b/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts index 450379b9..aa284f46 100644 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts @@ -1,19 +1,28 @@ -import { Component, inject } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { BookStore } from '../book.store'; import { Book } from '../shared/book'; @Component({ selector: 'app-book-list', - templateUrl: './book-list.html' + templateUrl: './book-list.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class BookList { protected store = inject(BookStore); - deleteBook(isbn: string): void { - this.store.deleteBook(isbn); + addBook(isbn: string, title: string): void { + if (!isbn || !title) { + return; + } + this.store.addBook({ isbn, title, rating: 0 }); + } + + rateUp(book: Book): void { + const rating = Math.min((book.rating ?? 0) + 1, 5); + this.store.updateBook({ ...book, rating }); } - addBook(book: Book): void { - this.store.addBook(book); + deleteBook(isbn: string): void { + this.store.deleteBook(isbn); } } diff --git a/material/ngrx-signal-store/demo/src/app/book.store.spec.ts b/material/ngrx-signal-store/demo/src/app/book.store.spec.ts index cf13c6f9..50fc59d3 100644 --- a/material/ngrx-signal-store/demo/src/app/book.store.spec.ts +++ b/material/ngrx-signal-store/demo/src/app/book.store.spec.ts @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing'; -import { of, throwError } from 'rxjs'; +import { of, Subject, throwError } from 'rxjs'; import { BookStore } from './book.store'; import { BookStoreService } from './shared/book-store.service'; @@ -25,6 +25,19 @@ describe('BookStore (SignalStore)', () => { expect(store.booksCount()).toBe(3); }); + it('setzt loading=true, solange der Ladevorgang läuft', () => { + const response = new Subject(); + const store = createStore({ getAll: () => response }); + // onInit hat loadBooks() ausgelöst; die Antwort steht noch aus: + expect(store.loading()).toBe(true); + + response.next([b('1')]); + response.complete(); + + expect(store.loading()).toBe(false); + expect(store.booksCount()).toBe(1); + }); + it('addBook hängt ein Buch immutabel an die Liste an', () => { const store = createStore({ getAll: () => of([b('1')]), @@ -64,16 +77,40 @@ describe('BookStore (SignalStore)', () => { it('schreibt eine Fehlermeldung in den State, wenn das Laden fehlschlägt', () => { const store = createStore({ - getAll: () => throwError(() => ({ message: 'Netzwerkfehler' })) + getAll: () => throwError(() => new Error('Netzwerkfehler')) }); expect(store.error()).toBe('Netzwerkfehler'); expect(store.loading()).toBe(false); }); + it('schreibt eine Fehlermeldung, wenn das Anlegen fehlschlägt', () => { + const store = createStore({ + getAll: () => of([b('1')]), + create: () => throwError(() => new Error('Doppelte ISBN')) + }); + + store.addBook(b('1', 'Doppelt')); + + expect(store.error()).toBe('Doppelte ISBN'); + expect(store.booksCount()).toBe(1); // Liste bleibt unverändert + }); + + it('ein erfolgreiches Schreiben räumt eine alte Fehlermeldung weg', () => { + const store = createStore({ + getAll: () => throwError(() => new Error('Netzwerkfehler')), + remove: () => of(undefined) + }); + expect(store.error()).toBe('Netzwerkfehler'); + + store.deleteBook('1'); + + expect(store.error()).toBeNull(); + }); + it('clearError setzt die Fehlermeldung zurück', () => { const store = createStore({ - getAll: () => throwError(() => ({ message: 'Netzwerkfehler' })) + getAll: () => throwError(() => new Error('Netzwerkfehler')) }); expect(store.error()).toBe('Netzwerkfehler'); diff --git a/material/ngrx-signal-store/demo/src/app/book.store.ts b/material/ngrx-signal-store/demo/src/app/book.store.ts index 39e0d856..04d2b2aa 100644 --- a/material/ngrx-signal-store/demo/src/app/book.store.ts +++ b/material/ngrx-signal-store/demo/src/app/book.store.ts @@ -13,6 +13,7 @@ import { tapResponse } from '@ngrx/operators'; import { Book } from './shared/book'; import { BookStoreService } from './shared/book-store.service'; +import { toMessage } from './shared/error-message'; type BookState = { books: Book[]; @@ -43,8 +44,7 @@ export const BookStore = signalStore( service.getAll().pipe( tapResponse({ next: books => patchState(store, { books }), - error: (err: { message: string }) => - patchState(store, { error: err.message }), + error: (err: unknown) => patchState(store, { error: toMessage(err) }), finalize: () => patchState(store, { loading: false }) }) ) @@ -53,13 +53,13 @@ export const BookStore = signalStore( ), addBook: rxMethod( pipe( + tap(() => patchState(store, { error: null })), concatMap(book => service.create(book).pipe( tapResponse({ next: created => patchState(store, state => ({ books: [...state.books, created] })), - error: (err: { message: string }) => - patchState(store, { error: err.message }) + error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) ) ) @@ -67,6 +67,7 @@ export const BookStore = signalStore( ), updateBook: rxMethod( pipe( + tap(() => patchState(store, { error: null })), concatMap(book => service.update(book).pipe( tapResponse({ @@ -74,8 +75,7 @@ export const BookStore = signalStore( patchState(store, state => ({ books: state.books.map(b => (b.isbn === updated.isbn ? updated : b)) })), - error: (err: { message: string }) => - patchState(store, { error: err.message }) + error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) ) ) @@ -83,6 +83,7 @@ export const BookStore = signalStore( ), deleteBook: rxMethod( pipe( + tap(() => patchState(store, { error: null })), concatMap(isbn => service.remove(isbn).pipe( tapResponse({ @@ -90,8 +91,7 @@ export const BookStore = signalStore( patchState(store, state => ({ books: state.books.filter(b => b.isbn !== isbn) })), - error: (err: { message: string }) => - patchState(store, { error: err.message }) + error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) ) ) diff --git a/material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts b/material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts index 8d1ebab3..a62e2442 100644 --- a/material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts +++ b/material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts @@ -1,11 +1,14 @@ import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { Book } from './book'; /** * In-Memory-Stand-in für ein echtes HTTP-Backend. * Die Methoden geben – wie ein HttpClient-Service – Observables zurück, * sodass der Store-Code identisch zum Artikel bleibt. + * + * `create()` wirft bewusst einen Fehler, wenn die ISBN bereits existiert – + * so lässt sich die Fehleranzeige der Anwendung auch ohne Server auslösen. */ @Injectable({ providedIn: 'root' }) export class BookStoreService { @@ -16,17 +19,20 @@ export class BookStoreService { ]; getAll(): Observable { - return of([...this.books]); + return of(this.books.map(book => ({ ...book }))); } create(book: Book): Observable { - this.books = [...this.books, book]; - return of(book); + if (this.books.some(b => b.isbn === book.isbn)) { + return throwError(() => new Error(`Ein Buch mit der ISBN ${book.isbn} existiert bereits.`)); + } + this.books = [...this.books, { ...book }]; + return of({ ...book }); } update(book: Book): Observable { - this.books = this.books.map(b => (b.isbn === book.isbn ? book : b)); - return of(book); + this.books = this.books.map(b => (b.isbn === book.isbn ? { ...book } : b)); + return of({ ...book }); } remove(isbn: string): Observable { diff --git a/material/ngrx-signal-store/demo/src/app/shared/error-message.ts b/material/ngrx-signal-store/demo/src/app/shared/error-message.ts new file mode 100644 index 00000000..08bbec65 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/shared/error-message.ts @@ -0,0 +1,12 @@ +/** + * Wandelt einen unbekannten Fehler (z. B. aus `tapResponse`) sicher in eine + * lesbare Meldung um. Der Fehler ist als `unknown` typisiert – wir prüfen die + * Form, statt sie blind anzunehmen. + * + * Bei einem echten HttpClient-Backend wäre der Fehler in der Regel eine + * `HttpErrorResponse`, die ebenfalls eine `message`-Eigenschaft trägt; die + * Prüfung ließe sich dafür leicht erweitern. + */ +export function toMessage(error: unknown): string { + return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; +} diff --git a/material/ngrx-signal-store/demo/src/styles.css b/material/ngrx-signal-store/demo/src/styles.css index 90d4ee00..b9b0344e 100644 --- a/material/ngrx-signal-store/demo/src/styles.css +++ b/material/ngrx-signal-store/demo/src/styles.css @@ -1 +1,16 @@ /* You can add global styles to this file, and also import other style files */ + +.loader { + font-style: italic; + color: #555; +} + +.error { + color: #b00020; +} + +.add-form { + display: flex; + gap: 0.5rem; + margin: 1rem 0; +} From edb50a3f873e47a36928a0c9b4bfea1cc61ab011 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Sun, 14 Jun 2026 22:52:40 +0200 Subject: [PATCH 17/24] =?UTF-8?q?material(ngrx):=20Feinschliff=20=E2=80=93?= =?UTF-8?q?=20Versionshinweise,=20clearError-Parit=C3=A4t,=20Tests,=20Poli?= =?UTF-8?q?tur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Korrektheit/Ehrlichkeit: - Versionshinweise (Teil 2 + 3) richtiggestellt: Angular 22 + NgRx 21 laufen via legacy-peer-deps zusammen (NgRx 22 noch nicht released) – keine "passende"/ Major-Schema-Aussage mehr - Minko-Gechev-Zitat (nicht mehr belegbar) aus Teil 1 entfernt Demo-Parität & exemplarischer Code: - clearError nun auch im Global Store (Action + Reducer + OK-Button) -> beide Demos und beide Artikel dismissen Fehler einheitlich - addBook nimmt die Input-Elemente entgegen, leert die Felder selbst (statt Mehrfach-Statement im Template); aria-label an den Eingabefeldern - toMessage-Kommentar präzisiert (HttpErrorResponse ist kein instanceof Error) - ungenutzte Deps @angular/forms und @angular/router aus beiden Demos entfernt Tests erweitert (Beweis der neuen UI-Logik): - Global 31, Signal 16 (vorher 27/12): add-on-click, Leer-Guard, rateUp-Clamp@5, clearError-Klick, clearError-Reducer Konsistenz/Politur: - Demo-READMEs: Hinweis auf flache CLI-Namen vs. BookMonkey-Konvention - Teil-2-Testhinweis entschärft (vitest-marbles nicht im Demo enthalten) - Teil-3-Inkrement: Ternär-Klammern + Testdaten (Title) an Demo angeglichen - "nach dem Dispatch" -> "nach dem Anlegen" im SignalStore-Text Beweis: Global 31/31, Signal 16/16 Vitest gruen; beide bauen (148 kB / 126 kB). --- material/ngrx-global-store/README.md | 43 ++++++--- material/ngrx-global-store/demo/README.md | 2 +- .../ngrx-global-store/demo/package-lock.json | 42 +-------- material/ngrx-global-store/demo/package.json | 2 - .../demo/src/app/book-list/book-list.html | 13 +-- .../demo/src/app/book-list/book-list.spec.ts | 59 ++++++++++--- .../demo/src/app/book-list/book-list.ts | 12 ++- .../demo/src/app/shared/error-message.ts | 6 +- .../demo/src/app/store/book.actions.ts | 2 + .../demo/src/app/store/book.reducer.spec.ts | 5 ++ .../demo/src/app/store/book.reducer.ts | 2 + material/ngrx-intro/README.md | 4 - material/ngrx-signal-store/README.md | 24 ++--- material/ngrx-signal-store/demo/README.md | 2 +- .../ngrx-signal-store/demo/package-lock.json | 42 +-------- material/ngrx-signal-store/demo/package.json | 2 - .../demo/src/app/book-list/book-list.html | 8 +- .../demo/src/app/book-list/book-list.spec.ts | 87 ++++++++++++++----- .../demo/src/app/book-list/book-list.ts | 8 +- .../demo/src/app/shared/error-message.ts | 6 +- 20 files changed, 199 insertions(+), 172 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index 7dc042bd..bc3afcec 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -17,7 +17,7 @@ Dieser Artikel ist **Teil 2** einer dreiteiligen Serie zum Thema State Managemen Im ersten Teil haben wir uns Schritt für Schritt ein eigenes Modell für zentrales State Management erarbeitet und dabei die Grundprinzipien von Redux kennengelernt. Jetzt setzen wir diese Ideen mit dem Framework NgRx in die Praxis um. -> **Hinweis zur Version:** Wir verwenden in diesem Artikel **Angular 22** mit der dazu passenden NgRx-Version (NgRx folgt dem Major-Versionsschema von Angular). Wir setzen durchgängig auf Standalone APIs und Signals: Den Store richten wir ausschließlich über die `provide…`-Funktionen ein und lesen ihn mit `selectSignal()` als Signal aus. Konstruktor-Injection brauchen wir nicht mehr – auch das offizielle Effect-Schematic erzeugt seine Abhängigkeiten inzwischen mit `inject()`. +> **Hinweis zur Version:** Wir verwenden in diesem Artikel **Angular 22**. NgRx folgt normalerweise dem Major-Versionsschema von Angular; zum Zeitpunkt der Veröffentlichung ist jedoch **NgRx 21** die aktuelle Version, die problemlos unter Angular 22 läuft (in den Beispiel-Apps per `legacy-peer-deps` installiert). Wir setzen durchgängig auf Standalone APIs und Signals: Den Store richten wir ausschließlich über die `provide…`-Funktionen ein und lesen ihn mit `selectSignal()` als Signal aus. Konstruktor-Injection brauchen wir nicht mehr – auch das offizielle Effect-Schematic erzeugt seine Abhängigkeiten inzwischen mit `inject()`. ## NgRx: Reactive State for Angular @@ -717,6 +717,16 @@ export const selectBooksError = createSelector( ); ``` +Damit Nutzerinnen und Nutzer eine Fehlermeldung auch aktiv wegklicken können, ergänzen wir eine schlanke `clearError`-Action und behandeln sie im Reducer: + +```ts +// books/store/book.actions.ts +export const clearError = createAction('[Book] Clear Error'); + +// books/store/book.reducer.ts (Ergänzung in createReducer) +on(BookActions.clearError, (state): State => ({ ...state, error: null })) +``` + ```ts // books/book-list/book-list.component.ts import { Component, inject } from '@angular/core'; @@ -738,11 +748,13 @@ export class BookListComponent { this.store.dispatch(BookActions.loadBooks()); } - addBook(isbn: string, title: string): void { - if (!isbn || !title) { + addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { + if (!isbn.value || !title.value) { return; } - this.store.dispatch(BookActions.createBook({ book: { isbn, title, rating: 0 } })); + this.store.dispatch(BookActions.createBook({ book: { isbn: isbn.value, title: title.value, rating: 0 } })); + isbn.value = ''; + title.value = ''; } rateUp(book: Book): void { @@ -753,23 +765,28 @@ export class BookListComponent { deleteBook(isbn: string): void { this.store.dispatch(BookActions.deleteBook({ isbn })); } + + clearError(): void { + this.store.dispatch(BookActions.clearError()); + } } ``` -Im Template zeigen wir die Fehlermeldung an, bieten ein kleines Formular zum Anlegen und lösen Ändern (Bewertung erhöhen) sowie Löschen über Buttons aus. Die Eingabefelder lesen wir bewusst ohne `FormsModule` über lokale Template-Referenzen (`#isbnEl`) aus, um den Fokus auf NgRx zu behalten: +Im Template zeigen wir die Fehlermeldung (mit einem Button zum Wegklicken) an, bieten ein kleines Formular zum Anlegen und lösen Ändern (Bewertung erhöhen) sowie Löschen über Buttons aus. Die Eingabefelder reichen wir bewusst ohne `FormsModule` als lokale Template-Referenzen (`#isbnEl`) direkt an die Methode weiter, die nach dem Dispatch die Felder leert – so bleibt der Fokus auf NgRx: ```html @if (error(); as error) { -
              {{ error }}
              +
              + {{ error }} + +
              }
              - - - + + +
                @@ -1018,7 +1035,7 @@ Auf diese Weise können wir die Verwaltung von Entitäten im State sehr effizien Alle auf Grundlage von NgRx entwickelten Bausteine sollten auch getestet werden. Dafür möchten wir in diesem Abschnitt einige Hinweise geben. Grundsätzlich werden bei der Initialisierung mit den Schematics von NgRx bereits Grundgerüste für die Unit-Tests angelegt – wir können also direkt loslegen. -> **Hinweis:** Die folgenden Listings zeigen die grundsätzlichen Testtechniken. Die lauffähige Beispiel-App im Ordner `demo/` enthält vollständige, mit **Vitest** umgesetzte Tests für Reducer, Selektoren, Effects und Komponente. Die `hot`/`cold`-Marbles und `spyOn()` aus den folgenden Beispielen setzen ein Jasmine-Setup voraus; mit Vitest stehen die äquivalenten Helfer unter `vi` bzw. über `vitest-marbles` bereit. +> **Hinweis:** Die folgenden Listings zeigen die grundsätzlichen Testtechniken. Die lauffähige Beispiel-App im Ordner `demo/` enthält vollständige, mit **Vitest** umgesetzte Tests für Reducer, Selektoren, Effects und Komponente. Die `hot`/`cold`-Marbles und `spyOn()` aus den folgenden Beispielen setzen ein Jasmine-Setup voraus; in einem Vitest-Projekt würde man stattdessen `vi.spyOn()` und z. B. das Paket `vitest-marbles` verwenden. Die Beispiel-App selbst vergleicht die Datenströme bewusst direkt (ohne Marbles). #### Actions @@ -1060,7 +1077,7 @@ import { selectAllBooks } from './book.selectors'; import { Book } from '../../shared/book'; // kleine Hilfsfunktion, um schnell Testdaten zu erzeugen -const book = (isbn: string): Book => ({ isbn, title: `Titel ${isbn}`, rating: 0 }); +const book = (isbn: string): Book => ({ isbn, title: `Title ${isbn}`, rating: 0 }); describe('Book Selectors', () => { it('should select all books', () => { diff --git a/material/ngrx-global-store/demo/README.md b/material/ngrx-global-store/demo/README.md index 34269df9..2dd401e3 100644 --- a/material/ngrx-global-store/demo/README.md +++ b/material/ngrx-global-store/demo/README.md @@ -9,7 +9,7 @@ Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie B > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. -> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/store/…`); hier liegen die Dateien direkt unter `src/app/store/…`. +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/store/…`, Komponente `BookListComponent`, Selektor `bm-book-list`); die Demo nutzt dagegen die flachen Angular-CLI-Standardnamen (`src/app/store/…`, `BookList`, `app-book-list`). ## Befehle diff --git a/material/ngrx-global-store/demo/package-lock.json b/material/ngrx-global-store/demo/package-lock.json index 1ffdceb8..870f44d6 100644 --- a/material/ngrx-global-store/demo/package-lock.json +++ b/material/ngrx-global-store/demo/package-lock.json @@ -11,9 +11,7 @@ "@angular/common": "^22.0.1", "@angular/compiler": "^22.0.1", "@angular/core": "^22.0.1", - "@angular/forms": "^22.0.1", "@angular/platform-browser": "^22.0.1", - "@angular/router": "^22.0.1", "@ngrx/effects": "^21.1.1", "@ngrx/store": "^21.1.1", "rxjs": "~7.8.0", @@ -1458,26 +1456,6 @@ } } }, - "node_modules/@angular/forms": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.0.1.tgz", - "integrity": "sha512-9an74j0DtSeY4XMp5crkAdlxmhdRNq6cKlN9dxGch6udEr2SjHDP7hYoNXJ6y5yna5FEa/t+1SltWF0OVPIVxw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "tslib": "^2.3.0", - "zod": "^4.0.10" - }, - "engines": { - "node": "^22.22.3 || ^24.15.0 || >=26.0.0" - }, - "peerDependencies": { - "@angular/common": "22.0.1", - "@angular/core": "22.0.1", - "@angular/platform-browser": "22.0.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@angular/platform-browser": { "version": "22.0.1", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.1.tgz", @@ -1500,24 +1478,6 @@ } } }, - "node_modules/@angular/router": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.0.1.tgz", - "integrity": "sha512-NHMkRAgBMKrE7g5lSe1H/espe0NX8axVBy6TtzEh/l7yxTnZiqUIjmRnIDb+LFGPRQVgDu3FxR/fvwQaosOn0w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^22.22.3 || ^24.15.0 || >=26.0.0" - }, - "peerDependencies": { - "@angular/common": "22.0.1", - "@angular/core": "22.0.1", - "@angular/platform-browser": "22.0.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", @@ -4488,6 +4448,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, "license": "MIT" }, "node_modules/@tufjs/canonical-json": { @@ -8999,6 +8960,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/material/ngrx-global-store/demo/package.json b/material/ngrx-global-store/demo/package.json index 3060e811..c8688d7b 100644 --- a/material/ngrx-global-store/demo/package.json +++ b/material/ngrx-global-store/demo/package.json @@ -14,9 +14,7 @@ "@angular/common": "^22.0.1", "@angular/compiler": "^22.0.1", "@angular/core": "^22.0.1", - "@angular/forms": "^22.0.1", "@angular/platform-browser": "^22.0.1", - "@angular/router": "^22.0.1", "@ngrx/effects": "^21.1.1", "@ngrx/store": "^21.1.1", "rxjs": "~7.8.0", diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.html b/material/ngrx-global-store/demo/src/app/book-list/book-list.html index f4913e18..5a0f58a4 100644 --- a/material/ngrx-global-store/demo/src/app/book-list/book-list.html +++ b/material/ngrx-global-store/demo/src/app/book-list/book-list.html @@ -5,15 +5,16 @@

                Books ({{ books().length }})

                } @if (error(); as error) { -
                {{ error }}
                +
                + {{ error }} + +
                }
                - - - + + +
                  diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts b/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts index d974f83f..53501b83 100644 --- a/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts +++ b/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts @@ -7,11 +7,12 @@ import * as BookActions from '../store/book.actions'; import { selectAllBooks, selectBooksError, selectBooksLoading } from '../store/book.selectors'; import { Book } from '../shared/book'; -const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title, rating: 0 }); +const b = (isbn: string, title = `Title ${isbn}`, rating = 0): Book => ({ isbn, title, rating }); interface Setup { fixture: ComponentFixture; dispatch: MockInstance<(action: unknown) => void>; + el: HTMLElement; } describe('BookList', () => { @@ -33,7 +34,7 @@ describe('BookList', () => { const dispatch = vi.spyOn(store, 'dispatch'); const fixture = TestBed.createComponent(BookList); await fixture.whenStable(); - return { fixture, dispatch }; + return { fixture, dispatch, el: fixture.nativeElement }; } it('dispatcht loadBooks beim Erzeugen', async () => { @@ -42,27 +43,61 @@ describe('BookList', () => { }); it('rendert die Buchliste', async () => { - const { fixture } = await setup([b('1', 'Eins'), b('2', 'Zwei')]); - const items = fixture.nativeElement.querySelectorAll('li'); + const { el } = await setup([b('1', 'Eins'), b('2', 'Zwei')]); + const items = el.querySelectorAll('li'); expect(items.length).toBe(2); expect(items[0].textContent).toContain('Eins'); }); it('zeigt den Ladeindikator', async () => { - const { fixture } = await setup([], true); - expect(fixture.nativeElement.querySelector('.loader')).toBeTruthy(); + const { el } = await setup([], true); + expect(el.querySelector('.loader')).toBeTruthy(); }); - it('zeigt eine Fehlermeldung', async () => { - const { fixture } = await setup([], false, 'Kaputt'); - expect(fixture.nativeElement.querySelector('.error')?.textContent).toContain('Kaputt'); + it('zeigt eine Fehlermeldung und dispatcht clearError beim Klick auf OK', async () => { + const { el, dispatch } = await setup([], false, 'Kaputt'); + expect(el.querySelector('.error')?.textContent).toContain('Kaputt'); + dispatch.mockClear(); + (el.querySelector('.error button') as HTMLButtonElement).click(); + expect(dispatch).toHaveBeenCalledWith(BookActions.clearError()); + }); + + it('dispatcht createBook beim Anlegen und leert die Felder', async () => { + const { el, dispatch } = await setup(); + dispatch.mockClear(); + const [isbn, title] = el.querySelectorAll('.add-form input'); + isbn.value = '111-1'; + title.value = 'Neues Buch'; + (el.querySelector('.add-form button') as HTMLButtonElement).click(); + expect(dispatch).toHaveBeenCalledWith( + BookActions.createBook({ book: { isbn: '111-1', title: 'Neues Buch', rating: 0 } }) + ); + expect(isbn.value).toBe(''); + expect(title.value).toBe(''); + }); + + it('legt nichts an, wenn die Felder leer sind', async () => { + const { el, dispatch } = await setup(); + dispatch.mockClear(); + (el.querySelector('.add-form button') as HTMLButtonElement).click(); + expect(dispatch).not.toHaveBeenCalled(); + }); + + it('rateUp erhöht die Bewertung und deckelt sie bei 5', async () => { + const { el, dispatch } = await setup([b('1', 'Eins', 4), b('2', 'Zwei', 5)]); + dispatch.mockClear(); + const rateButtons = el.querySelectorAll('li button:first-of-type'); + rateButtons[0].click(); // 4 -> 5 + expect(dispatch).toHaveBeenCalledWith(BookActions.updateBook({ book: { isbn: '1', title: 'Eins', rating: 5 } })); + dispatch.mockClear(); + rateButtons[1].click(); // 5 -> bleibt 5 + expect(dispatch).toHaveBeenCalledWith(BookActions.updateBook({ book: { isbn: '2', title: 'Zwei', rating: 5 } })); }); it('dispatcht deleteBook beim Klick auf Löschen', async () => { - const { fixture, dispatch } = await setup([b('1')]); + const { el, dispatch } = await setup([b('1')]); dispatch.mockClear(); - const deleteButton = fixture.nativeElement.querySelector('li button:last-child') as HTMLButtonElement; - deleteButton.click(); + (el.querySelector('li button:last-child') as HTMLButtonElement).click(); expect(dispatch).toHaveBeenCalledWith(BookActions.deleteBook({ isbn: '1' })); }); }); diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.ts b/material/ngrx-global-store/demo/src/app/book-list/book-list.ts index c34098ce..8f0c49e9 100644 --- a/material/ngrx-global-store/demo/src/app/book-list/book-list.ts +++ b/material/ngrx-global-store/demo/src/app/book-list/book-list.ts @@ -21,11 +21,13 @@ export class BookList { this.store.dispatch(BookActions.loadBooks()); } - addBook(isbn: string, title: string): void { - if (!isbn || !title) { + addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { + if (!isbn.value || !title.value) { return; } - this.store.dispatch(BookActions.createBook({ book: { isbn, title, rating: 0 } })); + this.store.dispatch(BookActions.createBook({ book: { isbn: isbn.value, title: title.value, rating: 0 } })); + isbn.value = ''; + title.value = ''; } rateUp(book: Book): void { @@ -36,4 +38,8 @@ export class BookList { deleteBook(isbn: string): void { this.store.dispatch(BookActions.deleteBook({ isbn })); } + + clearError(): void { + this.store.dispatch(BookActions.clearError()); + } } diff --git a/material/ngrx-global-store/demo/src/app/shared/error-message.ts b/material/ngrx-global-store/demo/src/app/shared/error-message.ts index 0021b504..ca813472 100644 --- a/material/ngrx-global-store/demo/src/app/shared/error-message.ts +++ b/material/ngrx-global-store/demo/src/app/shared/error-message.ts @@ -3,9 +3,9 @@ * sicher in eine lesbare Meldung um. Der Fehler ist als `unknown` typisiert – * wir prüfen die Form, statt sie blind anzunehmen. * - * Bei einem echten HttpClient-Backend wäre der Fehler in der Regel eine - * `HttpErrorResponse`, die ebenfalls eine `message`-Eigenschaft trägt; die - * Prüfung ließe sich dafür leicht erweitern. + * Hinweis: Eine `HttpErrorResponse` ist KEIN `instanceof Error` und liefe hier + * in den Fallback. Für ein echtes HttpClient-Backend würden wir die Prüfung + * erweitern, z. B. `if (error instanceof HttpErrorResponse) return error.message;`. */ export function toMessage(error: unknown): string { return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; diff --git a/material/ngrx-global-store/demo/src/app/store/book.actions.ts b/material/ngrx-global-store/demo/src/app/store/book.actions.ts index 63a95e95..d46d170e 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.actions.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.actions.ts @@ -5,6 +5,8 @@ export const loadBooks = createAction('[Book] Load Books'); export const loadBooksSuccess = createAction('[Book] Load Books Success', props<{ data: Book[] }>()); export const loadBooksFailure = createAction('[Book] Load Books Failure', props<{ error: string }>()); +export const clearError = createAction('[Book] Clear Error'); + export const createBook = createAction('[Book] Create Book', props<{ book: Book }>()); export const createBookSuccess = createAction('[Book] Create Book Success', props<{ book: Book }>()); export const createBookFailure = createAction('[Book] Create Book Failure', props<{ error: string }>()); diff --git a/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts index dc7250d8..f30ad9d3 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts @@ -26,6 +26,11 @@ describe('Book Reducer', () => { expect(state.error).toBe('boom'); }); + it('clearError setzt die Fehlermeldung zurück', () => { + const state = reducer({ ...initialState, error: 'boom' }, BookActions.clearError()); + expect(state.error).toBeNull(); + }); + it('createBookSuccess hängt ein Buch immutabel an', () => { const start: State = { ...initialState, books: [b('1')] }; const state = reducer(start, BookActions.createBookSuccess({ book: b('2') })); diff --git a/material/ngrx-global-store/demo/src/app/store/book.reducer.ts b/material/ngrx-global-store/demo/src/app/store/book.reducer.ts index 35b72ea7..d3c1edd8 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.reducer.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.reducer.ts @@ -31,6 +31,8 @@ export const reducer = createReducer( error: action.error })), + on(BookActions.clearError, (state): State => ({ ...state, error: null })), + // Schreibvorgänge: beim Auslösen eine alte Fehlermeldung zurücksetzen. on( BookActions.createBook, diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md index b15ff901..0e463390 100644 --- a/material/ngrx-intro/README.md +++ b/material/ngrx-intro/README.md @@ -15,10 +15,6 @@ Dieser Artikel ist **Teil 1** einer dreiteiligen Serie zum Thema State Managemen [[toc]] -> NgRx provides robust state management for small and large projects. It enforces proper separation of concerns. Using it from the start reduces the risk of spaghetti when the project evolves. -> -> – Minko Gechev, Mitglied des Angular-Teams - Wir haben unsere Anwendung bisher stets komponentenzentriert und serviceorientiert aufgebaut: Die Komponenten unserer Anwendung kommunizieren auf klar definierten Wegen über Property Bindings und Event Bindings. Um Daten zu erhalten und zu senden, nutzen die Komponenten verschiedene Services, in denen die HTTP-Kommunikation gekapselt ist oder über die wir Daten austauschen können. Diese Herangehensweise funktioniert im Prinzip sehr gut, und wir haben so eine vollständige Anwendung entwickeln können. Unsere Beispielanwendung ist allerdings vergleichsweise klein und übersichtlich – in der Praxis werden die Anwendungen wesentlich größer: Viele Komponenten greifen dann gleichzeitig auf geteilte Daten zu und nutzen dieselben Services. Auch die Performance spielt eine immer größere Rolle, je komplexer die Anwendung wird. Wir erreichen mit der bisher vorgestellten Herangehensweise schnell einen Punkt, an dem wir den Überblick über die Kommunikationswege verlieren. Es kommt immer häufiger zu unerklärlichen Konstellationen, da man nicht mehr nachvollziehen kann, welche Komponente andere Komponenten oder Services aufruft und in welcher Reihenfolge dies geschieht. Gleichzeitig führen die vielen Kommunikationswege zu entsprechend vielen Änderungen an den Daten, die von der Change Detection erkannt und verarbeitet werden müssen. Kurzum: Die Anwendung wird zunehmend schwerfälliger. diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index 1b6c1496..d5d0dd14 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -19,7 +19,7 @@ In [Teil 2](/material/ngrx-global-store) haben wir den **Global Store** kennenge Der Global Store ist mächtig, bringt für viele Anwendungsfälle aber spürbar viel Zeremonie mit: Schon das Laden einer simplen Buchliste verteilt sich über Actions, einen Reducer, Selektoren und einen Effect. Genau hier setzt der **SignalStore** aus dem Paket `@ngrx/signals` an. Er ist eine leichtgewichtige, signal-basierte Alternative, die mit deutlich weniger Code auskommt und sich nahtlos in modernes, signal-zentriertes Angular einfügt. -> **Hinweis zur Version:** Wir verwenden **Angular 22** und die dazu passende NgRx-Version. Der SignalStore wohnt im Paket `@ngrx/signals` und wird unabhängig vom Global Store (`@ngrx/store`) installiert. +> **Hinweis zur Version:** Wir verwenden **Angular 22** mit **NgRx 21** – zum Zeitpunkt der Veröffentlichung die aktuelle NgRx-Version, die problemlos unter Angular 22 läuft (in der Beispiel-App per `legacy-peer-deps` installiert). Der SignalStore wohnt im Paket `@ngrx/signals` und wird unabhängig vom Global Store (`@ngrx/store`) installiert. ## Der architektonische Unterschied zum Global Store @@ -279,7 +279,7 @@ updateBook: rxMethod( tapResponse({ next: updated => patchState(store, state => ({ - books: state.books.map(b => b.isbn === updated.isbn ? updated : b) + books: state.books.map(b => (b.isbn === updated.isbn ? updated : b)) })), error: (err: unknown) => patchState(store, { error: toMessage(err) }) }) @@ -443,11 +443,13 @@ import { Book } from '../shared/book'; export class BookListComponent { protected store = inject(BookStore); - addBook(isbn: string, title: string): void { - if (!isbn || !title) { + addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { + if (!isbn.value || !title.value) { return; } - this.store.addBook({ isbn, title, rating: 0 }); + this.store.addBook({ isbn: isbn.value, title: title.value, rating: 0 }); + isbn.value = ''; + title.value = ''; } rateUp(book: Book): void { @@ -479,11 +481,9 @@ Im Template lesen wir die Signale und lösen über Events die Methoden aus: ein }
                  - - - + + +
                    @@ -497,7 +497,7 @@ Im Template lesen wir die Signale und lösen über Events die Methoden aus: ein
                  ``` -Die Eingabefelder lesen wir bewusst ohne `FormsModule` über lokale Template-Referenzen (`#isbnEl`) aus, um den Fokus auf den SignalStore zu behalten. Weil der Store global (`providedIn: 'root'`) bereitsteht, teilen sich alle Komponenten dieselbe Instanz: Eine separate Detail- oder Formularkomponente, die `store.addBook()` aufruft, verändert denselben State, den die Liste anzeigt – die Oberfläche aktualisiert sich dank der Signale automatisch. +Die Eingabefelder reichen wir bewusst ohne `FormsModule` als lokale Template-Referenzen (`#isbnEl`) direkt an die Methode weiter, die nach dem Anlegen die Felder leert – so bleibt der Fokus auf dem SignalStore. Weil der Store global (`providedIn: 'root'`) bereitsteht, teilen sich alle Komponenten dieselbe Instanz: Eine separate Detail- oder Formularkomponente, die `store.addBook()` aufruft, verändert denselben State, den die Liste anzeigt – die Oberfläche aktualisiert sich dank der Signale automatisch. ### Weniger Boilerplate: Entitäten mit `withEntities` @@ -612,7 +612,7 @@ import { BookStoreService } from '../shared/book-store.service'; import { Book } from '../shared/book'; // kleine Hilfsfunktion, um schnell Testdaten zu erzeugen -const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ isbn, title }); +const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); describe('BookStore', () => { it('lädt Bücher und zählt sie', () => { diff --git a/material/ngrx-signal-store/demo/README.md b/material/ngrx-signal-store/demo/README.md index 7f01c26c..805fe419 100644 --- a/material/ngrx-signal-store/demo/README.md +++ b/material/ngrx-signal-store/demo/README.md @@ -9,7 +9,7 @@ Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. -> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/…`); hier liegt der Store direkt unter `src/app/book.store.ts`. +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/…`, Komponente `BookListComponent`, Selektor `bm-book-list`); die Demo nutzt dagegen die flachen Angular-CLI-Standardnamen (`src/app/book.store.ts`, `BookList`, `app-book-list`). ## Befehle diff --git a/material/ngrx-signal-store/demo/package-lock.json b/material/ngrx-signal-store/demo/package-lock.json index 11d4547e..9ec69226 100644 --- a/material/ngrx-signal-store/demo/package-lock.json +++ b/material/ngrx-signal-store/demo/package-lock.json @@ -11,9 +11,7 @@ "@angular/common": "^22.0.1", "@angular/compiler": "^22.0.1", "@angular/core": "^22.0.1", - "@angular/forms": "^22.0.1", "@angular/platform-browser": "^22.0.1", - "@angular/router": "^22.0.1", "@ngrx/operators": "^21.1.1", "@ngrx/signals": "^21.1.1", "rxjs": "~7.8.0", @@ -1458,26 +1456,6 @@ } } }, - "node_modules/@angular/forms": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.0.1.tgz", - "integrity": "sha512-9an74j0DtSeY4XMp5crkAdlxmhdRNq6cKlN9dxGch6udEr2SjHDP7hYoNXJ6y5yna5FEa/t+1SltWF0OVPIVxw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "tslib": "^2.3.0", - "zod": "^4.0.10" - }, - "engines": { - "node": "^22.22.3 || ^24.15.0 || >=26.0.0" - }, - "peerDependencies": { - "@angular/common": "22.0.1", - "@angular/core": "22.0.1", - "@angular/platform-browser": "22.0.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@angular/platform-browser": { "version": "22.0.1", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.1.tgz", @@ -1500,24 +1478,6 @@ } } }, - "node_modules/@angular/router": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.0.1.tgz", - "integrity": "sha512-NHMkRAgBMKrE7g5lSe1H/espe0NX8axVBy6TtzEh/l7yxTnZiqUIjmRnIDb+LFGPRQVgDu3FxR/fvwQaosOn0w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^22.22.3 || ^24.15.0 || >=26.0.0" - }, - "peerDependencies": { - "@angular/common": "22.0.1", - "@angular/core": "22.0.1", - "@angular/platform-browser": "22.0.1", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", @@ -4491,6 +4451,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, "license": "MIT" }, "node_modules/@tufjs/canonical-json": { @@ -9002,6 +8963,7 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz", "integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/material/ngrx-signal-store/demo/package.json b/material/ngrx-signal-store/demo/package.json index a6fcc69a..0507a5bd 100644 --- a/material/ngrx-signal-store/demo/package.json +++ b/material/ngrx-signal-store/demo/package.json @@ -14,9 +14,7 @@ "@angular/common": "^22.0.1", "@angular/compiler": "^22.0.1", "@angular/core": "^22.0.1", - "@angular/forms": "^22.0.1", "@angular/platform-browser": "^22.0.1", - "@angular/router": "^22.0.1", "@ngrx/operators": "^21.1.1", "@ngrx/signals": "^21.1.1", "rxjs": "~7.8.0", diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.html b/material/ngrx-signal-store/demo/src/app/book-list/book-list.html index a9003530..45ee80f4 100644 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.html +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.html @@ -12,11 +12,9 @@

                  Books ({{ store.booksCount() }})

                  }
                  - - - + + +
                    diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts b/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts index d93e7c52..93dbf0f3 100644 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts @@ -1,27 +1,27 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { of } from 'rxjs'; +import { of, throwError } from 'rxjs'; import { BookList } from './book-list'; import { BookStoreService } from '../shared/book-store.service'; import { Book } from '../shared/book'; -const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title, rating: 0 }); +const b = (isbn: string, title = `Title ${isbn}`, rating = 0): Book => ({ isbn, title, rating }); + +function serviceMock(books: Book[], overrides: Partial = {}): Partial { + return { + getAll: () => of(books), + create: (book: Book) => of(book), + update: (book: Book) => of(book), + remove: () => of(undefined), + ...overrides + }; +} describe('BookList (SignalStore)', () => { - async function setup(books: Book[] = []): Promise> { + async function setup(mock: Partial): Promise> { await TestBed.configureTestingModule({ imports: [BookList], - providers: [ - { - provide: BookStoreService, - useValue: { - getAll: () => of(books), - create: (book: Book) => of(book), - update: (book: Book) => of(book), - remove: () => of(undefined) - } - } - ] + providers: [{ provide: BookStoreService, useValue: mock }] }).compileComponents(); const fixture = TestBed.createComponent(BookList); @@ -30,19 +30,62 @@ describe('BookList (SignalStore)', () => { } it('rendert die vom Store geladenen Bücher', async () => { - const fixture = await setup([b('1', 'Eins'), b('2', 'Zwei')]); - const items = fixture.nativeElement.querySelectorAll('li'); - expect(items.length).toBe(2); - expect(items[0].textContent).toContain('Eins'); + const fixture = await setup(serviceMock([b('1', 'Eins'), b('2', 'Zwei')])); + const el = fixture.nativeElement as HTMLElement; + expect(el.querySelectorAll('li').length).toBe(2); + expect(el.querySelector('li')?.textContent).toContain('Eins'); + }); + + it('legt ein Buch an und leert die Felder', async () => { + const fixture = await setup(serviceMock([b('1', 'Eins')])); + const el = fixture.nativeElement as HTMLElement; + const [isbn, title] = el.querySelectorAll('.add-form input'); + isbn.value = '2'; + title.value = 'Zwei'; + (el.querySelector('.add-form button') as HTMLButtonElement).click(); + await fixture.whenStable(); + expect(el.querySelectorAll('li').length).toBe(2); + expect(isbn.value).toBe(''); + }); + + it('legt nichts an, wenn die Felder leer sind', async () => { + const fixture = await setup(serviceMock([b('1', 'Eins')])); + const el = fixture.nativeElement as HTMLElement; + (el.querySelector('.add-form button') as HTMLButtonElement).click(); + await fixture.whenStable(); + expect(el.querySelectorAll('li').length).toBe(1); + }); + + it('erhöht die Bewertung und deckelt sie bei 5', async () => { + const fixture = await setup(serviceMock([b('1', 'Eins', 4)])); + const el = fixture.nativeElement as HTMLElement; + const rateButton = el.querySelector('li button:first-of-type') as HTMLButtonElement; + + rateButton.click(); // 4 -> 5 + await fixture.whenStable(); + expect(el.querySelector('li')?.textContent).toContain('★ 5'); + + rateButton.click(); // bleibt bei 5 + await fixture.whenStable(); + expect(el.querySelector('li')?.textContent).toContain('★ 5'); }); it('entfernt ein Buch beim Klick auf Löschen', async () => { - const fixture = await setup([b('1', 'Eins')]); - const deleteButton = fixture.nativeElement.querySelector('li button:last-child') as HTMLButtonElement; + const fixture = await setup(serviceMock([b('1', 'Eins')])); + const el = fixture.nativeElement as HTMLElement; + (el.querySelector('li button:last-child') as HTMLButtonElement).click(); + await fixture.whenStable(); + expect(el.querySelectorAll('li').length).toBe(0); + }); + + it('blendet eine Fehlermeldung beim Klick auf OK wieder aus (clearError)', async () => { + const fixture = await setup(serviceMock([], { getAll: () => throwError(() => new Error('Kaputt')) })); + const el = fixture.nativeElement as HTMLElement; + expect(el.querySelector('.error')?.textContent).toContain('Kaputt'); - deleteButton.click(); + (el.querySelector('.error button') as HTMLButtonElement).click(); await fixture.whenStable(); - expect(fixture.nativeElement.querySelectorAll('li').length).toBe(0); + expect(el.querySelector('.error')).toBeNull(); }); }); diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts b/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts index aa284f46..bc3882d4 100644 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts @@ -10,11 +10,13 @@ import { Book } from '../shared/book'; export class BookList { protected store = inject(BookStore); - addBook(isbn: string, title: string): void { - if (!isbn || !title) { + addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { + if (!isbn.value || !title.value) { return; } - this.store.addBook({ isbn, title, rating: 0 }); + this.store.addBook({ isbn: isbn.value, title: title.value, rating: 0 }); + isbn.value = ''; + title.value = ''; } rateUp(book: Book): void { diff --git a/material/ngrx-signal-store/demo/src/app/shared/error-message.ts b/material/ngrx-signal-store/demo/src/app/shared/error-message.ts index 08bbec65..d43d1708 100644 --- a/material/ngrx-signal-store/demo/src/app/shared/error-message.ts +++ b/material/ngrx-signal-store/demo/src/app/shared/error-message.ts @@ -3,9 +3,9 @@ * lesbare Meldung um. Der Fehler ist als `unknown` typisiert – wir prüfen die * Form, statt sie blind anzunehmen. * - * Bei einem echten HttpClient-Backend wäre der Fehler in der Regel eine - * `HttpErrorResponse`, die ebenfalls eine `message`-Eigenschaft trägt; die - * Prüfung ließe sich dafür leicht erweitern. + * Hinweis: Eine `HttpErrorResponse` ist KEIN `instanceof Error` und liefe hier + * in den Fallback. Für ein echtes HttpClient-Backend würden wir die Prüfung + * erweitern, z. B. `if (error instanceof HttpErrorResponse) return error.message;`. */ export function toMessage(error: unknown): string { return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; From 455506efaa8b3c7131b45fb34ac3473931d42422 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Mon, 15 Jun 2026 06:51:17 +0200 Subject: [PATCH 18/24] =?UTF-8?q?material(ngrx):=20Angular-Namenskonventio?= =?UTF-8?q?n=20=E2=80=93=20.component/.service-Suffixe=20entfernt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Angular-Style-Guide hat die Typ-Suffixe abgeschafft (Datei- UND Klassennamen): - Komponenten in den Artikeln: BookListComponent -> BookList, book-list.component.{ts,html,spec.ts} -> book-list.{ts,html,spec.ts} (die Demos waren hier bereits flach) - Datenservice überall: book-store.service.ts -> book-api.ts, Klasse BookStoreService -> BookApi (Methoden getAll/create/update/remove unverändert). Der kurze, suffixfreie Name vermeidet zudem die Kollision mit dem NgRx-SignalStore BookStore in Teil 3. - Artikel, beide Demos und alle Specs angeglichen; Demo-READMEs aktualisiert (verbleibende Unterschiede: Feature-Ordner books/ vs. flach, Präfix bm- vs. app-). Beweis: Global 31/31, Signal 16/16 Vitest gruen; beide bauen (148 kB / 126 kB). --- material/ngrx-global-store/README.md | 62 +++++++++---------- material/ngrx-global-store/demo/README.md | 4 +- .../{book-store.service.ts => book-api.ts} | 2 +- .../demo/src/app/store/book.effects.spec.ts | 6 +- .../demo/src/app/store/book.effects.ts | 4 +- material/ngrx-intro/README.md | 2 +- material/ngrx-signal-store/README.md | 30 ++++----- material/ngrx-signal-store/demo/README.md | 4 +- .../demo/src/app/app.spec.ts | 4 +- .../demo/src/app/book-list/book-list.spec.ts | 8 +-- .../demo/src/app/book.store.spec.ts | 8 +-- .../demo/src/app/book.store.ts | 4 +- .../{book-store.service.ts => book-api.ts} | 2 +- 13 files changed, 70 insertions(+), 70 deletions(-) rename material/ngrx-global-store/demo/src/app/shared/{book-store.service.ts => book-api.ts} (97%) rename material/ngrx-signal-store/demo/src/app/shared/{book-store.service.ts => book-api.ts} (97%) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index bc3afcec..4683cf11 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -270,7 +270,7 @@ export const loadBooksFailure = createAction( Um mit dem Store zu kommunizieren und Zustandsänderungen anzustoßen, müssen die Actions von den Komponenten in den Store gesendet werden. Den Store fordern wir dazu mit der Funktion `inject()` in der Komponente an. -Der Store verfügt über eine Methode `dispatch()`, mit der wir eine Action in den Store dispatchen können. Beim Aufruf der `BookListComponent` soll das Laden der Buchliste angestoßen werden. Deshalb lösen wir dort gleich im Konstruktor die Action `loadBooks` aus. +Der Store verfügt über eine Methode `dispatch()`, mit der wir eine Action in den Store dispatchen können. Beim Aufruf der `BookList` soll das Laden der Buchliste angestoßen werden. Deshalb lösen wir dort gleich im Konstruktor die Action `loadBooks` aus. Wichtig ist, dass das exportierte `loadBooks` aus der Datei `book.actions.ts` selbst noch keine Action ist, sondern ein Action Creator, der ein Action-Objekt erzeugen kann. Dazu müssen wir die Funktion aufrufen. Hat die Action einen Payload, so übergeben wir ihn als Argument an den Action Creator: @@ -282,14 +282,14 @@ const mySuccessAction = loadBooksSuccess({ data: [/* ... */] }); Wir müssen die Funktion `loadBooks` also aufrufen, um ein Action-Objekt zu erhalten, das wir dispatchen können: ```ts -// books/book-list/book-list.component.ts +// books/book-list/book-list.ts import { Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; import { loadBooks } from '../store/book.actions'; @Component({ /* ... */ }) -export class BookListComponent { +export class BookList { private store = inject(Store); constructor() { @@ -367,7 +367,7 @@ Für alle unbekannten Actions liefert der Reducer automatisch den aktuellen Stat ### Selektoren: Daten aus dem State lesen -Fassen wir kurz zusammen, wie weit wir bisher gekommen sind: Wir haben Action Creators definiert und die Action `loadBooks` von der `BookListComponent` aus in den Store dispatcht. Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. +Fassen wir kurz zusammen, wie weit wir bisher gekommen sind: Wir haben Action Creators definiert und die Action `loadBooks` von der `BookList` aus in den Store dispatcht. Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. Um den Kreislauf des Datenflusses zu schließen, wollen wir die Daten aus dem State nun auslesen und in der Komponente darstellen. Der Store gibt dabei stets den vollständigen State aus. Um einzelne Teile daraus zu lesen, benötigen wir eine Projektion. In der Praxis werden diese Lesezugriffe schnell komplexer: Mitunter wollen wir Daten nicht nur einfach auslesen, sondern Projektionen über verschiedene Teile des States ausführen. Stellen wir uns vor, wir besitzen eine Liste von Autoren und Autorinnen und eine Liste von Büchern – wollen aber nun nur die Bücher ausgeben, die von einer bestimmten Person verfasst wurden. Dazu ist zusätzliche Logik nötig, die nicht in die Komponenten gehört. Stattdessen lagern wir diese Logik in separate Funktionen aus, die unabhängig von den Komponenten sind. Wir können eine solche Funktion mit einer Datenbankabfrage vergleichen: Die Query wird einmal definiert und kann beliebig komplex sein. Verschiedene Teile der Anwendung können diese Query nutzen und die Daten genau im benötigten Format erhalten. Die Funktionen zur Abfrage von Daten aus dem Store werden *Selektoren* genannt. @@ -418,16 +418,16 @@ export const selectAllBooks = createSelector( ); ``` -Diese Selektoren lesen wir nun in der `BookListComponent` aus. Der Store stellt dafür die Methode `selectSignal()` bereit: Sie erwartet einen Selektor und liefert ein **Signal** des ausgewählten State-Slice zurück. Im Gegensatz zur Methode `select()`, die ein Observable liefert, erhalten wir mit `selectSignal()` direkt ein Signal – das passt nahtlos in die signal-basierte Welt von modernem Angular und wir benötigen im Template keine `AsyncPipe`. +Diese Selektoren lesen wir nun in der `BookList` aus. Der Store stellt dafür die Methode `selectSignal()` bereit: Sie erwartet einen Selektor und liefert ein **Signal** des ausgewählten State-Slice zurück. Im Gegensatz zur Methode `select()`, die ein Observable liefert, erhalten wir mit `selectSignal()` direkt ein Signal – das passt nahtlos in die signal-basierte Welt von modernem Angular und wir benötigen im Template keine `AsyncPipe`. Das resultierende Signal aktualisiert sich nur dann, wenn sich der selektierte Wert tatsächlich verändert hat. Obwohl der Store bei *jeder* Änderung den gesamten State neu erzeugt, gibt unser `loading`-Signal also nur dann einen neuen Wert aus, wenn sich das `loading`-Flag wirklich geändert hat. Die folgende Abbildung veranschaulicht das: ![Diagramm: Der Store gibt bei jeder Änderung den gesamten State aus (loading wechselt false, true, true, false). Der Selektor store.selectSignal(state => state.book.loading) gibt darunter nur dann einen neuen Wert aus, wenn sich das loading-Flag tatsächlich ändert (false, true, false).](./selectors-gap.svg "Selektor mit store.selectSignal() verwenden") -Wir legen die Signale direkt als Properties in der Komponentenklasse ab. Wenn wir unsere Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. Da wir die Buchliste nun aus dem Store beziehen, benötigen wir den `BookStoreService` in der Komponente nicht mehr. +Wir legen die Signale direkt als Properties in der Komponentenklasse ab. Wenn wir unsere Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. Da wir die Buchliste nun aus dem Store beziehen, benötigen wir den `BookApi` in der Komponente nicht mehr. ```ts -// books/book-list/book-list.component.ts +// books/book-list/book-list.ts import { Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; @@ -435,7 +435,7 @@ import { loadBooks } from '../store/book.actions'; import { selectAllBooks, selectBooksLoading } from '../store/book.selectors'; @Component({ /* ... */ }) -export class BookListComponent { +export class BookList { private store = inject(Store); books = this.store.selectSignal(selectAllBooks); @@ -450,7 +450,7 @@ export class BookListComponent { Im Template der Komponente lesen wir die Signale aus, indem wir sie wie eine Funktion aufrufen (`books()`). Für die Buchliste nutzen wir den nativen Control Flow mit `@for`, für den Ladeindikator `@if`. Unser globales Stylesheet bietet dafür die passende CSS-Klasse `loader` an. ```html - +

                    Books

                      @for (book of books(); track book.isbn) { @@ -487,7 +487,7 @@ Technisch ist ein Effect also immer ein `Observable`. Alle so erzeugten Die Klasse `BookEffects` befindet sich in der Datei `books/store/book.effects.ts`. Ein Effect wird als ein Property in dieser Klasse definiert. Der Name des Properties spielt keine Rolle, sollte aber passend zur Aufgabe benannt werden. Das Ziel ist es, hier ein Observable zu entwickeln, das Actions ausgibt. Jeden Effect kapseln wir mit der Funktion `createEffect()`, sodass er automatisch in den Lebenszyklus von NgRx integriert wird. Ein erster Effect zum Laden von Daten wurde bereits automatisch generiert, weil wir beim Anlegen die Einstellung `--api` verwendet haben. Dieses Grundgerüst wollen wir vervollständigen, um die Buchliste per HTTP zu laden. -In die Klasse holen wir mit `inject()` zwei Abhängigkeiten: den `BookStoreService`, der die HTTP-Kommunikation kapselt, und den Service `Actions`. Über `Actions` erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. Dieser Datenstrom ist meist die Grundlage für unsere Effects. (Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: [5 useful NgRx effects that don't rely on actions](https://angular.schule/blog/2018-06-5-useful-effects-without-actions).) +In die Klasse holen wir mit `inject()` zwei Abhängigkeiten: den `BookApi`, der die HTTP-Kommunikation kapselt, und den Service `Actions`. Über `Actions` erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. Dieser Datenstrom ist meist die Grundlage für unsere Effects. (Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: [5 useful NgRx effects that don't rely on actions](https://angular.schule/blog/2018-06-5-useful-effects-without-actions).) Der Effect `loadBooks$` soll auf die Action `loadBooks` reagieren, den passenden HTTP-Request auslösen und Actions zurück in den Store leiten (Success und Failure). Wir zeigen zunächst den vollständigen Code und gehen die Implementierung anschließend Schritt für Schritt durch: @@ -499,13 +499,13 @@ import { catchError, map, switchMap } from 'rxjs/operators'; import { of } from 'rxjs'; import * as BookActions from './book.actions'; -import { BookStoreService } from '../../shared/book-store.service'; +import { BookApi } from '../../shared/book-api'; import { toMessage } from '../../shared/error-message'; @Injectable() export class BookEffects { private actions$ = inject(Actions); - private service = inject(BookStoreService); + private service = inject(BookApi); loadBooks$ = createEffect(() => { return this.actions$.pipe( @@ -526,7 +526,7 @@ Der Datenfluss beginnt beim Strom aller Actions aus der gesamten Anwendung: `thi Für jede dieser Actions wollen wir nun einen HTTP-Request ausführen und die Ergebnisse verarbeiten. Damit betreten wir erneut das Terrain der Higher-Order Observables. Wir müssen uns sorgfältig für einen der vier Flattening-Operatoren entscheiden. Unsere Wahl fällt hier auf `switchMap()`: Wird die Buchliste noch geladen und währenddessen erneut angefragt, soll nur die zuletzt gesendete Anfrage bearbeitet werden. Wir sollten allerdings nicht immer `switchMap()` in unseren Effects verwenden, sondern sorgfältig abwägen, welcher Flattening-Operator am besten zum jeweiligen Problem passt. -Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookStoreService` mit der Methode `getAll()` einsetzen, die ein Observable zurückgibt. Unser Effect ist nun ein Observable, das ein Array von Büchern liefert. Damit die Buchliste im Store verarbeitet werden kann, müssen wir sie in eine Action verpacken. Wir nutzen dazu den Operator `map()` und wandeln damit die Buchliste um in eine Action `loadBooksSuccess`, die die Liste enthält. +Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookApi` mit der Methode `getAll()` einsetzen, die ein Observable zurückgibt. Unser Effect ist nun ein Observable, das ein Array von Büchern liefert. Damit die Buchliste im Store verarbeitet werden kann, müssen wir sie in eine Action verpacken. Wir nutzen dazu den Operator `map()` und wandeln damit die Buchliste um in eine Action `loadBooksSuccess`, die die Liste enthält. Ähnlich gehen wir für den Fehlerfall vor: Hier nutzen wir den Operator `catchError()`, um einen Fehler in der Ausführung abzufangen. Wir werfen den Fehler allerdings an der Stelle nicht weiter, sondern kapseln ihn in eine Action `loadBooksFailure`, die wir als reguläres Element des Datenstroms weitergeben. Der Fehler im `catchError()` ist als `unknown` typisiert – an dieser Stelle kann schließlich alles Mögliche ankommen (ein `Error`, bei einem echten Backend eine `HttpErrorResponse`, im Extremfall ein String). Statt blind auf eine Eigenschaft wie `message` zuzugreifen, prüfen wir die Form des Fehlers in einer kleinen Hilfsfunktion `toMessage()`: @@ -572,11 +572,11 @@ import { catchError, map, switchMap } from 'rxjs/operators'; import { of } from 'rxjs'; import * as BookActions from './book.actions'; -import { BookStoreService } from '../../shared/book-store.service'; +import { BookApi } from '../../shared/book-api'; import { toMessage } from '../../shared/error-message'; export const loadBooks$ = createEffect( - (actions$ = inject(Actions), service = inject(BookStoreService)) => { + (actions$ = inject(Actions), service = inject(BookApi)) => { return actions$.pipe( ofType(BookActions.loadBooks), switchMap(() => @@ -625,7 +625,7 @@ export const deleteBookFailure = createAction('[Book] Delete Book Failure', prop Schon an dieser Liste zeigt sich der Preis des Patterns: Drei CRUD-Operationen ergeben neun zusätzliche Actions. (Kompakter notieren lässt sich das mit `createActionGroup()`, siehe Abschnitt "Wie geht's weiter?".) -**Effects.** Jede Mutation löst einen HTTP-Request aus. Ein wichtiger Unterschied zum Laden betrifft den Flattening-Operator: Beim Laden ist `switchMap()` richtig (eine neue Anfrage macht die alte überflüssig). Bei **schreibenden** Operationen wollen wir laufende Requests aber *nicht* abbrechen – sonst ginge womöglich ein Speichervorgang verloren. Hier ist `concatMap()` die sichere Wahl. Der `BookStoreService` bietet dazu die Methoden `create()`, `update()` und `remove()` an: +**Effects.** Jede Mutation löst einen HTTP-Request aus. Ein wichtiger Unterschied zum Laden betrifft den Flattening-Operator: Beim Laden ist `switchMap()` richtig (eine neue Anfrage macht die alte überflüssig). Bei **schreibenden** Operationen wollen wir laufende Requests aber *nicht* abbrechen – sonst ginge womöglich ein Speichervorgang verloren. Hier ist `concatMap()` die sichere Wahl. Der `BookApi` bietet dazu die Methoden `create()`, `update()` und `remove()` an: ```ts // books/store/book.effects.ts (Ergänzung in der Klasse BookEffects) @@ -728,7 +728,7 @@ on(BookActions.clearError, (state): State => ({ ...state, error: null })) ``` ```ts -// books/book-list/book-list.component.ts +// books/book-list/book-list.ts import { Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; @@ -737,7 +737,7 @@ import { selectAllBooks, selectBooksError, selectBooksLoading } from '../store/b import { Book } from '../../shared/book'; @Component({ /* ... */ }) -export class BookListComponent { +export class BookList { private store = inject(Store); books = this.store.selectSignal(selectAllBooks); @@ -775,7 +775,7 @@ export class BookListComponent { Im Template zeigen wir die Fehlermeldung (mit einem Button zum Wegklicken) an, bieten ein kleines Formular zum Anlegen und lösen Ändern (Bewertung erhöhen) sowie Löschen über Buttons aus. Die Eingabefelder reichen wir bewusst ohne `FormsModule` als lokale Template-Referenzen (`#isbnEl`) direkt an die Methode weiter, die nach dem Dispatch die Felder leert – so bleibt der Fokus auf NgRx: ```html - + @if (error(); as error) {
                      {{ error }} @@ -1134,7 +1134,7 @@ describe('BookEffects', () => { }); ``` -Für die konkreten Tests müssen wir ein Observable von Actions bereitstellen und das ausgegebene Observable sowie ggf. die ausgeführten Seiteneffekte prüfen. Das folgende Listing zeigt einen vollständigen Testaufbau. Hier stellen wir sicher, dass der Effect die gewünschte Action ausgibt und die Daten wie gewünscht über den `BookStoreService` bezieht. +Für die konkreten Tests müssen wir ein Observable von Actions bereitstellen und das ausgegebene Observable sowie ggf. die ausgeführten Seiteneffekte prüfen. Das folgende Listing zeigt einen vollständigen Testaufbau. Hier stellen wir sicher, dass der Effect die gewünschte Action ausgibt und die Daten wie gewünscht über den `BookApi` bezieht. ```ts // books/store/book.effects.spec.ts @@ -1148,7 +1148,7 @@ describe('BookEffects', () => { BookEffects, provideMockActions(() => actions$), { - provide: BookStoreService, + provide: BookApi, useValue: { getAll: () => of([]) } } ] @@ -1161,7 +1161,7 @@ describe('BookEffects', () => { const books = [book('1'), book('2'), book('3')]; // Implementierung von getAll() ersetzen - const bs = TestBed.inject(BookStoreService); + const bs = TestBed.inject(BookApi); spyOn(bs, 'getAll').and.callFake(() => of(books)); // Action auslösen @@ -1199,7 +1199,7 @@ describe('BookEffects', () => { it('should fire loadBooksSuccess for loadBooks', () => { const books = [book('1'), book('2'), book('3')]; - const bs = TestBed.inject(BookStoreService); + const bs = TestBed.inject(BookApi); spyOn(bs, 'getAll').and.callFake(() => of(books)); actions$ = hot('--a', { a: loadBooks() }); @@ -1233,7 +1233,7 @@ import { of } from 'rxjs'; @Injectable() export class BookEffects { private actions$ = inject(Actions); - private service = inject(BookStoreService); + private service = inject(BookApi); private store = inject(Store); loadBooks$ = createEffect(() => { @@ -1273,7 +1273,7 @@ describe('BookEffects', () => { initialState: { book: initialState } }), { - provide: BookStoreService, + provide: BookApi, useValue: { getAll: () => of([]) } @@ -1309,16 +1309,16 @@ describe('BookEffects', () => { }); ``` -Ebenso können wir mit dem `MockStore` die Rückgabewerte von Selektoren überschreiben. Unser letztes Beispiel zum Thema zeigt, wie wir sicherstellen können, dass die `BookListComponent` tatsächlich den Text *"Loading ..."* anzeigt, wenn im State das Loading-Flag entsprechend gesetzt ist. Es ist dabei nicht notwendig, den State im `MockStore` zu setzen, da wir auf diesen nicht direkt zugreifen. Die Komponente bezieht ihre Daten über die beiden Selektoren `selectAllBooks` und `selectBooksLoading`, die wir entsprechend "ausmocken": +Ebenso können wir mit dem `MockStore` die Rückgabewerte von Selektoren überschreiben. Unser letztes Beispiel zum Thema zeigt, wie wir sicherstellen können, dass die `BookList` tatsächlich den Text *"Loading ..."* anzeigt, wenn im State das Loading-Flag entsprechend gesetzt ist. Es ist dabei nicht notwendig, den State im `MockStore` zu setzen, da wir auf diesen nicht direkt zugreifen. Die Komponente bezieht ihre Daten über die beiden Selektoren `selectAllBooks` und `selectBooksLoading`, die wir entsprechend "ausmocken": ```ts -// books/book-list/book-list.component.spec.ts +// books/book-list/book-list.spec.ts import { provideMockStore } from '@ngrx/store/testing'; -describe('BookListComponent', () => { +describe('BookList', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ BookListComponent ], + imports: [ BookList ], providers: [ provideMockStore({ selectors: [ @@ -1332,7 +1332,7 @@ describe('BookListComponent', () => { }); it('should show a loading text', () => { - const fixture = TestBed.createComponent(BookListComponent); + const fixture = TestBed.createComponent(BookList); fixture.detectChanges(); const element = fixture.nativeElement; expect(element.querySelector('.loader').textContent).toBe('Loading ...'); @@ -1377,7 +1377,7 @@ In der Komponente nutzen wir die Facade, um die Daten aus den Properties zu lese ```ts @Component({ /* ... */ }) -export class BookListComponent { +export class BookList { private booksFacade = inject(BooksFacade); books = this.booksFacade.books; diff --git a/material/ngrx-global-store/demo/README.md b/material/ngrx-global-store/demo/README.md index 2dd401e3..3df3c880 100644 --- a/material/ngrx-global-store/demo/README.md +++ b/material/ngrx-global-store/demo/README.md @@ -5,11 +5,11 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern (Bewertung erhöhen) und löschen (CRUD) mit Actions, Reducer, Selektoren und Effects – inklusive Lade- und Fehleranzeige. - **Angular 22** + **NgRx 21** (`@ngrx/store`, `@ngrx/effects`) -- Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. +- Der `BookApi` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. -> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/store/…`, Komponente `BookListComponent`, Selektor `bm-book-list`); die Demo nutzt dagegen die flachen Angular-CLI-Standardnamen (`src/app/store/…`, `BookList`, `app-book-list`). +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/store/…`, Selektor-Präfix `bm-`); die Demo liegt dagegen flach unter `src/app/store/…` mit dem CLI-Standardpräfix `app-`. Klassen- und Dateinamen folgen in beiden Fällen der aktuellen Angular-Konvention ohne `.component`/`.service`-Suffix. ## Befehle diff --git a/material/ngrx-global-store/demo/src/app/shared/book-store.service.ts b/material/ngrx-global-store/demo/src/app/shared/book-api.ts similarity index 97% rename from material/ngrx-global-store/demo/src/app/shared/book-store.service.ts rename to material/ngrx-global-store/demo/src/app/shared/book-api.ts index b43b0e6b..e3fdb942 100644 --- a/material/ngrx-global-store/demo/src/app/shared/book-store.service.ts +++ b/material/ngrx-global-store/demo/src/app/shared/book-api.ts @@ -11,7 +11,7 @@ import { Book } from './book'; * so lässt sich die Fehleranzeige der Anwendung auch ohne Server auslösen. */ @Injectable({ providedIn: 'root' }) -export class BookStoreService { +export class BookApi { private books: Book[] = [ { isbn: '978-3-86490-900-1', title: 'Angular', rating: 5 }, { isbn: '978-3-86490-466-2', title: 'Angular für Einsteiger', rating: 4 }, diff --git a/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts index d54b7c93..db4be9bb 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts @@ -4,7 +4,7 @@ import { Action } from '@ngrx/store'; import { provideMockActions } from '@ngrx/effects/testing'; import { BookEffects } from './book.effects'; -import { BookStoreService } from '../shared/book-store.service'; +import { BookApi } from '../shared/book-api'; import * as BookActions from './book.actions'; import { Book } from '../shared/book'; @@ -13,12 +13,12 @@ const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); describe('BookEffects', () => { let actions$: Observable; - function setup(serviceMock: Partial): BookEffects { + function setup(serviceMock: Partial): BookEffects { TestBed.configureTestingModule({ providers: [ BookEffects, provideMockActions(() => actions$), - { provide: BookStoreService, useValue: serviceMock } + { provide: BookApi, useValue: serviceMock } ] }); return TestBed.inject(BookEffects); diff --git a/material/ngrx-global-store/demo/src/app/store/book.effects.ts b/material/ngrx-global-store/demo/src/app/store/book.effects.ts index 729e5554..ef3364cf 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.effects.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.effects.ts @@ -4,13 +4,13 @@ import { catchError, concatMap, map, switchMap } from 'rxjs/operators'; import { of } from 'rxjs'; import * as BookActions from './book.actions'; -import { BookStoreService } from '../shared/book-store.service'; +import { BookApi } from '../shared/book-api'; import { toMessage } from '../shared/error-message'; @Injectable() export class BookEffects { private actions$ = inject(Actions); - private service = inject(BookStoreService); + private service = inject(BookApi); // Lesen: switchMap – eine neue Anfrage macht die alte überflüssig. loadBooks$ = createEffect(() => diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md index 0e463390..373981c3 100644 --- a/material/ngrx-intro/README.md +++ b/material/ngrx-intro/README.md @@ -26,7 +26,7 @@ Mit wachsender Größe der Anwendung ergeben sich immer wieder folgende Fragen: - Wie reagieren wir an verschiedenen Stellen auf Ereignisse, die in der Anwendung auftreten? - Wie verwalten wir die Daten, die über die gesamte Anwendung verteilt sind? -Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookStoreService`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter treiben: Bislang waren Komponenten die "Hüterinnen" der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine gegenüber den anderen Komponenten abgeschottete Einheit. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. +Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookApi`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter treiben: Bislang waren Komponenten die "Hüterinnen" der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine gegenüber den anderen Komponenten abgeschottete Einheit. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. Wir wollen in diesem Artikel besprechen, wie eine solche zentrale Zustandsverwaltung (engl. *State Management*) realisiert werden kann. Dabei lernen wir das Architekturmuster *Redux* kennen und nutzen das populäre Framework *Reactive State for Angular (NgRx)*, um den Anwendungszustand zu verwalten und unsere Prozesse zu koordinieren. diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index d5d0dd14..7ad5d665 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -100,7 +100,7 @@ export const BookStore = signalStore( // ... providers: [BookStore] }) -export class BookListComponent { +export class BookList { private store = inject(BookStore); } ``` @@ -210,11 +210,11 @@ import { pipe, switchMap, tap } from 'rxjs'; import { patchState, withMethods } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; -import { BookStoreService } from '../shared/book-store.service'; +import { BookApi } from '../shared/book-api'; import { toMessage } from '../shared/error-message'; // innerhalb von signalStore(...): -withMethods((store, service = inject(BookStoreService)) => ({ +withMethods((store, service = inject(BookApi)) => ({ loadBooks: rxMethod( pipe( tap(() => patchState(store, { loading: true, error: null })), @@ -245,14 +245,14 @@ Die so erzeugte Methode rufen wir später einfach als `store.loadBooks()` auf. W ### Schreiben: Bücher anlegen, ändern und löschen -Jetzt zum Kern jeder echten Anwendung: Daten verändern. Wir definieren je eine Methode zum Anlegen, Ändern und Löschen. Jede löst einen HTTP-Request aus – über den `BookStoreService`, der wie schon in Teil 2 die HTTP-Aufrufe mit dem `HttpClient` kapselt und `create()`, `update()` und `remove()` anbietet (`provideHttpClient()` gehört dafür in die `app.config.ts`) – und schreibt das Ergebnis anschließend immutabel in den State. +Jetzt zum Kern jeder echten Anwendung: Daten verändern. Wir definieren je eine Methode zum Anlegen, Ändern und Löschen. Jede löst einen HTTP-Request aus – über den `BookApi`, der wie schon in Teil 2 die HTTP-Aufrufe mit dem `HttpClient` kapselt und `create()`, `update()` und `remove()` anbietet (`provideHttpClient()` gehört dafür in die `app.config.ts`) – und schreibt das Ergebnis anschließend immutabel in den State. Ein wichtiger Unterschied zum Laden betrifft den Flattening-Operator: Beim Laden ist `switchMap()` richtig (eine neue Anfrage macht die alte überflüssig). Bei **schreibenden** Operationen wollen wir laufende Requests aber *nicht* abbrechen – sonst ginge womöglich ein Speichervorgang verloren. Hier ist `concatMap()` die sichere Wahl: Die Anfragen werden der Reihe nach abgearbeitet. ```ts import { concatMap, pipe, tap } from 'rxjs'; -// innerhalb von withMethods((store, service = inject(BookStoreService)) => ({ ... })): +// innerhalb von withMethods((store, service = inject(BookApi)) => ({ ... })): // Anlegen: an die Liste anhängen addBook: rxMethod( @@ -329,7 +329,7 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { Book } from '../shared/book'; -import { BookStoreService } from '../shared/book-store.service'; +import { BookApi } from '../shared/book-api'; import { toMessage } from '../shared/error-message'; type BookState = { @@ -350,7 +350,7 @@ export const BookStore = signalStore( withComputed(({ books }) => ({ booksCount: computed(() => books().length) })), - withMethods((store, service = inject(BookStoreService)) => ({ + withMethods((store, service = inject(BookApi)) => ({ clearError(): void { patchState(store, { error: null }); }, @@ -430,17 +430,17 @@ Das ist der gesamte State-Management-Code für ein vollständiges CRUD-Feature Die Komponente injiziert den Store, liest die Signale im Template und ruft für Nutzeraktionen die Store-Methoden auf. Da der Store sich über `onInit` selbst lädt, braucht die Komponente keinen Lade-Code mehr: ```ts -// books/book-list/book-list.component.ts +// books/book-list/book-list.ts import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { BookStore } from '../book.store'; import { Book } from '../shared/book'; @Component({ selector: 'bm-book-list', - templateUrl: './book-list.component.html', + templateUrl: './book-list.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class BookListComponent { +export class BookList { protected store = inject(BookStore); addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { @@ -466,7 +466,7 @@ export class BookListComponent { Im Template lesen wir die Signale und lösen über Events die Methoden aus: ein kleines Formular zum Anlegen, pro Buch ein Button zum Erhöhen der Bewertung und ein Löschen-Button sowie eine Fehlermeldung aus dem `error`-Signal: ```html - +

                      Books ({{ store.booksCount() }})

                      @if (store.loading()) { @@ -522,14 +522,14 @@ import { } from '@ngrx/signals/entities'; import { Book } from '../shared/book'; -import { BookStoreService } from '../shared/book-store.service'; +import { BookApi } from '../shared/book-api'; const selectId: SelectEntityId = book => book.isbn; export const BookStore = signalStore( { providedIn: 'root' }, withEntities(), - withMethods((store, service = inject(BookStoreService)) => ({ + withMethods((store, service = inject(BookApi)) => ({ loadBooks: rxMethod( switchMap(() => service.getAll().pipe( @@ -608,7 +608,7 @@ Da ein SignalStore ein gewöhnlicher Service ist, testen wir ihn auch wie einen import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { BookStore } from './book.store'; -import { BookStoreService } from '../shared/book-store.service'; +import { BookApi } from '../shared/book-api'; import { Book } from '../shared/book'; // kleine Hilfsfunktion, um schnell Testdaten zu erzeugen @@ -619,7 +619,7 @@ describe('BookStore', () => { TestBed.configureTestingModule({ providers: [ { - provide: BookStoreService, + provide: BookApi, useValue: { getAll: () => of([b('1'), b('2'), b('3')]) } } ] diff --git a/material/ngrx-signal-store/demo/README.md b/material/ngrx-signal-store/demo/README.md index 805fe419..d4828fb2 100644 --- a/material/ngrx-signal-store/demo/README.md +++ b/material/ngrx-signal-store/demo/README.md @@ -5,11 +5,11 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern (Bewertung erhöhen) und löschen (CRUD) mit `signalStore`, `withState`, `withComputed`, `withMethods`, `rxMethod` und `patchState` – inklusive Lade- und Fehleranzeige. - **Angular 22** + **NgRx 21** (`@ngrx/signals`, `@ngrx/operators`) -- Der `BookStoreService` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. +- Der `BookApi` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. -> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/…`, Komponente `BookListComponent`, Selektor `bm-book-list`); die Demo nutzt dagegen die flachen Angular-CLI-Standardnamen (`src/app/book.store.ts`, `BookList`, `app-book-list`). +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/…`, Selektor-Präfix `bm-`); die Demo liegt dagegen flach unter `src/app/…` mit dem CLI-Standardpräfix `app-`. Klassen- und Dateinamen folgen in beiden Fällen der aktuellen Angular-Konvention ohne `.component`/`.service`-Suffix. ## Befehle diff --git a/material/ngrx-signal-store/demo/src/app/app.spec.ts b/material/ngrx-signal-store/demo/src/app/app.spec.ts index b094eac8..1dec1d3b 100644 --- a/material/ngrx-signal-store/demo/src/app/app.spec.ts +++ b/material/ngrx-signal-store/demo/src/app/app.spec.ts @@ -1,14 +1,14 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { App } from './app'; -import { BookStoreService } from './shared/book-store.service'; +import { BookApi } from './shared/book-api'; describe('App', () => { it('erzeugt die App und zeigt die Buchliste an', async () => { await TestBed.configureTestingModule({ imports: [App], providers: [ - { provide: BookStoreService, useValue: { getAll: () => of([]) } } + { provide: BookApi, useValue: { getAll: () => of([]) } } ] }).compileComponents(); diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts b/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts index 93dbf0f3..da8dcdf9 100644 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts @@ -2,12 +2,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { of, throwError } from 'rxjs'; import { BookList } from './book-list'; -import { BookStoreService } from '../shared/book-store.service'; +import { BookApi } from '../shared/book-api'; import { Book } from '../shared/book'; const b = (isbn: string, title = `Title ${isbn}`, rating = 0): Book => ({ isbn, title, rating }); -function serviceMock(books: Book[], overrides: Partial = {}): Partial { +function serviceMock(books: Book[], overrides: Partial = {}): Partial { return { getAll: () => of(books), create: (book: Book) => of(book), @@ -18,10 +18,10 @@ function serviceMock(books: Book[], overrides: Partial = {}): } describe('BookList (SignalStore)', () => { - async function setup(mock: Partial): Promise> { + async function setup(mock: Partial): Promise> { await TestBed.configureTestingModule({ imports: [BookList], - providers: [{ provide: BookStoreService, useValue: mock }] + providers: [{ provide: BookApi, useValue: mock }] }).compileComponents(); const fixture = TestBed.createComponent(BookList); diff --git a/material/ngrx-signal-store/demo/src/app/book.store.spec.ts b/material/ngrx-signal-store/demo/src/app/book.store.spec.ts index 50fc59d3..96afefcb 100644 --- a/material/ngrx-signal-store/demo/src/app/book.store.spec.ts +++ b/material/ngrx-signal-store/demo/src/app/book.store.spec.ts @@ -2,15 +2,15 @@ import { TestBed } from '@angular/core/testing'; import { of, Subject, throwError } from 'rxjs'; import { BookStore } from './book.store'; -import { BookStoreService } from './shared/book-store.service'; +import { BookApi } from './shared/book-api'; import { Book } from './shared/book'; const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); -/** Erzeugt einen frischen Store mit gemocktem BookStoreService. */ -function createStore(mock: Partial) { +/** Erzeugt einen frischen Store mit gemocktem BookApi. */ +function createStore(mock: Partial) { TestBed.configureTestingModule({ - providers: [{ provide: BookStoreService, useValue: mock }] + providers: [{ provide: BookApi, useValue: mock }] }); return TestBed.inject(BookStore); } diff --git a/material/ngrx-signal-store/demo/src/app/book.store.ts b/material/ngrx-signal-store/demo/src/app/book.store.ts index 04d2b2aa..5ed01aaf 100644 --- a/material/ngrx-signal-store/demo/src/app/book.store.ts +++ b/material/ngrx-signal-store/demo/src/app/book.store.ts @@ -12,7 +12,7 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { Book } from './shared/book'; -import { BookStoreService } from './shared/book-store.service'; +import { BookApi } from './shared/book-api'; import { toMessage } from './shared/error-message'; type BookState = { @@ -33,7 +33,7 @@ export const BookStore = signalStore( withComputed(({ books }) => ({ booksCount: computed(() => books().length) })), - withMethods((store, service = inject(BookStoreService)) => ({ + withMethods((store, service = inject(BookApi)) => ({ clearError(): void { patchState(store, { error: null }); }, diff --git a/material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts b/material/ngrx-signal-store/demo/src/app/shared/book-api.ts similarity index 97% rename from material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts rename to material/ngrx-signal-store/demo/src/app/shared/book-api.ts index a62e2442..404417b8 100644 --- a/material/ngrx-signal-store/demo/src/app/shared/book-store.service.ts +++ b/material/ngrx-signal-store/demo/src/app/shared/book-api.ts @@ -11,7 +11,7 @@ import { Book } from './book'; * so lässt sich die Fehleranzeige der Anwendung auch ohne Server auslösen. */ @Injectable({ providedIn: 'root' }) -export class BookStoreService { +export class BookApi { private books: Book[] = [ { isbn: '978-3-86490-900-1', title: 'Angular', rating: 5 }, { isbn: '978-3-86490-466-2', title: 'Angular für Einsteiger', rating: 4 }, From c7bab926732bfd370740f7f65f4b4f2c0c5b6ecf Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Mon, 15 Jun 2026 07:04:04 +0200 Subject: [PATCH 19/24] =?UTF-8?q?material(ngrx):=20Demos=20auf=20BookMonke?= =?UTF-8?q?y-Pr=C3=A4fix=20bm-=20umgestellt=20(statt=20app-)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - angular.json prefix app -> bm in beiden Demos - Selektoren: app-root -> bm-root, app-book-list -> bm-book-list - Templates/index.html entsprechend; Demos damit deckungsgleich zu den Artikeln - Demo-READMEs: Präfix-Unterschied entfernt (nur noch Ordnerstruktur differiert) Beweis: Global 31/31, Signal 16/16 Vitest gruen; beide bauen (148 kB / 126 kB). --- material/ngrx-global-store/demo/README.md | 2 +- material/ngrx-global-store/demo/angular.json | 2 +- material/ngrx-global-store/demo/src/app/app.html | 2 +- material/ngrx-global-store/demo/src/app/app.ts | 2 +- material/ngrx-global-store/demo/src/app/book-list/book-list.ts | 2 +- material/ngrx-global-store/demo/src/index.html | 2 +- material/ngrx-signal-store/demo/README.md | 2 +- material/ngrx-signal-store/demo/angular.json | 2 +- material/ngrx-signal-store/demo/src/app/app.html | 2 +- material/ngrx-signal-store/demo/src/app/app.ts | 2 +- material/ngrx-signal-store/demo/src/app/book-list/book-list.ts | 2 +- material/ngrx-signal-store/demo/src/index.html | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/material/ngrx-global-store/demo/README.md b/material/ngrx-global-store/demo/README.md index 3df3c880..07b18282 100644 --- a/material/ngrx-global-store/demo/README.md +++ b/material/ngrx-global-store/demo/README.md @@ -9,7 +9,7 @@ Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie B > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. -> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/store/…`, Selektor-Präfix `bm-`); die Demo liegt dagegen flach unter `src/app/store/…` mit dem CLI-Standardpräfix `app-`. Klassen- und Dateinamen folgen in beiden Fällen der aktuellen Angular-Konvention ohne `.component`/`.service`-Suffix. +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/store/…`); die Demo liegt dagegen flach unter `src/app/store/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. ## Befehle diff --git a/material/ngrx-global-store/demo/angular.json b/material/ngrx-global-store/demo/angular.json index 3043937e..980de3c1 100644 --- a/material/ngrx-global-store/demo/angular.json +++ b/material/ngrx-global-store/demo/angular.json @@ -11,7 +11,7 @@ "schematics": {}, "root": "", "sourceRoot": "src", - "prefix": "app", + "prefix": "bm", "architect": { "build": { "builder": "@angular/build:application", diff --git a/material/ngrx-global-store/demo/src/app/app.html b/material/ngrx-global-store/demo/src/app/app.html index 475128a5..2c3f1776 100644 --- a/material/ngrx-global-store/demo/src/app/app.html +++ b/material/ngrx-global-store/demo/src/app/app.html @@ -1 +1 @@ - + diff --git a/material/ngrx-global-store/demo/src/app/app.ts b/material/ngrx-global-store/demo/src/app/app.ts index 54511afa..120a9d7e 100644 --- a/material/ngrx-global-store/demo/src/app/app.ts +++ b/material/ngrx-global-store/demo/src/app/app.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { BookList } from './book-list/book-list'; @Component({ - selector: 'app-root', + selector: 'bm-root', imports: [BookList], templateUrl: './app.html', changeDetection: ChangeDetectionStrategy.OnPush diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.ts b/material/ngrx-global-store/demo/src/app/book-list/book-list.ts index 8f0c49e9..f0209cd3 100644 --- a/material/ngrx-global-store/demo/src/app/book-list/book-list.ts +++ b/material/ngrx-global-store/demo/src/app/book-list/book-list.ts @@ -6,7 +6,7 @@ import { selectAllBooks, selectBooksError, selectBooksLoading } from '../store/b import { Book } from '../shared/book'; @Component({ - selector: 'app-book-list', + selector: 'bm-book-list', templateUrl: './book-list.html', changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/material/ngrx-global-store/demo/src/index.html b/material/ngrx-global-store/demo/src/index.html index f9f06e72..e8f9c719 100644 --- a/material/ngrx-global-store/demo/src/index.html +++ b/material/ngrx-global-store/demo/src/index.html @@ -8,6 +8,6 @@ - + diff --git a/material/ngrx-signal-store/demo/README.md b/material/ngrx-signal-store/demo/README.md index d4828fb2..7274ee1e 100644 --- a/material/ngrx-signal-store/demo/README.md +++ b/material/ngrx-signal-store/demo/README.md @@ -9,7 +9,7 @@ Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. -> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/…`, Selektor-Präfix `bm-`); die Demo liegt dagegen flach unter `src/app/…` mit dem CLI-Standardpräfix `app-`. Klassen- und Dateinamen folgen in beiden Fällen der aktuellen Angular-Konvention ohne `.component`/`.service`-Suffix. +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/…`); die Demo liegt dagegen flach unter `src/app/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. ## Befehle diff --git a/material/ngrx-signal-store/demo/angular.json b/material/ngrx-signal-store/demo/angular.json index 3043937e..980de3c1 100644 --- a/material/ngrx-signal-store/demo/angular.json +++ b/material/ngrx-signal-store/demo/angular.json @@ -11,7 +11,7 @@ "schematics": {}, "root": "", "sourceRoot": "src", - "prefix": "app", + "prefix": "bm", "architect": { "build": { "builder": "@angular/build:application", diff --git a/material/ngrx-signal-store/demo/src/app/app.html b/material/ngrx-signal-store/demo/src/app/app.html index 475128a5..2c3f1776 100644 --- a/material/ngrx-signal-store/demo/src/app/app.html +++ b/material/ngrx-signal-store/demo/src/app/app.html @@ -1 +1 @@ - + diff --git a/material/ngrx-signal-store/demo/src/app/app.ts b/material/ngrx-signal-store/demo/src/app/app.ts index 54511afa..120a9d7e 100644 --- a/material/ngrx-signal-store/demo/src/app/app.ts +++ b/material/ngrx-signal-store/demo/src/app/app.ts @@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { BookList } from './book-list/book-list'; @Component({ - selector: 'app-root', + selector: 'bm-root', imports: [BookList], templateUrl: './app.html', changeDetection: ChangeDetectionStrategy.OnPush diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts b/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts index bc3882d4..b7d75eb9 100644 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts +++ b/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts @@ -3,7 +3,7 @@ import { BookStore } from '../book.store'; import { Book } from '../shared/book'; @Component({ - selector: 'app-book-list', + selector: 'bm-book-list', templateUrl: './book-list.html', changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/material/ngrx-signal-store/demo/src/index.html b/material/ngrx-signal-store/demo/src/index.html index f9f06e72..e8f9c719 100644 --- a/material/ngrx-signal-store/demo/src/index.html +++ b/material/ngrx-signal-store/demo/src/index.html @@ -8,6 +8,6 @@ - + From 1f6b57f48affb7adc969d17e7942299c4a2dec3d Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Mon, 15 Jun 2026 07:20:32 +0200 Subject: [PATCH 20/24] material(ngrx): Beispielprojekt BookMonkey -> BookManager umbenannt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Alle Nennungen des Projektnamens in den drei Teilen und beiden Demo-READMEs von "BookMonkey" auf "BookManager" umgestellt (Buch-Umbenennung 1. Auflage). - Der Selektor-/CLI-Präfix bm- bleibt: "bm" passt weiterhin (BookManager). - errata-3a/4a bewusst unverändert: Sie verweisen auf die historischen Projekte und veröffentlichte Artefakte (GitHub book-monkey4, npm book-monkey5-styles) und dürfen nicht umbenannt werden. --- material/ngrx-global-store/README.md | 8 ++++---- material/ngrx-global-store/demo/README.md | 2 +- material/ngrx-signal-store/README.md | 4 ++-- material/ngrx-signal-store/demo/README.md | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index 4683cf11..7642fd82 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -23,11 +23,11 @@ Im ersten Teil haben wir uns Schritt für Schritt ein eigenes Modell für zentra Das Framework *Reactive State for Angular (NgRx)* ist eine der populärsten Implementierungen für State Management mit Angular. Durch die gezielte Ausrichtung auf Angular fügt sich der Code gut in die Strukturen und Lebenszyklen einer Angular-Anwendung ein. NgRx setzt stark auf die Möglichkeiten der reaktiven Programmierung mit RxJS, ist also an vielen Stellen von Observables und Datenströmen geprägt. Über `selectSignal()` integriert es sich außerdem nahtlos in die signal-basierte Welt von modernem Angular. Die große Community und eine Reihe von verwandten Projekten machen NgRx zum wohl bekanntesten Werkzeug für Zustandsverwaltung mit Angular. -Wir wollen in diesem Abschnitt die Struktur und die Bausteine in der Welt von NgRx genauer besprechen. Außerdem wollen wir im BookMonkey einen Aspekt mithilfe von NgRx umsetzen, um so alle Bausteine auch praktisch zu üben. +Wir wollen in diesem Abschnitt die Struktur und die Bausteine in der Welt von NgRx genauer besprechen. Außerdem wollen wir im BookManager einen Aspekt mithilfe von NgRx umsetzen, um so alle Bausteine auch praktisch zu üben. ### Projekt vorbereiten -Als Grundlage für diesen praktischen Teil verwenden wir das Beispielprojekt BookMonkey aus dem Buch. Wir bauen auf der Variante mit Standalone Components auf, wie sie die aktuelle Angular-Version vorgibt. Wer mitentwickeln möchte, kann das bestehende BookMonkey-Projekt verwenden. +Als Grundlage für diesen praktischen Teil verwenden wir das Beispielprojekt BookManager aus dem Buch. Wir bauen auf der Variante mit Standalone Components auf, wie sie die aktuelle Angular-Version vorgibt. Wer mitentwickeln möchte, kann das bestehende BookManager-Projekt verwenden. ### Store einrichten @@ -326,7 +326,7 @@ Um das State-Objekt vor der Verwendung und Änderung zu klonen, können wir den > **Hinweis:** Ein verschachteltes Objekt können wir mit dem Spread-Operator klonen, indem wir jeden Zweig des Objekts einzeln kopieren. Wird das zu komplex, empfiehlt sich ein Hilfsmittel wie die native Funktion `structuredClone()`. Wir haben die verschiedenen Möglichkeiten in einem Blogartikel zusammengefasst: [10 useful operations for pure & immutable data structures](https://angular.schule/blog/2018-03-pure-immutable-operations). -Wir wollen nun auch für den BookMonkey passende Reducers entwickeln. Dazu überlegen wir zunächst, welche Zustandsänderungen von den Actions ausgelöst werden: +Wir wollen nun auch für den BookManager passende Reducers entwickeln. Dazu überlegen wir zunächst, welche Zustandsänderungen von den Actions ausgelöst werden: - `loadBooks`: Ladeindikator auf `true` setzen - `loadBooksSuccess`: Buchliste einfügen und Ladeindikator auf `false` setzen @@ -403,7 +403,7 @@ export const selectMyData = createSelector( Wir können auf diese Weise ein komplexes Gerüst von Selektoren definieren. Die Selektoren bauen aufeinander auf und kombinieren die Daten aus dem State so, wie sie in den Komponenten benötigt werden. Jeder einzelne Selektor nutzt die Memoization, um die berechneten Werte zu cachen. Die Berechnung in der Projektionsfunktion wird nur neu ausgeführt, wenn sich einer der Eingabewerte tatsächlich ändert. So sind auch komplexe Projektionen in großen Anwendungen ohne Nachteile in der Performance möglich. -Für den BookMonkey wollen wir zwei einfache Selektoren entwickeln, die uns Zugriff auf das `loading`-Flag und auf die Buchliste geben: +Für den BookManager wollen wir zwei einfache Selektoren entwickeln, die uns Zugriff auf das `loading`-Flag und auf die Buchliste geben: ```ts // books/store/book.selectors.ts diff --git a/material/ngrx-global-store/demo/README.md b/material/ngrx-global-store/demo/README.md index 07b18282..bd618c44 100644 --- a/material/ngrx-global-store/demo/README.md +++ b/material/ngrx-global-store/demo/README.md @@ -9,7 +9,7 @@ Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie B > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. -> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/store/…`); die Demo liegt dagegen flach unter `src/app/store/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookManager (Feature-Ordner `books/store/…`); die Demo liegt dagegen flach unter `src/app/store/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. ## Befehle diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index 7ad5d665..5bb34001 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -38,7 +38,7 @@ Kurz gesagt: Der Global Store ist *ein* großes, strenges System. Der SignalStor ## Der SignalStore im Detail -Schauen wir uns die Bausteine konkret an. Als durchgehendes Beispiel verwenden wir wieder den BookMonkey. Wir bauen Schritt für Schritt einen vollständigen `BookStore`, der die Buchliste vom Server lädt **und** Bücher anlegen, ändern und löschen kann – inklusive Lade- und Fehleranzeige. So lässt sich der direkte Vergleich zum Global Store aus Teil 2 ziehen. +Schauen wir uns die Bausteine konkret an. Als durchgehendes Beispiel verwenden wir wieder den BookManager. Wir bauen Schritt für Schritt einen vollständigen `BookStore`, der die Buchliste vom Server lädt **und** Bücher anlegen, ändern und löschen kann – inklusive Lade- und Fehleranzeige. So lässt sich der direkte Vergleich zum Global Store aus Teil 2 ziehen. ### Installation @@ -503,7 +503,7 @@ Die Eingabefelder reichen wir bewusst ohne `FormsModule` als lokale Template-Ref Die immutablen Array-Operationen aus unseren CRUD-Methoden (`[...state.books, created]`, `map(...)`, `filter(...)`) wiederholen sich in jeder Anwendung. Genau diese Routine nimmt uns das Plugin `@ngrx/signals/entities` mit dem Feature `withEntities()` ab – das Gegenstück zu `@ngrx/entity` aus Teil 2. Es legt die Signale `entityMap`, `ids` und `entities` an und bringt fertige Updater mit: `addEntity`, `updateEntity`, `removeEntity`, `setAllEntities` und weitere. -Standardmäßig erwartet `withEntities` ein Property `id`. Da ein Buch im BookMonkey über seine `isbn` identifiziert wird, geben wir – wie schon bei `@ngrx/entity` – einen eigenen ID-Selektor an. Bei `add*`-, `set*`- und `update*`-Updatern übergeben wir ihn als zweites Argument; die `remove*`-Updater ermitteln die ID automatisch: +Standardmäßig erwartet `withEntities` ein Property `id`. Da ein Buch im BookManager über seine `isbn` identifiziert wird, geben wir – wie schon bei `@ngrx/entity` – einen eigenen ID-Selektor an. Bei `add*`-, `set*`- und `update*`-Updatern übergeben wir ihn als zweites Argument; die `remove*`-Updater ermitteln die ID automatisch: ```ts // books/book.store.ts (Entity-Variante) diff --git a/material/ngrx-signal-store/demo/README.md b/material/ngrx-signal-store/demo/README.md index 7274ee1e..e4ecd471 100644 --- a/material/ngrx-signal-store/demo/README.md +++ b/material/ngrx-signal-store/demo/README.md @@ -9,7 +9,7 @@ Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden > **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. -> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookMonkey (Feature-Ordner `books/…`); die Demo liegt dagegen flach unter `src/app/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. +> **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookManager (Feature-Ordner `books/…`); die Demo liegt dagegen flach unter `src/app/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. ## Befehle From 56746671d9195436b541da8515c075623e3be8a3 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Mon, 15 Jun 2026 16:54:56 +0200 Subject: [PATCH 21/24] =?UTF-8?q?material(ngrx):=20Versionshinweise=20vers?= =?UTF-8?q?ionsunabh=C3=A4ngig=20formuliert=20(weniger=20Pflege)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Feste "Angular 22 / NgRx 21"-Nennungen aus den Artikel-Hinweisen entfernt; übrig bleibt die dauerhaft gültige Aussage zu Standalone/Signals/inject(). - Demo-READMEs: Stack ohne feste Nummern (Versionen stehen in package.json); der legacy-peer-deps-Hinweis ist jetzt allgemein formuliert (gilt für jeden Angular-Major-Wechsel, nicht nur 21↔22) – muss bei NgRx-Releases nicht mehr angefasst werden. - Historische Hinweise ("seit NgRx 18/19.2") bleiben, sie sind dauerhaft korrekt. - Konkrete Versionen leben nur noch in package.json (per ng update pflegbar). --- material/ngrx-global-store/README.md | 2 +- material/ngrx-global-store/demo/README.md | 4 ++-- material/ngrx-signal-store/README.md | 2 +- material/ngrx-signal-store/demo/README.md | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index 7642fd82..d469f7d7 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -17,7 +17,7 @@ Dieser Artikel ist **Teil 2** einer dreiteiligen Serie zum Thema State Managemen Im ersten Teil haben wir uns Schritt für Schritt ein eigenes Modell für zentrales State Management erarbeitet und dabei die Grundprinzipien von Redux kennengelernt. Jetzt setzen wir diese Ideen mit dem Framework NgRx in die Praxis um. -> **Hinweis zur Version:** Wir verwenden in diesem Artikel **Angular 22**. NgRx folgt normalerweise dem Major-Versionsschema von Angular; zum Zeitpunkt der Veröffentlichung ist jedoch **NgRx 21** die aktuelle Version, die problemlos unter Angular 22 läuft (in den Beispiel-Apps per `legacy-peer-deps` installiert). Wir setzen durchgängig auf Standalone APIs und Signals: Den Store richten wir ausschließlich über die `provide…`-Funktionen ein und lesen ihn mit `selectSignal()` als Signal aus. Konstruktor-Injection brauchen wir nicht mehr – auch das offizielle Effect-Schematic erzeugt seine Abhängigkeiten inzwischen mit `inject()`. +> **Hinweis:** Wir setzen durchgängig auf moderne Standalone APIs und Signals: Den Store richten wir ausschließlich über die `provide…`-Funktionen ein und lesen ihn mit `selectSignal()` als Signal aus. Konstruktor-Injection brauchen wir nicht mehr – auch das offizielle Effect-Schematic erzeugt seine Abhängigkeiten mit `inject()`. ## NgRx: Reactive State for Angular diff --git a/material/ngrx-global-store/demo/README.md b/material/ngrx-global-store/demo/README.md index bd618c44..0c5fa38c 100644 --- a/material/ngrx-global-store/demo/README.md +++ b/material/ngrx-global-store/demo/README.md @@ -4,10 +4,10 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern (Bewertung erhöhen) und löschen (CRUD) mit Actions, Reducer, Selektoren und Effects – inklusive Lade- und Fehleranzeige. -- **Angular 22** + **NgRx 21** (`@ngrx/store`, `@ngrx/effects`) +- **Angular** + **NgRx** (`@ngrx/store`, `@ngrx/effects`) – die konkreten Versionen stehen in der `package.json` - Der `BookApi` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. -> **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. +> **Hinweis:** NgRx und Angular folgen demselben Major-Versionsschema. Kurz nach einem neuen Angular-Major passt die NgRx-Peer-Range noch nicht; damit `npm install` trotzdem durchläuft, liegt eine `.npmrc` mit `legacy-peer-deps=true` bei. Sie stört nicht, sobald die Versionen wieder zusammenpassen. > **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookManager (Feature-Ordner `books/store/…`); die Demo liegt dagegen flach unter `src/app/store/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index 5bb34001..0d0f65c6 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -19,7 +19,7 @@ In [Teil 2](/material/ngrx-global-store) haben wir den **Global Store** kennenge Der Global Store ist mächtig, bringt für viele Anwendungsfälle aber spürbar viel Zeremonie mit: Schon das Laden einer simplen Buchliste verteilt sich über Actions, einen Reducer, Selektoren und einen Effect. Genau hier setzt der **SignalStore** aus dem Paket `@ngrx/signals` an. Er ist eine leichtgewichtige, signal-basierte Alternative, die mit deutlich weniger Code auskommt und sich nahtlos in modernes, signal-zentriertes Angular einfügt. -> **Hinweis zur Version:** Wir verwenden **Angular 22** mit **NgRx 21** – zum Zeitpunkt der Veröffentlichung die aktuelle NgRx-Version, die problemlos unter Angular 22 läuft (in der Beispiel-App per `legacy-peer-deps` installiert). Der SignalStore wohnt im Paket `@ngrx/signals` und wird unabhängig vom Global Store (`@ngrx/store`) installiert. +> **Hinweis:** Der SignalStore wohnt im eigenen Paket `@ngrx/signals` und wird unabhängig vom Global Store (`@ngrx/store`) installiert. ## Der architektonische Unterschied zum Global Store diff --git a/material/ngrx-signal-store/demo/README.md b/material/ngrx-signal-store/demo/README.md index e4ecd471..e32fa190 100644 --- a/material/ngrx-signal-store/demo/README.md +++ b/material/ngrx-signal-store/demo/README.md @@ -4,10 +4,10 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern (Bewertung erhöhen) und löschen (CRUD) mit `signalStore`, `withState`, `withComputed`, `withMethods`, `rxMethod` und `patchState` – inklusive Lade- und Fehleranzeige. -- **Angular 22** + **NgRx 21** (`@ngrx/signals`, `@ngrx/operators`) +- **Angular** + **NgRx** (`@ngrx/signals`, `@ngrx/operators`) – die konkreten Versionen stehen in der `package.json` - Der `BookApi` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. -> **Hinweis:** NgRx 21 gibt als Peer-Range noch `@angular/core: ^21.0.0` an, läuft aber problemlos unter Angular 22. Damit `npm install` nicht an dieser Range scheitert, liegt im Projekt eine `.npmrc` mit `legacy-peer-deps=true`. +> **Hinweis:** NgRx und Angular folgen demselben Major-Versionsschema. Kurz nach einem neuen Angular-Major passt die NgRx-Peer-Range noch nicht; damit `npm install` trotzdem durchläuft, liegt eine `.npmrc` mit `legacy-peer-deps=true` bei. Sie stört nicht, sobald die Versionen wieder zusammenpassen. > **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookManager (Feature-Ordner `books/…`); die Demo liegt dagegen flach unter `src/app/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. From 583ae28a5a7f7cd4117263e1c045a39515aa256f Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Mon, 15 Jun 2026 18:24:38 +0200 Subject: [PATCH 22/24] material(ngrx): Versions-/Install-Hinweise ganz entfernt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Die "Hinweis zur Version"-Blöcke in Teil 2 und Teil 3 komplett gestrichen. - Demo-READMEs: legacy-peer-deps-Hinweis entfernt, Versions-Zusatz an der Paketzeile entfernt. Die .npmrc bleibt liegen, npm install läuft also weiterhin ohne Zutun durch – nur eben ohne erklärenden Kasten. - "Projektstruktur"-Notiz bleibt (versionsunabhängig). Damit enthalten die Texte keinerlei versionsgebundene Meta-Hinweise mehr. --- material/ngrx-global-store/README.md | 2 -- material/ngrx-global-store/demo/README.md | 4 +--- material/ngrx-signal-store/README.md | 2 -- material/ngrx-signal-store/demo/README.md | 4 +--- 4 files changed, 2 insertions(+), 10 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index d469f7d7..d171fd55 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -17,8 +17,6 @@ Dieser Artikel ist **Teil 2** einer dreiteiligen Serie zum Thema State Managemen Im ersten Teil haben wir uns Schritt für Schritt ein eigenes Modell für zentrales State Management erarbeitet und dabei die Grundprinzipien von Redux kennengelernt. Jetzt setzen wir diese Ideen mit dem Framework NgRx in die Praxis um. -> **Hinweis:** Wir setzen durchgängig auf moderne Standalone APIs und Signals: Den Store richten wir ausschließlich über die `provide…`-Funktionen ein und lesen ihn mit `selectSignal()` als Signal aus. Konstruktor-Injection brauchen wir nicht mehr – auch das offizielle Effect-Schematic erzeugt seine Abhängigkeiten mit `inject()`. - ## NgRx: Reactive State for Angular Das Framework *Reactive State for Angular (NgRx)* ist eine der populärsten Implementierungen für State Management mit Angular. Durch die gezielte Ausrichtung auf Angular fügt sich der Code gut in die Strukturen und Lebenszyklen einer Angular-Anwendung ein. NgRx setzt stark auf die Möglichkeiten der reaktiven Programmierung mit RxJS, ist also an vielen Stellen von Observables und Datenströmen geprägt. Über `selectSignal()` integriert es sich außerdem nahtlos in die signal-basierte Welt von modernem Angular. Die große Community und eine Reihe von verwandten Projekten machen NgRx zum wohl bekanntesten Werkzeug für Zustandsverwaltung mit Angular. diff --git a/material/ngrx-global-store/demo/README.md b/material/ngrx-global-store/demo/README.md index 0c5fa38c..2db1243b 100644 --- a/material/ngrx-global-store/demo/README.md +++ b/material/ngrx-global-store/demo/README.md @@ -4,11 +4,9 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil Sie zeigt den kompletten Code aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern (Bewertung erhöhen) und löschen (CRUD) mit Actions, Reducer, Selektoren und Effects – inklusive Lade- und Fehleranzeige. -- **Angular** + **NgRx** (`@ngrx/store`, `@ngrx/effects`) – die konkreten Versionen stehen in der `package.json` +- **Angular** + **NgRx** (`@ngrx/store`, `@ngrx/effects`) - Der `BookApi` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. -> **Hinweis:** NgRx und Angular folgen demselben Major-Versionsschema. Kurz nach einem neuen Angular-Major passt die NgRx-Peer-Range noch nicht; damit `npm install` trotzdem durchläuft, liegt eine `.npmrc` mit `legacy-peer-deps=true` bei. Sie stört nicht, sobald die Versionen wieder zusammenpassen. - > **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookManager (Feature-Ordner `books/store/…`); die Demo liegt dagegen flach unter `src/app/store/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. ## Befehle diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index 0d0f65c6..b9136c20 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -19,8 +19,6 @@ In [Teil 2](/material/ngrx-global-store) haben wir den **Global Store** kennenge Der Global Store ist mächtig, bringt für viele Anwendungsfälle aber spürbar viel Zeremonie mit: Schon das Laden einer simplen Buchliste verteilt sich über Actions, einen Reducer, Selektoren und einen Effect. Genau hier setzt der **SignalStore** aus dem Paket `@ngrx/signals` an. Er ist eine leichtgewichtige, signal-basierte Alternative, die mit deutlich weniger Code auskommt und sich nahtlos in modernes, signal-zentriertes Angular einfügt. -> **Hinweis:** Der SignalStore wohnt im eigenen Paket `@ngrx/signals` und wird unabhängig vom Global Store (`@ngrx/store`) installiert. - ## Der architektonische Unterschied zum Global Store Bevor wir Code schreiben, lohnt sich der Blick auf die grundlegend andere Architektur. Global Store und SignalStore lösen dasselbe Problem – zentrale Zustandsverwaltung – auf gegensätzliche Weise. diff --git a/material/ngrx-signal-store/demo/README.md b/material/ngrx-signal-store/demo/README.md index e32fa190..c57b89bf 100644 --- a/material/ngrx-signal-store/demo/README.md +++ b/material/ngrx-signal-store/demo/README.md @@ -4,11 +4,9 @@ Lauffähige Angular-Anwendung zum Artikel **[State Management mit NgRx – Teil Sie zeigt den kompletten `BookStore` aus dem Artikel in Aktion: Buchliste laden sowie Bücher anlegen, ändern (Bewertung erhöhen) und löschen (CRUD) mit `signalStore`, `withState`, `withComputed`, `withMethods`, `rxMethod` und `patchState` – inklusive Lade- und Fehleranzeige. -- **Angular** + **NgRx** (`@ngrx/signals`, `@ngrx/operators`) – die konkreten Versionen stehen in der `package.json` +- **Angular** + **NgRx** (`@ngrx/signals`, `@ngrx/operators`) - Der `BookApi` ist ein In-Memory-Stand-in für ein echtes HTTP-Backend, damit die App ohne Server läuft. Er wirft bei einer doppelten ISBN bewusst einen Fehler, sodass sich die Fehleranzeige auch ohne Server auslösen lässt. -> **Hinweis:** NgRx und Angular folgen demselben Major-Versionsschema. Kurz nach einem neuen Angular-Major passt die NgRx-Peer-Range noch nicht; damit `npm install` trotzdem durchläuft, liegt eine `.npmrc` mit `legacy-peer-deps=true` bei. Sie stört nicht, sobald die Versionen wieder zusammenpassen. - > **Projektstruktur:** Diese Demo ist eine abgeflachte Minimal-Reproduktion. Der Artikel beschreibt die Integration in den vollständigen BookManager (Feature-Ordner `books/…`); die Demo liegt dagegen flach unter `src/app/…`. Klassen-, Datei- und Selektornamen (Präfix `bm-`, keine `.component`/`.service`-Suffixe) folgen in beiden Fällen denselben Angular-Konventionen. ## Befehle From 2794540371ebebd08d3f8417c71d4fdf95c3aacd Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Wed, 17 Jun 2026 16:36:15 +0200 Subject: [PATCH 23/24] material(ngrx): Demos ans aktuelle Buch (BookManager) angeglichen - Datenservice: In-Memory raus -> echter HttpClient `BookStore` (@Service()) gegen die oeffentliche API api1.angular-buch.com (getAll/create/remove), provideHttpClient in app.config; Service-Tests via HttpTestingController. - Volles Buch-Modell (isbn/title/subtitle?/authors/description/imageUrl/ createdAt); kein rating mehr, kein update (hat das Buch ebenfalls nicht). - Favoriten als reiner Client-State (likeBook/clearLikedBooks) -> Kontrast Server-State (Effects/rxMethod) vs. lokaler State ohne Seiteneffekt. - Komponenten im Buch-Stil: praesentationale book-card (like/remove-Output) + smarte books-overview (Favoriten- + Buecher-Sektion). - Naming: Datenservice BookStore (buchidentisch); global = injizierter Store, signal = BookSignalStore. Prefix bm- -> app- (wie das Buch). Beweis: Global 37/37, Signal 21/21 Vitest gruen; beide bauen (174/152 kB). --- material/ngrx-global-store/demo/angular.json | 2 +- .../demo/src/app/app.config.ts | 2 + .../ngrx-global-store/demo/src/app/app.html | 2 +- .../demo/src/app/app.spec.ts | 14 +- .../ngrx-global-store/demo/src/app/app.ts | 6 +- .../demo/src/app/book-card/book-card.css | 48 +++++++ .../demo/src/app/book-card/book-card.html | 17 +++ .../demo/src/app/book-card/book-card.ts | 22 ++++ .../demo/src/app/book-list/book-list.html | 28 ---- .../demo/src/app/book-list/book-list.spec.ts | 103 --------------- .../demo/src/app/book-list/book-list.ts | 45 ------- .../src/app/books-overview/books-overview.css | 24 ++++ .../app/books-overview/books-overview.html | 38 ++++++ .../app/books-overview/books-overview.spec.ts | 124 ++++++++++++++++++ .../src/app/books-overview/books-overview.ts | 69 ++++++++++ .../demo/src/app/shared/book-api.ts | 42 ------ .../demo/src/app/shared/book-store.spec.ts | 78 +++++++++++ .../demo/src/app/shared/book-store.ts | 32 +++++ .../demo/src/app/shared/book.ts | 6 +- .../demo/src/app/shared/error-message.ts | 16 ++- .../demo/src/app/store/book.actions.ts | 15 ++- .../demo/src/app/store/book.effects.spec.ts | 36 ++--- .../demo/src/app/store/book.effects.ts | 31 ++--- .../demo/src/app/store/book.reducer.spec.ts | 40 ++++-- .../demo/src/app/store/book.reducer.ts | 26 ++-- .../demo/src/app/store/book.selectors.spec.ts | 30 ++++- .../demo/src/app/store/book.selectors.ts | 5 + .../ngrx-global-store/demo/src/index.html | 2 +- material/ngrx-signal-store/demo/angular.json | 2 +- .../demo/src/app/app.config.ts | 6 +- .../ngrx-signal-store/demo/src/app/app.html | 2 +- .../demo/src/app/app.spec.ts | 13 +- .../ngrx-signal-store/demo/src/app/app.ts | 6 +- .../demo/src/app/book-card/book-card.css | 48 +++++++ .../demo/src/app/book-card/book-card.html | 17 +++ .../demo/src/app/book-card/book-card.ts | 22 ++++ .../demo/src/app/book-list/book-list.html | 28 ---- .../demo/src/app/book-list/book-list.spec.ts | 91 ------------- .../demo/src/app/book-list/book-list.ts | 30 ----- .../demo/src/app/book.store.spec.ts | 99 ++++++-------- .../demo/src/app/book.store.ts | 51 +++---- .../src/app/books-overview/books-overview.css | 24 ++++ .../app/books-overview/books-overview.html | 38 ++++++ .../app/books-overview/books-overview.spec.ts | 93 +++++++++++++ .../src/app/books-overview/books-overview.ts | 45 +++++++ .../demo/src/app/shared/book-api.ts | 42 ------ .../demo/src/app/shared/book-store.spec.ts | 78 +++++++++++ .../demo/src/app/shared/book-store.ts | 31 +++++ .../demo/src/app/shared/book.ts | 6 +- .../demo/src/app/shared/error-message.ts | 16 ++- .../ngrx-signal-store/demo/src/index.html | 2 +- 51 files changed, 1083 insertions(+), 610 deletions(-) create mode 100644 material/ngrx-global-store/demo/src/app/book-card/book-card.css create mode 100644 material/ngrx-global-store/demo/src/app/book-card/book-card.html create mode 100644 material/ngrx-global-store/demo/src/app/book-card/book-card.ts delete mode 100644 material/ngrx-global-store/demo/src/app/book-list/book-list.html delete mode 100644 material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts delete mode 100644 material/ngrx-global-store/demo/src/app/book-list/book-list.ts create mode 100644 material/ngrx-global-store/demo/src/app/books-overview/books-overview.css create mode 100644 material/ngrx-global-store/demo/src/app/books-overview/books-overview.html create mode 100644 material/ngrx-global-store/demo/src/app/books-overview/books-overview.spec.ts create mode 100644 material/ngrx-global-store/demo/src/app/books-overview/books-overview.ts delete mode 100644 material/ngrx-global-store/demo/src/app/shared/book-api.ts create mode 100644 material/ngrx-global-store/demo/src/app/shared/book-store.spec.ts create mode 100644 material/ngrx-global-store/demo/src/app/shared/book-store.ts create mode 100644 material/ngrx-signal-store/demo/src/app/book-card/book-card.css create mode 100644 material/ngrx-signal-store/demo/src/app/book-card/book-card.html create mode 100644 material/ngrx-signal-store/demo/src/app/book-card/book-card.ts delete mode 100644 material/ngrx-signal-store/demo/src/app/book-list/book-list.html delete mode 100644 material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts delete mode 100644 material/ngrx-signal-store/demo/src/app/book-list/book-list.ts create mode 100644 material/ngrx-signal-store/demo/src/app/books-overview/books-overview.css create mode 100644 material/ngrx-signal-store/demo/src/app/books-overview/books-overview.html create mode 100644 material/ngrx-signal-store/demo/src/app/books-overview/books-overview.spec.ts create mode 100644 material/ngrx-signal-store/demo/src/app/books-overview/books-overview.ts delete mode 100644 material/ngrx-signal-store/demo/src/app/shared/book-api.ts create mode 100644 material/ngrx-signal-store/demo/src/app/shared/book-store.spec.ts create mode 100644 material/ngrx-signal-store/demo/src/app/shared/book-store.ts diff --git a/material/ngrx-global-store/demo/angular.json b/material/ngrx-global-store/demo/angular.json index 980de3c1..3043937e 100644 --- a/material/ngrx-global-store/demo/angular.json +++ b/material/ngrx-global-store/demo/angular.json @@ -11,7 +11,7 @@ "schematics": {}, "root": "", "sourceRoot": "src", - "prefix": "bm", + "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", diff --git a/material/ngrx-global-store/demo/src/app/app.config.ts b/material/ngrx-global-store/demo/src/app/app.config.ts index 74874b49..6491b54d 100644 --- a/material/ngrx-global-store/demo/src/app/app.config.ts +++ b/material/ngrx-global-store/demo/src/app/app.config.ts @@ -1,4 +1,5 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideHttpClient, withFetch } from '@angular/common/http'; import { provideState, provideStore } from '@ngrx/store'; import { provideEffects } from '@ngrx/effects'; @@ -8,6 +9,7 @@ import { BookEffects } from './store/book.effects'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), + provideHttpClient(withFetch()), provideStore(), provideState(fromBook.bookFeatureKey, fromBook.reducer), provideEffects(BookEffects) diff --git a/material/ngrx-global-store/demo/src/app/app.html b/material/ngrx-global-store/demo/src/app/app.html index 2c3f1776..7061a422 100644 --- a/material/ngrx-global-store/demo/src/app/app.html +++ b/material/ngrx-global-store/demo/src/app/app.html @@ -1 +1 @@ - + diff --git a/material/ngrx-global-store/demo/src/app/app.spec.ts b/material/ngrx-global-store/demo/src/app/app.spec.ts index 31c6559f..cefac6cd 100644 --- a/material/ngrx-global-store/demo/src/app/app.spec.ts +++ b/material/ngrx-global-store/demo/src/app/app.spec.ts @@ -2,16 +2,22 @@ import { TestBed } from '@angular/core/testing'; import { provideMockStore } from '@ngrx/store/testing'; import { App } from './app'; -import { selectAllBooks, selectBooksError, selectBooksLoading } from './store/book.selectors'; +import { + selectAllBooks, + selectBooksError, + selectBooksLoading, + selectLikedBooks +} from './store/book.selectors'; describe('App', () => { - it('erzeugt die App und zeigt die Buchliste an', async () => { + it('erzeugt die App und zeigt die Bücher-Übersicht an', async () => { await TestBed.configureTestingModule({ imports: [App], providers: [ provideMockStore({ selectors: [ { selector: selectAllBooks, value: [] }, + { selector: selectLikedBooks, value: [] }, { selector: selectBooksLoading, value: false }, { selector: selectBooksError, value: null } ] @@ -22,7 +28,7 @@ describe('App', () => { const fixture = TestBed.createComponent(App); await fixture.whenStable(); - const h1 = fixture.nativeElement.querySelector('h1') as HTMLElement; - expect(h1?.textContent).toContain('Books'); + expect(fixture.nativeElement.querySelector('app-books-overview')).toBeTruthy(); + expect(fixture.nativeElement.textContent).toContain('Bücher'); }); }); diff --git a/material/ngrx-global-store/demo/src/app/app.ts b/material/ngrx-global-store/demo/src/app/app.ts index 120a9d7e..47c6d2a1 100644 --- a/material/ngrx-global-store/demo/src/app/app.ts +++ b/material/ngrx-global-store/demo/src/app/app.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { BookList } from './book-list/book-list'; +import { BooksOverview } from './books-overview/books-overview'; @Component({ - selector: 'bm-root', - imports: [BookList], + selector: 'app-root', + imports: [BooksOverview], templateUrl: './app.html', changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/material/ngrx-global-store/demo/src/app/book-card/book-card.css b/material/ngrx-global-store/demo/src/app/book-card/book-card.css new file mode 100644 index 00000000..42ab120d --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/book-card/book-card.css @@ -0,0 +1,48 @@ +.book-card { + display: flex; + flex-direction: column; + border: 1px solid #ddd; + border-radius: 8px; + overflow: hidden; + background: #fff; +} + +.book-card__cover { + width: 100%; + height: 160px; + object-fit: cover; + background: #f0f0f0; +} + +.book-card__body { + flex: 1; + padding: 0.75rem; +} + +.book-card__body h3 { + margin: 0 0 0.25rem; + font-size: 1rem; +} + +.book-card__subtitle { + margin: 0 0 0.25rem; + color: #555; + font-style: italic; +} + +.book-card__authors { + margin: 0 0 0.25rem; + font-size: 0.85rem; +} + +.book-card__isbn { + margin: 0; + font-size: 0.8rem; + color: #888; +} + +.book-card__footer { + display: flex; + gap: 0.5rem; + padding: 0.5rem 0.75rem 0.75rem; +} diff --git a/material/ngrx-global-store/demo/src/app/book-card/book-card.html b/material/ngrx-global-store/demo/src/app/book-card/book-card.html new file mode 100644 index 00000000..29a417cf --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/book-card/book-card.html @@ -0,0 +1,17 @@ +@let b = book(); + +
                      + +
                      +

                      {{ b.title }}

                      + @if (b.subtitle) { +

                      {{ b.subtitle }}

                      + } +

                      {{ b.authors.join(', ') }}

                      +

                      ISBN {{ b.isbn }}

                      +
                      +
                      + + +
                      +
                      diff --git a/material/ngrx-global-store/demo/src/app/book-card/book-card.ts b/material/ngrx-global-store/demo/src/app/book-card/book-card.ts new file mode 100644 index 00000000..e4a01934 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/book-card/book-card.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { Book } from '../shared/book'; + +@Component({ + selector: 'app-book-card', + templateUrl: './book-card.html', + styleUrl: './book-card.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BookCard { + readonly book = input.required(); + readonly like = output(); + readonly remove = output(); + + likeBook(): void { + this.like.emit(this.book()); + } + + removeBook(): void { + this.remove.emit(this.book().isbn); + } +} diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.html b/material/ngrx-global-store/demo/src/app/book-list/book-list.html deleted file mode 100644 index 5a0f58a4..00000000 --- a/material/ngrx-global-store/demo/src/app/book-list/book-list.html +++ /dev/null @@ -1,28 +0,0 @@ -

                      Books ({{ books().length }})

                      - -@if (loading()) { -
                      Loading ...
                      -} - -@if (error(); as error) { -
                      - {{ error }} - -
                      -} - -
                      - - - -
                      - -
                        - @for (book of books(); track book.isbn) { -
                      • - {{ book.title }} – ★ {{ book.rating ?? 0 }} - - -
                      • - } -
                      diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts b/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts deleted file mode 100644 index 53501b83..00000000 --- a/material/ngrx-global-store/demo/src/app/book-list/book-list.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import type { MockInstance } from 'vitest'; - -import { BookList } from './book-list'; -import * as BookActions from '../store/book.actions'; -import { selectAllBooks, selectBooksError, selectBooksLoading } from '../store/book.selectors'; -import { Book } from '../shared/book'; - -const b = (isbn: string, title = `Title ${isbn}`, rating = 0): Book => ({ isbn, title, rating }); - -interface Setup { - fixture: ComponentFixture; - dispatch: MockInstance<(action: unknown) => void>; - el: HTMLElement; -} - -describe('BookList', () => { - async function setup(books: Book[] = [], loading = false, error: string | null = null): Promise { - await TestBed.configureTestingModule({ - imports: [BookList], - providers: [ - provideMockStore({ - selectors: [ - { selector: selectAllBooks, value: books }, - { selector: selectBooksLoading, value: loading }, - { selector: selectBooksError, value: error } - ] - }) - ] - }).compileComponents(); - - const store = TestBed.inject(MockStore); - const dispatch = vi.spyOn(store, 'dispatch'); - const fixture = TestBed.createComponent(BookList); - await fixture.whenStable(); - return { fixture, dispatch, el: fixture.nativeElement }; - } - - it('dispatcht loadBooks beim Erzeugen', async () => { - const { dispatch } = await setup(); - expect(dispatch).toHaveBeenCalledWith(BookActions.loadBooks()); - }); - - it('rendert die Buchliste', async () => { - const { el } = await setup([b('1', 'Eins'), b('2', 'Zwei')]); - const items = el.querySelectorAll('li'); - expect(items.length).toBe(2); - expect(items[0].textContent).toContain('Eins'); - }); - - it('zeigt den Ladeindikator', async () => { - const { el } = await setup([], true); - expect(el.querySelector('.loader')).toBeTruthy(); - }); - - it('zeigt eine Fehlermeldung und dispatcht clearError beim Klick auf OK', async () => { - const { el, dispatch } = await setup([], false, 'Kaputt'); - expect(el.querySelector('.error')?.textContent).toContain('Kaputt'); - dispatch.mockClear(); - (el.querySelector('.error button') as HTMLButtonElement).click(); - expect(dispatch).toHaveBeenCalledWith(BookActions.clearError()); - }); - - it('dispatcht createBook beim Anlegen und leert die Felder', async () => { - const { el, dispatch } = await setup(); - dispatch.mockClear(); - const [isbn, title] = el.querySelectorAll('.add-form input'); - isbn.value = '111-1'; - title.value = 'Neues Buch'; - (el.querySelector('.add-form button') as HTMLButtonElement).click(); - expect(dispatch).toHaveBeenCalledWith( - BookActions.createBook({ book: { isbn: '111-1', title: 'Neues Buch', rating: 0 } }) - ); - expect(isbn.value).toBe(''); - expect(title.value).toBe(''); - }); - - it('legt nichts an, wenn die Felder leer sind', async () => { - const { el, dispatch } = await setup(); - dispatch.mockClear(); - (el.querySelector('.add-form button') as HTMLButtonElement).click(); - expect(dispatch).not.toHaveBeenCalled(); - }); - - it('rateUp erhöht die Bewertung und deckelt sie bei 5', async () => { - const { el, dispatch } = await setup([b('1', 'Eins', 4), b('2', 'Zwei', 5)]); - dispatch.mockClear(); - const rateButtons = el.querySelectorAll('li button:first-of-type'); - rateButtons[0].click(); // 4 -> 5 - expect(dispatch).toHaveBeenCalledWith(BookActions.updateBook({ book: { isbn: '1', title: 'Eins', rating: 5 } })); - dispatch.mockClear(); - rateButtons[1].click(); // 5 -> bleibt 5 - expect(dispatch).toHaveBeenCalledWith(BookActions.updateBook({ book: { isbn: '2', title: 'Zwei', rating: 5 } })); - }); - - it('dispatcht deleteBook beim Klick auf Löschen', async () => { - const { el, dispatch } = await setup([b('1')]); - dispatch.mockClear(); - (el.querySelector('li button:last-child') as HTMLButtonElement).click(); - expect(dispatch).toHaveBeenCalledWith(BookActions.deleteBook({ isbn: '1' })); - }); -}); diff --git a/material/ngrx-global-store/demo/src/app/book-list/book-list.ts b/material/ngrx-global-store/demo/src/app/book-list/book-list.ts deleted file mode 100644 index f0209cd3..00000000 --- a/material/ngrx-global-store/demo/src/app/book-list/book-list.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { Store } from '@ngrx/store'; - -import * as BookActions from '../store/book.actions'; -import { selectAllBooks, selectBooksError, selectBooksLoading } from '../store/book.selectors'; -import { Book } from '../shared/book'; - -@Component({ - selector: 'bm-book-list', - templateUrl: './book-list.html', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class BookList { - private store = inject(Store); - - books = this.store.selectSignal(selectAllBooks); - loading = this.store.selectSignal(selectBooksLoading); - error = this.store.selectSignal(selectBooksError); - - constructor() { - this.store.dispatch(BookActions.loadBooks()); - } - - addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { - if (!isbn.value || !title.value) { - return; - } - this.store.dispatch(BookActions.createBook({ book: { isbn: isbn.value, title: title.value, rating: 0 } })); - isbn.value = ''; - title.value = ''; - } - - rateUp(book: Book): void { - const rating = Math.min((book.rating ?? 0) + 1, 5); - this.store.dispatch(BookActions.updateBook({ book: { ...book, rating } })); - } - - deleteBook(isbn: string): void { - this.store.dispatch(BookActions.deleteBook({ isbn })); - } - - clearError(): void { - this.store.dispatch(BookActions.clearError()); - } -} diff --git a/material/ngrx-global-store/demo/src/app/books-overview/books-overview.css b/material/ngrx-global-store/demo/src/app/books-overview/books-overview.css new file mode 100644 index 00000000..df347da1 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/books-overview/books-overview.css @@ -0,0 +1,24 @@ +section { + margin-bottom: 2rem; +} + +.add-form { + display: flex; + gap: 0.5rem; + margin: 1rem 0; +} + +.book-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; +} + +.loader { + font-style: italic; + color: #555; +} + +.error { + color: #b00020; +} diff --git a/material/ngrx-global-store/demo/src/app/books-overview/books-overview.html b/material/ngrx-global-store/demo/src/app/books-overview/books-overview.html new file mode 100644 index 00000000..958ab412 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/books-overview/books-overview.html @@ -0,0 +1,38 @@ +
                      +

                      Favoriten ({{ likedBooks().length }})

                      + +
                        + @for (book of likedBooks(); track book.isbn) { +
                      • {{ book.title }}
                      • + } @empty { +
                      • Noch keine Favoriten.
                      • + } +
                      +
                      + +
                      +

                      Bücher ({{ books().length }})

                      + + @if (loading()) { +

                      Lädt …

                      + } + + @if (error(); as error) { +

                      + {{ error }} + +

                      + } + +
                      + + + +
                      + +
                      + @for (book of books(); track book.isbn) { + + } +
                      +
                      diff --git a/material/ngrx-global-store/demo/src/app/books-overview/books-overview.spec.ts b/material/ngrx-global-store/demo/src/app/books-overview/books-overview.spec.ts new file mode 100644 index 00000000..090677bb --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/books-overview/books-overview.spec.ts @@ -0,0 +1,124 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import type { MockInstance } from 'vitest'; + +import { BooksOverview } from './books-overview'; +import * as BookActions from '../store/book.actions'; +import { + selectAllBooks, + selectBooksError, + selectBooksLoading, + selectLikedBooks +} from '../store/book.selectors'; +import { Book } from '../shared/book'; + +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ + isbn, + title, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); + +interface Setup { + fixture: ComponentFixture; + dispatch: MockInstance<(action: unknown) => void>; + el: HTMLElement; +} + +describe('BooksOverview', () => { + async function setup( + books: Book[] = [], + liked: Book[] = [], + loading = false, + error: string | null = null + ): Promise { + await TestBed.configureTestingModule({ + imports: [BooksOverview], + providers: [ + provideMockStore({ + selectors: [ + { selector: selectAllBooks, value: books }, + { selector: selectLikedBooks, value: liked }, + { selector: selectBooksLoading, value: loading }, + { selector: selectBooksError, value: error } + ] + }) + ] + }).compileComponents(); + + const store = TestBed.inject(MockStore); + const dispatch = vi.spyOn(store, 'dispatch'); + const fixture = TestBed.createComponent(BooksOverview); + await fixture.whenStable(); + return { fixture, dispatch, el: fixture.nativeElement }; + } + + it('dispatcht loadBooks beim Erzeugen', async () => { + const { dispatch } = await setup(); + expect(dispatch).toHaveBeenCalledWith(BookActions.loadBooks()); + }); + + it('rendert eine Karte pro Buch', async () => { + const { el } = await setup([b('1'), b('2')]); + expect(el.querySelectorAll('app-book-card').length).toBe(2); + }); + + it('zeigt die Favoriten', async () => { + const { el } = await setup([], [b('1', 'Lieblingsbuch')]); + expect(el.querySelector('.favorites')?.textContent).toContain('Lieblingsbuch'); + }); + + it('dispatcht likeBook beim Klick auf Favorit', async () => { + const book = b('1'); + const { el, dispatch } = await setup([book]); + dispatch.mockClear(); + const likeButton = el.querySelector('app-book-card .book-card__footer button') as HTMLButtonElement; + likeButton.click(); + expect(dispatch).toHaveBeenCalledWith(BookActions.likeBook({ book })); + }); + + it('dispatcht deleteBook beim Klick auf Löschen', async () => { + const { el, dispatch } = await setup([b('1')]); + dispatch.mockClear(); + const buttons = el.querySelectorAll('app-book-card .book-card__footer button'); + (buttons[1] as HTMLButtonElement).click(); + expect(dispatch).toHaveBeenCalledWith(BookActions.deleteBook({ isbn: '1' })); + }); + + it('dispatcht createBook beim Anlegen und leert die Felder', async () => { + const { el, dispatch } = await setup(); + dispatch.mockClear(); + const [isbn, title] = el.querySelectorAll('.add-form input'); + isbn.value = '978-x'; + title.value = 'Neu'; + (el.querySelector('.add-form button') as HTMLButtonElement).click(); + + const action = dispatch.mock.calls.at(-1)?.[0] as ReturnType; + expect(action.type).toBe(BookActions.createBook.type); + expect(action.book.isbn).toBe('978-x'); + expect(action.book.title).toBe('Neu'); + expect(isbn.value).toBe(''); + }); + + it('dispatcht clearLikedBooks beim Leeren', async () => { + const { el, dispatch } = await setup([], [b('1')]); + dispatch.mockClear(); + (el.querySelector('.favorites button') as HTMLButtonElement).click(); + expect(dispatch).toHaveBeenCalledWith(BookActions.clearLikedBooks()); + }); + + it('zeigt eine Fehlermeldung und dispatcht clearError beim Klick auf OK', async () => { + const { el, dispatch } = await setup([], [], false, 'Kaputt'); + expect(el.querySelector('.error')?.textContent).toContain('Kaputt'); + dispatch.mockClear(); + (el.querySelector('.error button') as HTMLButtonElement).click(); + expect(dispatch).toHaveBeenCalledWith(BookActions.clearError()); + }); + + it('zeigt den Ladeindikator', async () => { + const { el } = await setup([], [], true); + expect(el.querySelector('.loader')).toBeTruthy(); + }); +}); diff --git a/material/ngrx-global-store/demo/src/app/books-overview/books-overview.ts b/material/ngrx-global-store/demo/src/app/books-overview/books-overview.ts new file mode 100644 index 00000000..78e7d0c3 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/books-overview/books-overview.ts @@ -0,0 +1,69 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { Store } from '@ngrx/store'; + +import * as BookActions from '../store/book.actions'; +import { + selectAllBooks, + selectBooksError, + selectBooksLoading, + selectLikedBooks +} from '../store/book.selectors'; +import { Book } from '../shared/book'; +import { BookCard } from '../book-card/book-card'; + +@Component({ + selector: 'app-books-overview', + imports: [BookCard], + templateUrl: './books-overview.html', + styleUrl: './books-overview.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BooksOverview { + private store = inject(Store); + + books = this.store.selectSignal(selectAllBooks); + loading = this.store.selectSignal(selectBooksLoading); + error = this.store.selectSignal(selectBooksError); + likedBooks = this.store.selectSignal(selectLikedBooks); + + constructor() { + this.store.dispatch(BookActions.loadBooks()); + } + + addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { + if (!isbn.value || !title.value) { + return; + } + this.store.dispatch(BookActions.createBook({ book: this.#newBook(isbn.value, title.value) })); + isbn.value = ''; + title.value = ''; + } + + deleteBook(isbn: string): void { + this.store.dispatch(BookActions.deleteBook({ isbn })); + } + + likeBook(book: Book): void { + this.store.dispatch(BookActions.likeBook({ book })); + } + + clearLikedBooks(): void { + this.store.dispatch(BookActions.clearLikedBooks()); + } + + clearError(): void { + this.store.dispatch(BookActions.clearError()); + } + + // Ein vollständiges Buch mit sinnvollen Defaults, damit die echte API den POST annimmt. + #newBook(isbn: string, title: string): Book { + return { + isbn, + title, + authors: ['Unbekannt'], + description: 'Über die Demo angelegt.', + imageUrl: 'https://cdn.ng-buch.de/cover-placeholder.png', + createdAt: new Date().toISOString() + }; + } +} diff --git a/material/ngrx-global-store/demo/src/app/shared/book-api.ts b/material/ngrx-global-store/demo/src/app/shared/book-api.ts deleted file mode 100644 index e3fdb942..00000000 --- a/material/ngrx-global-store/demo/src/app/shared/book-api.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable, of, throwError } from 'rxjs'; -import { Book } from './book'; - -/** - * In-Memory-Stand-in für ein echtes HTTP-Backend. - * Die Methoden geben – wie ein HttpClient-Service – Observables zurück, - * sodass der Effect-Code identisch zum Artikel bleibt. - * - * `create()` wirft bewusst einen Fehler, wenn die ISBN bereits existiert – - * so lässt sich die Fehleranzeige der Anwendung auch ohne Server auslösen. - */ -@Injectable({ providedIn: 'root' }) -export class BookApi { - private books: Book[] = [ - { isbn: '978-3-86490-900-1', title: 'Angular', rating: 5 }, - { isbn: '978-3-86490-466-2', title: 'Angular für Einsteiger', rating: 4 }, - { isbn: '978-3-96009-187-4', title: 'Webentwicklung mit Angular', rating: 4 } - ]; - - getAll(): Observable { - return of(this.books.map(book => ({ ...book }))); - } - - create(book: Book): Observable { - if (this.books.some(b => b.isbn === book.isbn)) { - return throwError(() => new Error(`Ein Buch mit der ISBN ${book.isbn} existiert bereits.`)); - } - this.books = [...this.books, { ...book }]; - return of({ ...book }); - } - - update(book: Book): Observable { - this.books = this.books.map(b => (b.isbn === book.isbn ? { ...book } : b)); - return of({ ...book }); - } - - remove(isbn: string): Observable { - this.books = this.books.filter(b => b.isbn !== isbn); - return of(undefined); - } -} diff --git a/material/ngrx-global-store/demo/src/app/shared/book-store.spec.ts b/material/ngrx-global-store/demo/src/app/shared/book-store.spec.ts new file mode 100644 index 00000000..98e5a293 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/shared/book-store.spec.ts @@ -0,0 +1,78 @@ +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; + +import { BookStore } from './book-store'; +import { Book } from './book'; + +const API = 'https://api1.angular-buch.com'; +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ + isbn, + title, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); + +describe('BookStore (HTTP)', () => { + let store: BookStore; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideHttpClientTesting()] + }); + store = TestBed.inject(BookStore); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => httpMock.verify()); + + it('getAll lädt die Bücher per GET', () => { + const books = [b('1'), b('2')]; + let result: Book[] | undefined; + store.getAll().subscribe(r => (result = r)); + + const req = httpMock.expectOne(`${API}/books`); + expect(req.request.method).toBe('GET'); + req.flush(books); + + expect(result).toEqual(books); + }); + + it('create schickt einen POST und liefert das angelegte Buch', () => { + const book = b('3', 'Neu'); + let result: Book | undefined; + store.create(book).subscribe(r => (result = r)); + + const req = httpMock.expectOne(`${API}/books`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(book); + req.flush(book); + + expect(result).toEqual(book); + }); + + it('create reicht einen Fehler bei doppelter ISBN (HTTP 409) durch', () => { + let failed = false; + store.create(b('1')).subscribe({ error: () => (failed = true) }); + + httpMock + .expectOne(`${API}/books`) + .flush({ error: 'ISBN already exists' }, { status: 409, statusText: 'Conflict' }); + + expect(failed).toBe(true); + }); + + it('remove schickt einen DELETE auf die ISBN', () => { + let done = false; + store.remove('1').subscribe(() => (done = true)); + + const req = httpMock.expectOne(`${API}/books/1`); + expect(req.request.method).toBe('DELETE'); + req.flush(null); + + expect(done).toBe(true); + }); +}); diff --git a/material/ngrx-global-store/demo/src/app/shared/book-store.ts b/material/ngrx-global-store/demo/src/app/shared/book-store.ts new file mode 100644 index 00000000..7b60ff22 --- /dev/null +++ b/material/ngrx-global-store/demo/src/app/shared/book-store.ts @@ -0,0 +1,32 @@ +import { inject, Service } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { Book } from './book'; + +/** + * Datenzugriff auf die echte BookManager-API (wie im Buch). + * Eine öffentlich gehostete Instanz liegt unter `api1.angular-buch.com`, + * sodass die Demo ohne eigenes Backend läuft. + * + * Wir geben hier bewusst `Observable`s zurück (statt `httpResource`/`Promise` + * wie in späteren Buch-Iterationen), weil unsere Effects bzw. `rxMethod` + * direkt mit Observables arbeiten. + */ +@Service() +export class BookStore { + #http = inject(HttpClient); + #apiUrl = 'https://api1.angular-buch.com'; + + getAll(): Observable { + return this.#http.get(`${this.#apiUrl}/books`); + } + + create(book: Book): Observable { + return this.#http.post(`${this.#apiUrl}/books`, book); + } + + remove(isbn: string): Observable { + return this.#http.delete(`${this.#apiUrl}/books/${isbn}`); + } +} diff --git a/material/ngrx-global-store/demo/src/app/shared/book.ts b/material/ngrx-global-store/demo/src/app/shared/book.ts index fda322a3..0a527c1c 100644 --- a/material/ngrx-global-store/demo/src/app/shared/book.ts +++ b/material/ngrx-global-store/demo/src/app/shared/book.ts @@ -1,5 +1,9 @@ export interface Book { isbn: string; title: string; - rating?: number; + subtitle?: string; + authors: string[]; + description: string; + imageUrl: string; + createdAt: string; } diff --git a/material/ngrx-global-store/demo/src/app/shared/error-message.ts b/material/ngrx-global-store/demo/src/app/shared/error-message.ts index ca813472..aab77faa 100644 --- a/material/ngrx-global-store/demo/src/app/shared/error-message.ts +++ b/material/ngrx-global-store/demo/src/app/shared/error-message.ts @@ -1,12 +1,14 @@ +import { HttpErrorResponse } from '@angular/common/http'; + /** - * Wandelt einen unbekannten Fehler (z. B. aus `catchError` oder `tapResponse`) - * sicher in eine lesbare Meldung um. Der Fehler ist als `unknown` typisiert – - * wir prüfen die Form, statt sie blind anzunehmen. - * - * Hinweis: Eine `HttpErrorResponse` ist KEIN `instanceof Error` und liefe hier - * in den Fallback. Für ein echtes HttpClient-Backend würden wir die Prüfung - * erweitern, z. B. `if (error instanceof HttpErrorResponse) return error.message;`. + * Wandelt einen unbekannten Fehler sicher in eine lesbare Meldung um. + * Der Fehler ist als `unknown` typisiert – wir prüfen die Form, statt sie + * blind anzunehmen. Die BookManager-API liefert ihre Fehler als `{ error: string }` + * (z. B. HTTP 409 bei doppelter ISBN), verpackt in einer `HttpErrorResponse`. */ export function toMessage(error: unknown): string { + if (error instanceof HttpErrorResponse) { + return error.error?.error ?? error.message; + } return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; } diff --git a/material/ngrx-global-store/demo/src/app/store/book.actions.ts b/material/ngrx-global-store/demo/src/app/store/book.actions.ts index d46d170e..e36059bc 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.actions.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.actions.ts @@ -1,20 +1,23 @@ import { createAction, props } from '@ngrx/store'; import { Book } from '../shared/book'; +// Laden export const loadBooks = createAction('[Book] Load Books'); export const loadBooksSuccess = createAction('[Book] Load Books Success', props<{ data: Book[] }>()); export const loadBooksFailure = createAction('[Book] Load Books Failure', props<{ error: string }>()); -export const clearError = createAction('[Book] Clear Error'); - +// Anlegen export const createBook = createAction('[Book] Create Book', props<{ book: Book }>()); export const createBookSuccess = createAction('[Book] Create Book Success', props<{ book: Book }>()); export const createBookFailure = createAction('[Book] Create Book Failure', props<{ error: string }>()); -export const updateBook = createAction('[Book] Update Book', props<{ book: Book }>()); -export const updateBookSuccess = createAction('[Book] Update Book Success', props<{ book: Book }>()); -export const updateBookFailure = createAction('[Book] Update Book Failure', props<{ error: string }>()); - +// Löschen export const deleteBook = createAction('[Book] Delete Book', props<{ isbn: string }>()); export const deleteBookSuccess = createAction('[Book] Delete Book Success', props<{ isbn: string }>()); export const deleteBookFailure = createAction('[Book] Delete Book Failure', props<{ error: string }>()); + +export const clearError = createAction('[Book] Clear Error'); + +// Favoriten – reiner Client-State, ganz ohne Seiteneffekt +export const likeBook = createAction('[Book] Like Book', props<{ book: Book }>()); +export const clearLikedBooks = createAction('[Book] Clear Liked Books'); diff --git a/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts index db4be9bb..8b9c3b50 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.effects.spec.ts @@ -4,21 +4,28 @@ import { Action } from '@ngrx/store'; import { provideMockActions } from '@ngrx/effects/testing'; import { BookEffects } from './book.effects'; -import { BookApi } from '../shared/book-api'; +import { BookStore } from '../shared/book-store'; import * as BookActions from './book.actions'; import { Book } from '../shared/book'; -const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ + isbn, + title, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); describe('BookEffects', () => { let actions$: Observable; - function setup(serviceMock: Partial): BookEffects { + function setup(mock: Partial): BookEffects { TestBed.configureTestingModule({ providers: [ BookEffects, provideMockActions(() => actions$), - { provide: BookApi, useValue: serviceMock } + { provide: BookStore, useValue: mock } ] }); return TestBed.inject(BookEffects); @@ -55,32 +62,13 @@ describe('BookEffects', () => { it('createBook$ feuert createBookFailure bei einem Fehler', () => { const effects = setup({ create: () => throwError(() => new Error('Doppelte ISBN')) }); - actions$ = of(BookActions.createBook({ book: b('3', 'Neu') })); + actions$ = of(BookActions.createBook({ book: b('3') })); let result: Action | undefined; effects.createBook$.subscribe(action => (result = action)); expect(result).toEqual(BookActions.createBookFailure({ error: 'Doppelte ISBN' })); }); - it('updateBook$ feuert updateBookSuccess', () => { - const book = b('1', 'Geändert'); - const effects = setup({ update: x => of(x) }); - actions$ = of(BookActions.updateBook({ book })); - - let result: Action | undefined; - effects.updateBook$.subscribe(action => (result = action)); - expect(result).toEqual(BookActions.updateBookSuccess({ book })); - }); - - it('updateBook$ feuert updateBookFailure bei einem Fehler', () => { - const effects = setup({ update: () => throwError(() => new Error('Speicherfehler')) }); - actions$ = of(BookActions.updateBook({ book: b('1') })); - - let result: Action | undefined; - effects.updateBook$.subscribe(action => (result = action)); - expect(result).toEqual(BookActions.updateBookFailure({ error: 'Speicherfehler' })); - }); - it('deleteBook$ feuert deleteBookSuccess mit der ISBN', () => { const effects = setup({ remove: () => of(undefined) }); actions$ = of(BookActions.deleteBook({ isbn: '1' })); diff --git a/material/ngrx-global-store/demo/src/app/store/book.effects.ts b/material/ngrx-global-store/demo/src/app/store/book.effects.ts index ef3364cf..fbbeb207 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.effects.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.effects.ts @@ -4,22 +4,23 @@ import { catchError, concatMap, map, switchMap } from 'rxjs/operators'; import { of } from 'rxjs'; import * as BookActions from './book.actions'; -import { BookApi } from '../shared/book-api'; +import { BookStore } from '../shared/book-store'; import { toMessage } from '../shared/error-message'; @Injectable() export class BookEffects { private actions$ = inject(Actions); - private service = inject(BookApi); + private bookStore = inject(BookStore); // Lesen: switchMap – eine neue Anfrage macht die alte überflüssig. loadBooks$ = createEffect(() => this.actions$.pipe( ofType(BookActions.loadBooks), switchMap(() => - this.service.getAll().pipe( + this.bookStore.getAll().pipe( map(data => BookActions.loadBooksSuccess({ data })), - catchError((error: unknown) => of(BookActions.loadBooksFailure({ error: toMessage(error) }))) + catchError((error: unknown) => + of(BookActions.loadBooksFailure({ error: toMessage(error) }))) ) ) ) @@ -30,21 +31,10 @@ export class BookEffects { this.actions$.pipe( ofType(BookActions.createBook), concatMap(({ book }) => - this.service.create(book).pipe( + this.bookStore.create(book).pipe( map(created => BookActions.createBookSuccess({ book: created })), - catchError((error: unknown) => of(BookActions.createBookFailure({ error: toMessage(error) }))) - ) - ) - ) - ); - - updateBook$ = createEffect(() => - this.actions$.pipe( - ofType(BookActions.updateBook), - concatMap(({ book }) => - this.service.update(book).pipe( - map(updated => BookActions.updateBookSuccess({ book: updated })), - catchError((error: unknown) => of(BookActions.updateBookFailure({ error: toMessage(error) }))) + catchError((error: unknown) => + of(BookActions.createBookFailure({ error: toMessage(error) }))) ) ) ) @@ -54,9 +44,10 @@ export class BookEffects { this.actions$.pipe( ofType(BookActions.deleteBook), concatMap(({ isbn }) => - this.service.remove(isbn).pipe( + this.bookStore.remove(isbn).pipe( map(() => BookActions.deleteBookSuccess({ isbn })), - catchError((error: unknown) => of(BookActions.deleteBookFailure({ error: toMessage(error) }))) + catchError((error: unknown) => + of(BookActions.deleteBookFailure({ error: toMessage(error) }))) ) ) ) diff --git a/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts index f30ad9d3..98f9beed 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.reducer.spec.ts @@ -4,7 +4,14 @@ import { initialState, reducer, State } from './book.reducer'; import * as BookActions from './book.actions'; import { Book } from '../shared/book'; -const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ + isbn, + title, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); describe('Book Reducer', () => { it('loadBooks setzt loading=true und error=null', () => { @@ -35,13 +42,7 @@ describe('Book Reducer', () => { const start: State = { ...initialState, books: [b('1')] }; const state = reducer(start, BookActions.createBookSuccess({ book: b('2') })); expect(state.books.map(x => x.isbn)).toEqual(['1', '2']); - expect(start.books.length).toBe(1); // Original unverändert - }); - - it('updateBookSuccess ersetzt das passende Buch', () => { - const start: State = { ...initialState, books: [b('1', 'Alt'), b('2', 'Zwei')] }; - const state = reducer(start, BookActions.updateBookSuccess({ book: b('1', 'Neu') })); - expect(state.books.find(x => x.isbn === '1')?.title).toBe('Neu'); + expect(start.books.length).toBe(1); }); it('deleteBookSuccess entfernt das Buch anhand der ISBN', () => { @@ -50,17 +51,34 @@ describe('Book Reducer', () => { expect(state.books.map(x => x.isbn)).toEqual(['2']); }); - it('ein Schreibvorgang (Auslöser-Action) setzt eine alte Fehlermeldung zurück', () => { + it('ein Schreibvorgang (Auslöser) setzt eine alte Fehlermeldung zurück', () => { const state = reducer({ ...initialState, error: 'alt' }, BookActions.createBook({ book: b('9') })); expect(state.error).toBeNull(); }); - it('alle Failure-Actions setzen error (Mehrfach-on)', () => { + it('Failure-Actions setzen error', () => { expect(reducer(initialState, BookActions.createBookFailure({ error: 'c' })).error).toBe('c'); - expect(reducer(initialState, BookActions.updateBookFailure({ error: 'u' })).error).toBe('u'); expect(reducer(initialState, BookActions.deleteBookFailure({ error: 'd' })).error).toBe('d'); }); + it('likeBook fügt ein Buch zu den Favoriten hinzu', () => { + const state = reducer(initialState, BookActions.likeBook({ book: b('1') })); + expect(state.likedBooks.map(x => x.isbn)).toEqual(['1']); + }); + + it('likeBook dedupliziert anhand der ISBN', () => { + const start: State = { ...initialState, likedBooks: [b('1')] }; + const state = reducer(start, BookActions.likeBook({ book: b('1') })); + expect(state.likedBooks.length).toBe(1); + expect(state).toBe(start); // unverändert, keine neue Referenz + }); + + it('clearLikedBooks leert die Favoriten', () => { + const start: State = { ...initialState, likedBooks: [b('1'), b('2')] }; + const state = reducer(start, BookActions.clearLikedBooks()); + expect(state.likedBooks).toEqual([]); + }); + it('lässt den State bei unbekannter Action unverändert', () => { const state = reducer(initialState, { type: 'unbekannt' } as Action); expect(state).toBe(initialState); diff --git a/material/ngrx-global-store/demo/src/app/store/book.reducer.ts b/material/ngrx-global-store/demo/src/app/store/book.reducer.ts index d3c1edd8..fe421168 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.reducer.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.reducer.ts @@ -8,12 +8,14 @@ export interface State { books: Book[]; loading: boolean; error: string | null; + likedBooks: Book[]; } export const initialState: State = { books: [], loading: false, - error: null + error: null, + likedBooks: [] }; export const reducer = createReducer( @@ -34,21 +36,12 @@ export const reducer = createReducer( on(BookActions.clearError, (state): State => ({ ...state, error: null })), // Schreibvorgänge: beim Auslösen eine alte Fehlermeldung zurücksetzen. - on( - BookActions.createBook, - BookActions.updateBook, - BookActions.deleteBook, - (state): State => ({ ...state, error: null }) - ), + on(BookActions.createBook, BookActions.deleteBook, (state): State => ({ ...state, error: null })), on(BookActions.createBookSuccess, (state, action): State => ({ ...state, books: [...state.books, action.book] })), - on(BookActions.updateBookSuccess, (state, action): State => ({ - ...state, - books: state.books.map(b => (b.isbn === action.book.isbn ? action.book : b)) - })), on(BookActions.deleteBookSuccess, (state, action): State => ({ ...state, books: state.books.filter(b => b.isbn !== action.isbn) @@ -56,8 +49,15 @@ export const reducer = createReducer( on( BookActions.createBookFailure, - BookActions.updateBookFailure, BookActions.deleteBookFailure, (state, action): State => ({ ...state, error: action.error }) - ) + ), + + // Favoriten – reiner Client-State, dedupliziert anhand der ISBN, kein Effect nötig. + on(BookActions.likeBook, (state, action): State => + state.likedBooks.some(b => b.isbn === action.book.isbn) + ? state + : { ...state, likedBooks: [...state.likedBooks, action.book] } + ), + on(BookActions.clearLikedBooks, (state): State => ({ ...state, likedBooks: [] })) ); diff --git a/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts b/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts index 9b5c48da..d1dc0efd 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.selectors.spec.ts @@ -1,11 +1,29 @@ -import { selectAllBooks, selectBookState, selectBooksError, selectBooksLoading } from './book.selectors'; +import { + selectAllBooks, + selectBookState, + selectBooksError, + selectBooksLoading, + selectLikedBooks +} from './book.selectors'; import { bookFeatureKey, State } from './book.reducer'; import { Book } from '../shared/book'; -const b = (isbn: string): Book => ({ isbn, title: `Title ${isbn}` }); +const b = (isbn: string): Book => ({ + isbn, + title: `Titel ${isbn}`, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); describe('Book Selectors', () => { - const bookState: State = { books: [b('1'), b('2')], loading: true, error: 'x' }; + const bookState: State = { + books: [b('1'), b('2')], + loading: true, + error: 'x', + likedBooks: [b('1')] + }; const rootState: Record = { [bookFeatureKey]: bookState }; it('selectBookState liefert den Feature-State', () => { @@ -13,7 +31,7 @@ describe('Book Selectors', () => { }); it('selectAllBooks liefert die Buchliste', () => { - expect(selectAllBooks(rootState)).toEqual([b('1'), b('2')]); + expect(selectAllBooks(rootState).map(x => x.isbn)).toEqual(['1', '2']); }); it('selectBooksLoading liefert das loading-Flag', () => { @@ -23,4 +41,8 @@ describe('Book Selectors', () => { it('selectBooksError liefert die Fehlermeldung', () => { expect(selectBooksError(rootState)).toBe('x'); }); + + it('selectLikedBooks liefert die Favoriten', () => { + expect(selectLikedBooks(rootState).map(x => x.isbn)).toEqual(['1']); + }); }); diff --git a/material/ngrx-global-store/demo/src/app/store/book.selectors.ts b/material/ngrx-global-store/demo/src/app/store/book.selectors.ts index 8208871e..abc908b0 100644 --- a/material/ngrx-global-store/demo/src/app/store/book.selectors.ts +++ b/material/ngrx-global-store/demo/src/app/store/book.selectors.ts @@ -19,3 +19,8 @@ export const selectBooksError = createSelector( selectBookState, state => state.error ); + +export const selectLikedBooks = createSelector( + selectBookState, + state => state.likedBooks +); diff --git a/material/ngrx-global-store/demo/src/index.html b/material/ngrx-global-store/demo/src/index.html index e8f9c719..f9f06e72 100644 --- a/material/ngrx-global-store/demo/src/index.html +++ b/material/ngrx-global-store/demo/src/index.html @@ -8,6 +8,6 @@ - + diff --git a/material/ngrx-signal-store/demo/angular.json b/material/ngrx-signal-store/demo/angular.json index 980de3c1..3043937e 100644 --- a/material/ngrx-signal-store/demo/angular.json +++ b/material/ngrx-signal-store/demo/angular.json @@ -11,7 +11,7 @@ "schematics": {}, "root": "", "sourceRoot": "src", - "prefix": "bm", + "prefix": "app", "architect": { "build": { "builder": "@angular/build:application", diff --git a/material/ngrx-signal-store/demo/src/app/app.config.ts b/material/ngrx-signal-store/demo/src/app/app.config.ts index cb999ab9..b0282ae4 100644 --- a/material/ngrx-signal-store/demo/src/app/app.config.ts +++ b/material/ngrx-signal-store/demo/src/app/app.config.ts @@ -1,5 +1,9 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideHttpClient, withFetch } from '@angular/common/http'; export const appConfig: ApplicationConfig = { - providers: [provideBrowserGlobalErrorListeners()] + providers: [ + provideBrowserGlobalErrorListeners(), + provideHttpClient(withFetch()) + ] }; diff --git a/material/ngrx-signal-store/demo/src/app/app.html b/material/ngrx-signal-store/demo/src/app/app.html index 2c3f1776..7061a422 100644 --- a/material/ngrx-signal-store/demo/src/app/app.html +++ b/material/ngrx-signal-store/demo/src/app/app.html @@ -1 +1 @@ - + diff --git a/material/ngrx-signal-store/demo/src/app/app.spec.ts b/material/ngrx-signal-store/demo/src/app/app.spec.ts index 1dec1d3b..2c915141 100644 --- a/material/ngrx-signal-store/demo/src/app/app.spec.ts +++ b/material/ngrx-signal-store/demo/src/app/app.spec.ts @@ -1,21 +1,20 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; + import { App } from './app'; -import { BookApi } from './shared/book-api'; +import { BookStore } from './shared/book-store'; describe('App', () => { - it('erzeugt die App und zeigt die Buchliste an', async () => { + it('erzeugt die App und zeigt die Bücher-Übersicht an', async () => { await TestBed.configureTestingModule({ imports: [App], - providers: [ - { provide: BookApi, useValue: { getAll: () => of([]) } } - ] + providers: [{ provide: BookStore, useValue: { getAll: () => of([]) } }] }).compileComponents(); const fixture = TestBed.createComponent(App); await fixture.whenStable(); - const h1 = fixture.nativeElement.querySelector('h1') as HTMLElement; - expect(h1?.textContent).toContain('Books'); + expect(fixture.nativeElement.querySelector('app-books-overview')).toBeTruthy(); + expect(fixture.nativeElement.textContent).toContain('Bücher'); }); }); diff --git a/material/ngrx-signal-store/demo/src/app/app.ts b/material/ngrx-signal-store/demo/src/app/app.ts index 120a9d7e..47c6d2a1 100644 --- a/material/ngrx-signal-store/demo/src/app/app.ts +++ b/material/ngrx-signal-store/demo/src/app/app.ts @@ -1,9 +1,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { BookList } from './book-list/book-list'; +import { BooksOverview } from './books-overview/books-overview'; @Component({ - selector: 'bm-root', - imports: [BookList], + selector: 'app-root', + imports: [BooksOverview], templateUrl: './app.html', changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/material/ngrx-signal-store/demo/src/app/book-card/book-card.css b/material/ngrx-signal-store/demo/src/app/book-card/book-card.css new file mode 100644 index 00000000..42ab120d --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/book-card/book-card.css @@ -0,0 +1,48 @@ +.book-card { + display: flex; + flex-direction: column; + border: 1px solid #ddd; + border-radius: 8px; + overflow: hidden; + background: #fff; +} + +.book-card__cover { + width: 100%; + height: 160px; + object-fit: cover; + background: #f0f0f0; +} + +.book-card__body { + flex: 1; + padding: 0.75rem; +} + +.book-card__body h3 { + margin: 0 0 0.25rem; + font-size: 1rem; +} + +.book-card__subtitle { + margin: 0 0 0.25rem; + color: #555; + font-style: italic; +} + +.book-card__authors { + margin: 0 0 0.25rem; + font-size: 0.85rem; +} + +.book-card__isbn { + margin: 0; + font-size: 0.8rem; + color: #888; +} + +.book-card__footer { + display: flex; + gap: 0.5rem; + padding: 0.5rem 0.75rem 0.75rem; +} diff --git a/material/ngrx-signal-store/demo/src/app/book-card/book-card.html b/material/ngrx-signal-store/demo/src/app/book-card/book-card.html new file mode 100644 index 00000000..29a417cf --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/book-card/book-card.html @@ -0,0 +1,17 @@ +@let b = book(); + +
                      + +
                      +

                      {{ b.title }}

                      + @if (b.subtitle) { +

                      {{ b.subtitle }}

                      + } +

                      {{ b.authors.join(', ') }}

                      +

                      ISBN {{ b.isbn }}

                      +
                      +
                      + + +
                      +
                      diff --git a/material/ngrx-signal-store/demo/src/app/book-card/book-card.ts b/material/ngrx-signal-store/demo/src/app/book-card/book-card.ts new file mode 100644 index 00000000..e4a01934 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/book-card/book-card.ts @@ -0,0 +1,22 @@ +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { Book } from '../shared/book'; + +@Component({ + selector: 'app-book-card', + templateUrl: './book-card.html', + styleUrl: './book-card.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BookCard { + readonly book = input.required(); + readonly like = output(); + readonly remove = output(); + + likeBook(): void { + this.like.emit(this.book()); + } + + removeBook(): void { + this.remove.emit(this.book().isbn); + } +} diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.html b/material/ngrx-signal-store/demo/src/app/book-list/book-list.html deleted file mode 100644 index 45ee80f4..00000000 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.html +++ /dev/null @@ -1,28 +0,0 @@ -

                      Books ({{ store.booksCount() }})

                      - -@if (store.loading()) { -
                      Loading ...
                      -} - -@if (store.error(); as error) { -
                      - {{ error }} - -
                      -} - -
                      - - - -
                      - -
                        - @for (book of store.books(); track book.isbn) { -
                      • - {{ book.title }} – ★ {{ book.rating ?? 0 }} - - -
                      • - } -
                      diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts b/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts deleted file mode 100644 index da8dcdf9..00000000 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { of, throwError } from 'rxjs'; - -import { BookList } from './book-list'; -import { BookApi } from '../shared/book-api'; -import { Book } from '../shared/book'; - -const b = (isbn: string, title = `Title ${isbn}`, rating = 0): Book => ({ isbn, title, rating }); - -function serviceMock(books: Book[], overrides: Partial = {}): Partial { - return { - getAll: () => of(books), - create: (book: Book) => of(book), - update: (book: Book) => of(book), - remove: () => of(undefined), - ...overrides - }; -} - -describe('BookList (SignalStore)', () => { - async function setup(mock: Partial): Promise> { - await TestBed.configureTestingModule({ - imports: [BookList], - providers: [{ provide: BookApi, useValue: mock }] - }).compileComponents(); - - const fixture = TestBed.createComponent(BookList); - await fixture.whenStable(); - return fixture; - } - - it('rendert die vom Store geladenen Bücher', async () => { - const fixture = await setup(serviceMock([b('1', 'Eins'), b('2', 'Zwei')])); - const el = fixture.nativeElement as HTMLElement; - expect(el.querySelectorAll('li').length).toBe(2); - expect(el.querySelector('li')?.textContent).toContain('Eins'); - }); - - it('legt ein Buch an und leert die Felder', async () => { - const fixture = await setup(serviceMock([b('1', 'Eins')])); - const el = fixture.nativeElement as HTMLElement; - const [isbn, title] = el.querySelectorAll('.add-form input'); - isbn.value = '2'; - title.value = 'Zwei'; - (el.querySelector('.add-form button') as HTMLButtonElement).click(); - await fixture.whenStable(); - expect(el.querySelectorAll('li').length).toBe(2); - expect(isbn.value).toBe(''); - }); - - it('legt nichts an, wenn die Felder leer sind', async () => { - const fixture = await setup(serviceMock([b('1', 'Eins')])); - const el = fixture.nativeElement as HTMLElement; - (el.querySelector('.add-form button') as HTMLButtonElement).click(); - await fixture.whenStable(); - expect(el.querySelectorAll('li').length).toBe(1); - }); - - it('erhöht die Bewertung und deckelt sie bei 5', async () => { - const fixture = await setup(serviceMock([b('1', 'Eins', 4)])); - const el = fixture.nativeElement as HTMLElement; - const rateButton = el.querySelector('li button:first-of-type') as HTMLButtonElement; - - rateButton.click(); // 4 -> 5 - await fixture.whenStable(); - expect(el.querySelector('li')?.textContent).toContain('★ 5'); - - rateButton.click(); // bleibt bei 5 - await fixture.whenStable(); - expect(el.querySelector('li')?.textContent).toContain('★ 5'); - }); - - it('entfernt ein Buch beim Klick auf Löschen', async () => { - const fixture = await setup(serviceMock([b('1', 'Eins')])); - const el = fixture.nativeElement as HTMLElement; - (el.querySelector('li button:last-child') as HTMLButtonElement).click(); - await fixture.whenStable(); - expect(el.querySelectorAll('li').length).toBe(0); - }); - - it('blendet eine Fehlermeldung beim Klick auf OK wieder aus (clearError)', async () => { - const fixture = await setup(serviceMock([], { getAll: () => throwError(() => new Error('Kaputt')) })); - const el = fixture.nativeElement as HTMLElement; - expect(el.querySelector('.error')?.textContent).toContain('Kaputt'); - - (el.querySelector('.error button') as HTMLButtonElement).click(); - await fixture.whenStable(); - - expect(el.querySelector('.error')).toBeNull(); - }); -}); diff --git a/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts b/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts deleted file mode 100644 index b7d75eb9..00000000 --- a/material/ngrx-signal-store/demo/src/app/book-list/book-list.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { BookStore } from '../book.store'; -import { Book } from '../shared/book'; - -@Component({ - selector: 'bm-book-list', - templateUrl: './book-list.html', - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class BookList { - protected store = inject(BookStore); - - addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { - if (!isbn.value || !title.value) { - return; - } - this.store.addBook({ isbn: isbn.value, title: title.value, rating: 0 }); - isbn.value = ''; - title.value = ''; - } - - rateUp(book: Book): void { - const rating = Math.min((book.rating ?? 0) + 1, 5); - this.store.updateBook({ ...book, rating }); - } - - deleteBook(isbn: string): void { - this.store.deleteBook(isbn); - } -} diff --git a/material/ngrx-signal-store/demo/src/app/book.store.spec.ts b/material/ngrx-signal-store/demo/src/app/book.store.spec.ts index 96afefcb..86893a10 100644 --- a/material/ngrx-signal-store/demo/src/app/book.store.spec.ts +++ b/material/ngrx-signal-store/demo/src/app/book.store.spec.ts @@ -1,34 +1,39 @@ import { TestBed } from '@angular/core/testing'; import { of, Subject, throwError } from 'rxjs'; -import { BookStore } from './book.store'; -import { BookApi } from './shared/book-api'; +import { BookSignalStore } from './book.store'; +import { BookStore } from './shared/book-store'; import { Book } from './shared/book'; -const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ + isbn, + title, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); -/** Erzeugt einen frischen Store mit gemocktem BookApi. */ -function createStore(mock: Partial) { +/** Erzeugt einen frischen Store mit gemocktem BookStore-Service. */ +function createStore(mock: Partial) { TestBed.configureTestingModule({ - providers: [{ provide: BookApi, useValue: mock }] + providers: [{ provide: BookStore, useValue: mock }] }); - return TestBed.inject(BookStore); + return TestBed.inject(BookSignalStore); } -describe('BookStore (SignalStore)', () => { +describe('BookSignalStore', () => { it('lädt beim Init die Bücher und zählt sie', () => { const store = createStore({ getAll: () => of([b('1'), b('2'), b('3')]) }); expect(store.loading()).toBe(false); expect(store.error()).toBeNull(); - expect(store.books().length).toBe(3); expect(store.booksCount()).toBe(3); }); it('setzt loading=true, solange der Ladevorgang läuft', () => { const response = new Subject(); const store = createStore({ getAll: () => response }); - // onInit hat loadBooks() ausgelöst; die Antwort steht noch aus: expect(store.loading()).toBe(true); response.next([b('1')]); @@ -38,48 +43,20 @@ describe('BookStore (SignalStore)', () => { expect(store.booksCount()).toBe(1); }); - it('addBook hängt ein Buch immutabel an die Liste an', () => { - const store = createStore({ - getAll: () => of([b('1')]), - create: book => of(book) - }); - expect(store.booksCount()).toBe(1); - - store.addBook(b('2', 'Neues Buch')); - - expect(store.booksCount()).toBe(2); + it('addBook hängt ein Buch an die Liste an', () => { + const store = createStore({ getAll: () => of([b('1')]), create: book => of(book) }); + store.addBook(b('2', 'Neu')); expect(store.books().map(x => x.isbn)).toEqual(['1', '2']); }); - it('updateBook ersetzt das passende Buch', () => { - const store = createStore({ - getAll: () => of([b('1', 'Alt'), b('2', 'Zwei')]), - update: book => of(book) - }); - - store.updateBook(b('1', 'Neu')); - - expect(store.books().find(x => x.isbn === '1')?.title).toBe('Neu'); - expect(store.booksCount()).toBe(2); - }); - it('deleteBook entfernt das Buch anhand der ISBN', () => { - const store = createStore({ - getAll: () => of([b('1'), b('2')]), - remove: () => of(undefined) - }); - + const store = createStore({ getAll: () => of([b('1'), b('2')]), remove: () => of(undefined) }); store.deleteBook('1'); - - expect(store.booksCount()).toBe(1); - expect(store.books()[0].isbn).toBe('2'); + expect(store.books().map(x => x.isbn)).toEqual(['2']); }); it('schreibt eine Fehlermeldung in den State, wenn das Laden fehlschlägt', () => { - const store = createStore({ - getAll: () => throwError(() => new Error('Netzwerkfehler')) - }); - + const store = createStore({ getAll: () => throwError(() => new Error('Netzwerkfehler')) }); expect(store.error()).toBe('Netzwerkfehler'); expect(store.loading()).toBe(false); }); @@ -89,11 +66,16 @@ describe('BookStore (SignalStore)', () => { getAll: () => of([b('1')]), create: () => throwError(() => new Error('Doppelte ISBN')) }); - - store.addBook(b('1', 'Doppelt')); - + store.addBook(b('1')); expect(store.error()).toBe('Doppelte ISBN'); - expect(store.booksCount()).toBe(1); // Liste bleibt unverändert + expect(store.booksCount()).toBe(1); + }); + + it('clearError setzt die Fehlermeldung zurück', () => { + const store = createStore({ getAll: () => throwError(() => new Error('Netzwerkfehler')) }); + expect(store.error()).toBe('Netzwerkfehler'); + store.clearError(); + expect(store.error()).toBeNull(); }); it('ein erfolgreiches Schreiben räumt eine alte Fehlermeldung weg', () => { @@ -102,20 +84,21 @@ describe('BookStore (SignalStore)', () => { remove: () => of(undefined) }); expect(store.error()).toBe('Netzwerkfehler'); - store.deleteBook('1'); - expect(store.error()).toBeNull(); }); - it('clearError setzt die Fehlermeldung zurück', () => { - const store = createStore({ - getAll: () => throwError(() => new Error('Netzwerkfehler')) - }); - expect(store.error()).toBe('Netzwerkfehler'); - - store.clearError(); + it('likeBook fügt einen Favoriten hinzu und dedupliziert anhand der ISBN', () => { + const store = createStore({ getAll: () => of([]) }); + store.likeBook(b('1')); + store.likeBook(b('1')); + expect(store.likedCount()).toBe(1); + }); - expect(store.error()).toBeNull(); + it('clearLikedBooks leert die Favoriten', () => { + const store = createStore({ getAll: () => of([]) }); + store.likeBook(b('1')); + store.clearLikedBooks(); + expect(store.likedCount()).toBe(0); }); }); diff --git a/material/ngrx-signal-store/demo/src/app/book.store.ts b/material/ngrx-signal-store/demo/src/app/book.store.ts index 5ed01aaf..9f5c2764 100644 --- a/material/ngrx-signal-store/demo/src/app/book.store.ts +++ b/material/ngrx-signal-store/demo/src/app/book.store.ts @@ -12,36 +12,51 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { Book } from './shared/book'; -import { BookApi } from './shared/book-api'; +import { BookStore } from './shared/book-store'; import { toMessage } from './shared/error-message'; type BookState = { books: Book[]; loading: boolean; error: string | null; + likedBooks: Book[]; }; const initialState: BookState = { books: [], loading: false, - error: null + error: null, + likedBooks: [] }; -export const BookStore = signalStore( +export const BookSignalStore = signalStore( { providedIn: 'root' }, withState(initialState), - withComputed(({ books }) => ({ - booksCount: computed(() => books().length) + withComputed(({ books, likedBooks }) => ({ + booksCount: computed(() => books().length), + likedCount: computed(() => likedBooks().length) })), - withMethods((store, service = inject(BookApi)) => ({ + withMethods((store, bookStore = inject(BookStore)) => ({ clearError(): void { patchState(store, { error: null }); }, + + // Favoriten – reiner Client-State, dedupliziert, ganz ohne Seiteneffekt. + likeBook(book: Book): void { + if (!store.likedBooks().some(b => b.isbn === book.isbn)) { + patchState(store, state => ({ likedBooks: [...state.likedBooks, book] })); + } + }, + clearLikedBooks(): void { + patchState(store, { likedBooks: [] }); + }, + + // Lesen: switchMap – eine neue Anfrage macht die alte überflüssig. loadBooks: rxMethod( pipe( tap(() => patchState(store, { loading: true, error: null })), switchMap(() => - service.getAll().pipe( + bookStore.getAll().pipe( tapResponse({ next: books => patchState(store, { books }), error: (err: unknown) => patchState(store, { error: toMessage(err) }), @@ -51,11 +66,13 @@ export const BookStore = signalStore( ) ) ), + + // Schreiben: concatMap – laufende Requests werden nicht abgebrochen. addBook: rxMethod( pipe( tap(() => patchState(store, { error: null })), concatMap(book => - service.create(book).pipe( + bookStore.create(book).pipe( tapResponse({ next: created => patchState(store, state => ({ books: [...state.books, created] })), @@ -65,27 +82,11 @@ export const BookStore = signalStore( ) ) ), - updateBook: rxMethod( - pipe( - tap(() => patchState(store, { error: null })), - concatMap(book => - service.update(book).pipe( - tapResponse({ - next: updated => - patchState(store, state => ({ - books: state.books.map(b => (b.isbn === updated.isbn ? updated : b)) - })), - error: (err: unknown) => patchState(store, { error: toMessage(err) }) - }) - ) - ) - ) - ), deleteBook: rxMethod( pipe( tap(() => patchState(store, { error: null })), concatMap(isbn => - service.remove(isbn).pipe( + bookStore.remove(isbn).pipe( tapResponse({ next: () => patchState(store, state => ({ diff --git a/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.css b/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.css new file mode 100644 index 00000000..df347da1 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.css @@ -0,0 +1,24 @@ +section { + margin-bottom: 2rem; +} + +.add-form { + display: flex; + gap: 0.5rem; + margin: 1rem 0; +} + +.book-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; +} + +.loader { + font-style: italic; + color: #555; +} + +.error { + color: #b00020; +} diff --git a/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.html b/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.html new file mode 100644 index 00000000..73535121 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.html @@ -0,0 +1,38 @@ +
                      +

                      Favoriten ({{ store.likedCount() }})

                      + +
                        + @for (book of store.likedBooks(); track book.isbn) { +
                      • {{ book.title }}
                      • + } @empty { +
                      • Noch keine Favoriten.
                      • + } +
                      +
                      + +
                      +

                      Bücher ({{ store.booksCount() }})

                      + + @if (store.loading()) { +

                      Lädt …

                      + } + + @if (store.error(); as error) { +

                      + {{ error }} + +

                      + } + +
                      + + + +
                      + +
                      + @for (book of store.books(); track book.isbn) { + + } +
                      +
                      diff --git a/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.spec.ts b/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.spec.ts new file mode 100644 index 00000000..42f9730d --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.spec.ts @@ -0,0 +1,93 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { of, throwError } from 'rxjs'; + +import { BooksOverview } from './books-overview'; +import { BookStore } from '../shared/book-store'; +import { Book } from '../shared/book'; + +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ + isbn, + title, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); + +function serviceMock(books: Book[], overrides: Partial = {}): Partial { + return { + getAll: () => of(books), + create: (book: Book) => of(book), + remove: () => of(undefined), + ...overrides + }; +} + +describe('BooksOverview (SignalStore)', () => { + async function setup(mock: Partial): Promise> { + await TestBed.configureTestingModule({ + imports: [BooksOverview], + providers: [{ provide: BookStore, useValue: mock }] + }).compileComponents(); + + const fixture = TestBed.createComponent(BooksOverview); + await fixture.whenStable(); + return fixture; + } + + it('rendert die geladenen Bücher als Karten', async () => { + const fixture = await setup(serviceMock([b('1'), b('2')])); + expect(fixture.nativeElement.querySelectorAll('app-book-card').length).toBe(2); + }); + + it('legt ein Buch an und leert die Felder', async () => { + const fixture = await setup(serviceMock([b('1')])); + const el = fixture.nativeElement as HTMLElement; + const [isbn, title] = el.querySelectorAll('.add-form input'); + isbn.value = '2'; + title.value = 'Neu'; + (el.querySelector('.add-form button') as HTMLButtonElement).click(); + await fixture.whenStable(); + expect(el.querySelectorAll('app-book-card').length).toBe(2); + expect(isbn.value).toBe(''); + }); + + it('entfernt ein Buch beim Klick auf Löschen', async () => { + const fixture = await setup(serviceMock([b('1')])); + const el = fixture.nativeElement as HTMLElement; + const buttons = el.querySelectorAll('app-book-card .book-card__footer button'); + (buttons[1] as HTMLButtonElement).click(); + await fixture.whenStable(); + expect(el.querySelectorAll('app-book-card').length).toBe(0); + }); + + it('markiert ein Buch als Favorit', async () => { + const fixture = await setup(serviceMock([b('1', 'Lieblingsbuch')])); + const el = fixture.nativeElement as HTMLElement; + const likeButton = el.querySelector('app-book-card .book-card__footer button') as HTMLButtonElement; + likeButton.click(); + await fixture.whenStable(); + expect(el.querySelector('.favorites')?.textContent).toContain('Lieblingsbuch'); + }); + + it('leert die Favoriten', async () => { + const fixture = await setup(serviceMock([b('1', 'Lieblingsbuch')])); + const el = fixture.nativeElement as HTMLElement; + (el.querySelector('app-book-card .book-card__footer button') as HTMLButtonElement).click(); + await fixture.whenStable(); + (el.querySelector('.favorites button') as HTMLButtonElement).click(); + await fixture.whenStable(); + expect(el.querySelector('.favorites')?.textContent).toContain('Noch keine Favoriten'); + }); + + it('blendet eine Fehlermeldung beim Klick auf OK wieder aus', async () => { + const fixture = await setup(serviceMock([], { getAll: () => throwError(() => new Error('Kaputt')) })); + const el = fixture.nativeElement as HTMLElement; + expect(el.querySelector('.error')?.textContent).toContain('Kaputt'); + + (el.querySelector('.error button') as HTMLButtonElement).click(); + await fixture.whenStable(); + + expect(el.querySelector('.error')).toBeNull(); + }); +}); diff --git a/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.ts b/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.ts new file mode 100644 index 00000000..8873e26f --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/books-overview/books-overview.ts @@ -0,0 +1,45 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; + +import { Book } from '../shared/book'; +import { BookSignalStore } from '../book.store'; +import { BookCard } from '../book-card/book-card'; + +@Component({ + selector: 'app-books-overview', + imports: [BookCard], + templateUrl: './books-overview.html', + styleUrl: './books-overview.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BooksOverview { + protected store = inject(BookSignalStore); + + addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { + if (!isbn.value || !title.value) { + return; + } + this.store.addBook(this.#newBook(isbn.value, title.value)); + isbn.value = ''; + title.value = ''; + } + + deleteBook(isbn: string): void { + this.store.deleteBook(isbn); + } + + likeBook(book: Book): void { + this.store.likeBook(book); + } + + // Ein vollständiges Buch mit sinnvollen Defaults, damit die echte API den POST annimmt. + #newBook(isbn: string, title: string): Book { + return { + isbn, + title, + authors: ['Unbekannt'], + description: 'Über die Demo angelegt.', + imageUrl: 'https://cdn.ng-buch.de/cover-placeholder.png', + createdAt: new Date().toISOString() + }; + } +} diff --git a/material/ngrx-signal-store/demo/src/app/shared/book-api.ts b/material/ngrx-signal-store/demo/src/app/shared/book-api.ts deleted file mode 100644 index 404417b8..00000000 --- a/material/ngrx-signal-store/demo/src/app/shared/book-api.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Observable, of, throwError } from 'rxjs'; -import { Book } from './book'; - -/** - * In-Memory-Stand-in für ein echtes HTTP-Backend. - * Die Methoden geben – wie ein HttpClient-Service – Observables zurück, - * sodass der Store-Code identisch zum Artikel bleibt. - * - * `create()` wirft bewusst einen Fehler, wenn die ISBN bereits existiert – - * so lässt sich die Fehleranzeige der Anwendung auch ohne Server auslösen. - */ -@Injectable({ providedIn: 'root' }) -export class BookApi { - private books: Book[] = [ - { isbn: '978-3-86490-900-1', title: 'Angular', rating: 5 }, - { isbn: '978-3-86490-466-2', title: 'Angular für Einsteiger', rating: 4 }, - { isbn: '978-3-96009-187-4', title: 'Webentwicklung mit Angular', rating: 4 } - ]; - - getAll(): Observable { - return of(this.books.map(book => ({ ...book }))); - } - - create(book: Book): Observable { - if (this.books.some(b => b.isbn === book.isbn)) { - return throwError(() => new Error(`Ein Buch mit der ISBN ${book.isbn} existiert bereits.`)); - } - this.books = [...this.books, { ...book }]; - return of({ ...book }); - } - - update(book: Book): Observable { - this.books = this.books.map(b => (b.isbn === book.isbn ? { ...book } : b)); - return of({ ...book }); - } - - remove(isbn: string): Observable { - this.books = this.books.filter(b => b.isbn !== isbn); - return of(undefined); - } -} diff --git a/material/ngrx-signal-store/demo/src/app/shared/book-store.spec.ts b/material/ngrx-signal-store/demo/src/app/shared/book-store.spec.ts new file mode 100644 index 00000000..98e5a293 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/shared/book-store.spec.ts @@ -0,0 +1,78 @@ +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; + +import { BookStore } from './book-store'; +import { Book } from './book'; + +const API = 'https://api1.angular-buch.com'; +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ + isbn, + title, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); + +describe('BookStore (HTTP)', () => { + let store: BookStore; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideHttpClientTesting()] + }); + store = TestBed.inject(BookStore); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => httpMock.verify()); + + it('getAll lädt die Bücher per GET', () => { + const books = [b('1'), b('2')]; + let result: Book[] | undefined; + store.getAll().subscribe(r => (result = r)); + + const req = httpMock.expectOne(`${API}/books`); + expect(req.request.method).toBe('GET'); + req.flush(books); + + expect(result).toEqual(books); + }); + + it('create schickt einen POST und liefert das angelegte Buch', () => { + const book = b('3', 'Neu'); + let result: Book | undefined; + store.create(book).subscribe(r => (result = r)); + + const req = httpMock.expectOne(`${API}/books`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(book); + req.flush(book); + + expect(result).toEqual(book); + }); + + it('create reicht einen Fehler bei doppelter ISBN (HTTP 409) durch', () => { + let failed = false; + store.create(b('1')).subscribe({ error: () => (failed = true) }); + + httpMock + .expectOne(`${API}/books`) + .flush({ error: 'ISBN already exists' }, { status: 409, statusText: 'Conflict' }); + + expect(failed).toBe(true); + }); + + it('remove schickt einen DELETE auf die ISBN', () => { + let done = false; + store.remove('1').subscribe(() => (done = true)); + + const req = httpMock.expectOne(`${API}/books/1`); + expect(req.request.method).toBe('DELETE'); + req.flush(null); + + expect(done).toBe(true); + }); +}); diff --git a/material/ngrx-signal-store/demo/src/app/shared/book-store.ts b/material/ngrx-signal-store/demo/src/app/shared/book-store.ts new file mode 100644 index 00000000..e5f0ad01 --- /dev/null +++ b/material/ngrx-signal-store/demo/src/app/shared/book-store.ts @@ -0,0 +1,31 @@ +import { inject, Service } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { Book } from './book'; + +/** + * Datenzugriff auf die echte BookManager-API (wie im Buch). + * Eine öffentlich gehostete Instanz liegt unter `api1.angular-buch.com`, + * sodass die Demo ohne eigenes Backend läuft. + * + * Wir geben hier bewusst `Observable`s zurück (statt `httpResource`/`Promise` + * wie in späteren Buch-Iterationen), weil `rxMethod` direkt mit Observables arbeitet. + */ +@Service() +export class BookStore { + #http = inject(HttpClient); + #apiUrl = 'https://api1.angular-buch.com'; + + getAll(): Observable { + return this.#http.get(`${this.#apiUrl}/books`); + } + + create(book: Book): Observable { + return this.#http.post(`${this.#apiUrl}/books`, book); + } + + remove(isbn: string): Observable { + return this.#http.delete(`${this.#apiUrl}/books/${isbn}`); + } +} diff --git a/material/ngrx-signal-store/demo/src/app/shared/book.ts b/material/ngrx-signal-store/demo/src/app/shared/book.ts index fda322a3..0a527c1c 100644 --- a/material/ngrx-signal-store/demo/src/app/shared/book.ts +++ b/material/ngrx-signal-store/demo/src/app/shared/book.ts @@ -1,5 +1,9 @@ export interface Book { isbn: string; title: string; - rating?: number; + subtitle?: string; + authors: string[]; + description: string; + imageUrl: string; + createdAt: string; } diff --git a/material/ngrx-signal-store/demo/src/app/shared/error-message.ts b/material/ngrx-signal-store/demo/src/app/shared/error-message.ts index d43d1708..aab77faa 100644 --- a/material/ngrx-signal-store/demo/src/app/shared/error-message.ts +++ b/material/ngrx-signal-store/demo/src/app/shared/error-message.ts @@ -1,12 +1,14 @@ +import { HttpErrorResponse } from '@angular/common/http'; + /** - * Wandelt einen unbekannten Fehler (z. B. aus `tapResponse`) sicher in eine - * lesbare Meldung um. Der Fehler ist als `unknown` typisiert – wir prüfen die - * Form, statt sie blind anzunehmen. - * - * Hinweis: Eine `HttpErrorResponse` ist KEIN `instanceof Error` und liefe hier - * in den Fallback. Für ein echtes HttpClient-Backend würden wir die Prüfung - * erweitern, z. B. `if (error instanceof HttpErrorResponse) return error.message;`. + * Wandelt einen unbekannten Fehler sicher in eine lesbare Meldung um. + * Der Fehler ist als `unknown` typisiert – wir prüfen die Form, statt sie + * blind anzunehmen. Die BookManager-API liefert ihre Fehler als `{ error: string }` + * (z. B. HTTP 409 bei doppelter ISBN), verpackt in einer `HttpErrorResponse`. */ export function toMessage(error: unknown): string { + if (error instanceof HttpErrorResponse) { + return error.error?.error ?? error.message; + } return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; } diff --git a/material/ngrx-signal-store/demo/src/index.html b/material/ngrx-signal-store/demo/src/index.html index e8f9c719..f9f06e72 100644 --- a/material/ngrx-signal-store/demo/src/index.html +++ b/material/ngrx-signal-store/demo/src/index.html @@ -8,6 +8,6 @@ - + From f09b420d7fa059c7ae954c84e0c8f81fb4d1aa07 Mon Sep 17 00:00:00 2001 From: Johannes Hoppe Date: Wed, 17 Jun 2026 18:38:35 +0200 Subject: [PATCH 24/24] material(ngrx): Artikel Teil 1-3 aufs aktuelle Buch portiert (Text + Code) Alle Code-Schnipsel jetzt deckungsgleich mit den bewiesenen Demo-Apps: - Datenservice BookStore (@Service(), HttpClient gegen api1.angular-buch.com), kein In-Memory; volles Buch-Modell ohne rating; provideHttpClient. - update/PUT entfernt (hat das Buch nicht): CRUD-Abschnitt = "anlegen und loeschen". - NEU: Favoriten als reiner Client-State (likeBook/clearLikedBooks ohne Effect bzw. ohne rxMethod) - didaktischer Kontrast Server-State vs. lokaler State. - "store"-Mehrdeutigkeit aufgeloest: Datenservice = BookStore (buchidentisch), global = injizierter Store, signal = BookSignalStore. - Komponenten im Buch-Stil (book-card/books-overview), Prefix app-. - Fehler ueber HttpErrorResponse/toMessage; Tests mit HttpTestingController. - Teil 1: BookApi-Nennung -> BookStore. Verifiziert: 0 Alt-Residuen, Fences balanciert, Code = Demo (Agent-Gegenpruefung), wir-Form, keine Versions-Pins. --- material/ngrx-global-store/README.md | 671 ++++++++++++++++++--------- material/ngrx-intro/README.md | 2 +- material/ngrx-signal-store/README.md | 446 ++++++++++++------ 3 files changed, 762 insertions(+), 357 deletions(-) diff --git a/material/ngrx-global-store/README.md b/material/ngrx-global-store/README.md index d171fd55..968a1cc5 100644 --- a/material/ngrx-global-store/README.md +++ b/material/ngrx-global-store/README.md @@ -60,6 +60,7 @@ In einer Standalone-Anwendung registrieren wir den Store über die Funktion `pro ```ts // app.config.ts import { ApplicationConfig, isDevMode } from '@angular/core'; +import { provideHttpClient, withFetch } from '@angular/common/http'; import { provideStore } from '@ngrx/store'; import { provideEffects } from '@ngrx/effects'; import { provideStoreDevtools } from '@ngrx/store-devtools'; @@ -67,6 +68,7 @@ import { provideStoreDevtools } from '@ngrx/store-devtools'; export const appConfig: ApplicationConfig = { providers: [ // ... + provideHttpClient(withFetch()), provideStore(), provideEffects(), provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }) @@ -74,6 +76,8 @@ export const appConfig: ApplicationConfig = { }; ``` +Wir holen die Buchliste später per HTTP von einer echten API. Damit der `HttpClient` zur Verfügung steht, registrieren wir ihn hier gleich mit `provideHttpClient(withFetch())`. + Mit dieser Konfiguration ist der Store zwar schon aktiv, aber wir haben noch nicht festgelegt, wie das zentrale State-Objekt strukturiert sein soll. Da wir auch mit NgRx modular entwickeln, definieren wir die State-Struktur nicht zentral, sondern lagern alle neuen Bausteine in eigene Dateien aus und registrieren sie pro Feature. ### Feature anlegen @@ -268,7 +272,7 @@ export const loadBooksFailure = createAction( Um mit dem Store zu kommunizieren und Zustandsänderungen anzustoßen, müssen die Actions von den Komponenten in den Store gesendet werden. Den Store fordern wir dazu mit der Funktion `inject()` in der Komponente an. -Der Store verfügt über eine Methode `dispatch()`, mit der wir eine Action in den Store dispatchen können. Beim Aufruf der `BookList` soll das Laden der Buchliste angestoßen werden. Deshalb lösen wir dort gleich im Konstruktor die Action `loadBooks` aus. +Der Store verfügt über eine Methode `dispatch()`, mit der wir eine Action in den Store dispatchen können. Beim Aufruf der `BooksOverview` soll das Laden der Buchliste angestoßen werden. Deshalb lösen wir dort gleich im Konstruktor die Action `loadBooks` aus. Wichtig ist, dass das exportierte `loadBooks` aus der Datei `book.actions.ts` selbst noch keine Action ist, sondern ein Action Creator, der ein Action-Objekt erzeugen kann. Dazu müssen wir die Funktion aufrufen. Hat die Action einen Payload, so übergeben wir ihn als Argument an den Action Creator: @@ -280,14 +284,14 @@ const mySuccessAction = loadBooksSuccess({ data: [/* ... */] }); Wir müssen die Funktion `loadBooks` also aufrufen, um ein Action-Objekt zu erhalten, das wir dispatchen können: ```ts -// books/book-list/book-list.ts +// books/books-overview/books-overview.ts import { Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; import { loadBooks } from '../store/book.actions'; @Component({ /* ... */ }) -export class BookList { +export class BooksOverview { private store = inject(Store); constructor() { @@ -365,7 +369,7 @@ Für alle unbekannten Actions liefert der Reducer automatisch den aktuellen Stat ### Selektoren: Daten aus dem State lesen -Fassen wir kurz zusammen, wie weit wir bisher gekommen sind: Wir haben Action Creators definiert und die Action `loadBooks` von der `BookList` aus in den Store dispatcht. Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. +Fassen wir kurz zusammen, wie weit wir bisher gekommen sind: Wir haben Action Creators definiert und die Action `loadBooks` von der `BooksOverview` aus in den Store dispatcht. Dort reagiert der Reducer auf die Actions und erzeugt einen neuen State mit der passenden Änderung: Das `loading`-Flag wird auf `true` gesetzt. Um den Kreislauf des Datenflusses zu schließen, wollen wir die Daten aus dem State nun auslesen und in der Komponente darstellen. Der Store gibt dabei stets den vollständigen State aus. Um einzelne Teile daraus zu lesen, benötigen wir eine Projektion. In der Praxis werden diese Lesezugriffe schnell komplexer: Mitunter wollen wir Daten nicht nur einfach auslesen, sondern Projektionen über verschiedene Teile des States ausführen. Stellen wir uns vor, wir besitzen eine Liste von Autoren und Autorinnen und eine Liste von Büchern – wollen aber nun nur die Bücher ausgeben, die von einer bestimmten Person verfasst wurden. Dazu ist zusätzliche Logik nötig, die nicht in die Komponenten gehört. Stattdessen lagern wir diese Logik in separate Funktionen aus, die unabhängig von den Komponenten sind. Wir können eine solche Funktion mit einer Datenbankabfrage vergleichen: Die Query wird einmal definiert und kann beliebig komplex sein. Verschiedene Teile der Anwendung können diese Query nutzen und die Daten genau im benötigten Format erhalten. Die Funktionen zur Abfrage von Daten aus dem Store werden *Selektoren* genannt. @@ -416,16 +420,16 @@ export const selectAllBooks = createSelector( ); ``` -Diese Selektoren lesen wir nun in der `BookList` aus. Der Store stellt dafür die Methode `selectSignal()` bereit: Sie erwartet einen Selektor und liefert ein **Signal** des ausgewählten State-Slice zurück. Im Gegensatz zur Methode `select()`, die ein Observable liefert, erhalten wir mit `selectSignal()` direkt ein Signal – das passt nahtlos in die signal-basierte Welt von modernem Angular und wir benötigen im Template keine `AsyncPipe`. +Diese Selektoren lesen wir nun in der `BooksOverview` aus. Der Store stellt dafür die Methode `selectSignal()` bereit: Sie erwartet einen Selektor und liefert ein **Signal** des ausgewählten State-Slice zurück. Im Gegensatz zur Methode `select()`, die ein Observable liefert, erhalten wir mit `selectSignal()` direkt ein Signal – das passt nahtlos in die signal-basierte Welt von modernem Angular und wir benötigen im Template keine `AsyncPipe`. Das resultierende Signal aktualisiert sich nur dann, wenn sich der selektierte Wert tatsächlich verändert hat. Obwohl der Store bei *jeder* Änderung den gesamten State neu erzeugt, gibt unser `loading`-Signal also nur dann einen neuen Wert aus, wenn sich das `loading`-Flag wirklich geändert hat. Die folgende Abbildung veranschaulicht das: ![Diagramm: Der Store gibt bei jeder Änderung den gesamten State aus (loading wechselt false, true, true, false). Der Selektor store.selectSignal(state => state.book.loading) gibt darunter nur dann einen neuen Wert aus, wenn sich das loading-Flag tatsächlich ändert (false, true, false).](./selectors-gap.svg "Selektor mit store.selectSignal() verwenden") -Wir legen die Signale direkt als Properties in der Komponentenklasse ab. Wenn wir unsere Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. Da wir die Buchliste nun aus dem Store beziehen, benötigen wir den `BookApi` in der Komponente nicht mehr. +Wir legen die Signale direkt als Properties in der Komponentenklasse ab. Wenn wir unsere Anwendung sauber strukturieren, sollten die Komponenten immer so aussehen und keine zusätzliche Logik für die Datenaufbereitung beinhalten. Da wir die Buchliste nun aus dem Store beziehen, benötigen wir den `BookStore` in der Komponente nicht mehr – die HTTP-Kommunikation findet jetzt ausschließlich im Effect statt. ```ts -// books/book-list/book-list.ts +// books/books-overview/books-overview.ts import { Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; @@ -433,7 +437,7 @@ import { loadBooks } from '../store/book.actions'; import { selectAllBooks, selectBooksLoading } from '../store/book.selectors'; @Component({ /* ... */ }) -export class BookList { +export class BooksOverview { private store = inject(Store); books = this.store.selectSignal(selectAllBooks); @@ -448,16 +452,16 @@ export class BookList { Im Template der Komponente lesen wir die Signale aus, indem wir sie wie eine Funktion aufrufen (`books()`). Für die Buchliste nutzen wir den nativen Control Flow mit `@for`, für den Ladeindikator `@if`. Unser globales Stylesheet bietet dafür die passende CSS-Klasse `loader` an. ```html - -

                      Books

                      -
                        + +

                        Bücher ({{ books().length }})

                        +
                          @for (book of books(); track book.isbn) { -
                        • +
                        • {{ book.title }}
                        • }
                        @if (loading()) { -
                        Loading ...
                        +

                        Lädt …

                        } ``` @@ -485,7 +489,36 @@ Technisch ist ein Effect also immer ein `Observable`. Alle so erzeugten Die Klasse `BookEffects` befindet sich in der Datei `books/store/book.effects.ts`. Ein Effect wird als ein Property in dieser Klasse definiert. Der Name des Properties spielt keine Rolle, sollte aber passend zur Aufgabe benannt werden. Das Ziel ist es, hier ein Observable zu entwickeln, das Actions ausgibt. Jeden Effect kapseln wir mit der Funktion `createEffect()`, sodass er automatisch in den Lebenszyklus von NgRx integriert wird. Ein erster Effect zum Laden von Daten wurde bereits automatisch generiert, weil wir beim Anlegen die Einstellung `--api` verwendet haben. Dieses Grundgerüst wollen wir vervollständigen, um die Buchliste per HTTP zu laden. -In die Klasse holen wir mit `inject()` zwei Abhängigkeiten: den `BookApi`, der die HTTP-Kommunikation kapselt, und den Service `Actions`. Über `Actions` erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. Dieser Datenstrom ist meist die Grundlage für unsere Effects. (Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: [5 useful NgRx effects that don't rely on actions](https://angular.schule/blog/2018-06-5-useful-effects-without-actions).) +In die Klasse holen wir mit `inject()` zwei Abhängigkeiten: den `BookStore`, der die HTTP-Kommunikation kapselt, und den Service `Actions`. Über `Actions` erhalten wir ein Observable, das alle Actions liefert, die in der Anwendung auftreten. Dieser Datenstrom ist meist die Grundlage für unsere Effects. (Ein Effect muss nicht auf Actions basieren. Einige Beispiele für solche Effects haben wir in einem Blogartikel zusammengefasst: [5 useful NgRx effects that don't rely on actions](https://angular.schule/blog/2018-06-5-useful-effects-without-actions).) + +Den Datenzugriff kapseln wir wie im Buch in einem Service `BookStore`. Er nutzt den `HttpClient`, um mit der API zu kommunizieren. Damit die Demo ohne eigenes Backend läuft, sprechen wir eine öffentlich gehostete Instanz der BookManager-API unter `https://api1.angular-buch.com` an. Der Service gibt bewusst `Observable`s zurück, denn unsere Effects arbeiten direkt mit Observables: + +```ts +// shared/book-store.ts +import { inject, Service } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +import { Book } from './book'; + +@Service() +export class BookStore { + #http = inject(HttpClient); + #apiUrl = 'https://api1.angular-buch.com'; + + getAll(): Observable { + return this.#http.get(`${this.#apiUrl}/books`); + } + + create(book: Book): Observable { + return this.#http.post(`${this.#apiUrl}/books`, book); + } + + remove(isbn: string): Observable { + return this.#http.delete(`${this.#apiUrl}/books/${isbn}`); + } +} +``` Der Effect `loadBooks$` soll auf die Action `loadBooks` reagieren, den passenden HTTP-Request auslösen und Actions zurück in den Store leiten (Success und Failure). Wir zeigen zunächst den vollständigen Code und gehen die Implementierung anschließend Schritt für Schritt durch: @@ -497,26 +530,26 @@ import { catchError, map, switchMap } from 'rxjs/operators'; import { of } from 'rxjs'; import * as BookActions from './book.actions'; -import { BookApi } from '../../shared/book-api'; +import { BookStore } from '../../shared/book-store'; import { toMessage } from '../../shared/error-message'; @Injectable() export class BookEffects { private actions$ = inject(Actions); - private service = inject(BookApi); + private bookStore = inject(BookStore); - loadBooks$ = createEffect(() => { - return this.actions$.pipe( + loadBooks$ = createEffect(() => + this.actions$.pipe( ofType(BookActions.loadBooks), switchMap(() => - this.service.getAll().pipe( + this.bookStore.getAll().pipe( map(data => BookActions.loadBooksSuccess({ data })), catchError((error: unknown) => of(BookActions.loadBooksFailure({ error: toMessage(error) }))) ) ) - ); - }); + ) + ); } ``` @@ -524,13 +557,18 @@ Der Datenfluss beginnt beim Strom aller Actions aus der gesamten Anwendung: `thi Für jede dieser Actions wollen wir nun einen HTTP-Request ausführen und die Ergebnisse verarbeiten. Damit betreten wir erneut das Terrain der Higher-Order Observables. Wir müssen uns sorgfältig für einen der vier Flattening-Operatoren entscheiden. Unsere Wahl fällt hier auf `switchMap()`: Wird die Buchliste noch geladen und währenddessen erneut angefragt, soll nur die zuletzt gesendete Anfrage bearbeitet werden. Wir sollten allerdings nicht immer `switchMap()` in unseren Effects verwenden, sondern sorgfältig abwägen, welcher Flattening-Operator am besten zum jeweiligen Problem passt. -Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookApi` mit der Methode `getAll()` einsetzen, die ein Observable zurückgibt. Unser Effect ist nun ein Observable, das ein Array von Büchern liefert. Damit die Buchliste im Store verarbeitet werden kann, müssen wir sie in eine Action verpacken. Wir nutzen dazu den Operator `map()` und wandeln damit die Buchliste um in eine Action `loadBooksSuccess`, die die Liste enthält. +Mithilfe von `switchMap()` lösen wir den HTTP-Request aus, indem wir den `BookStore` mit der Methode `getAll()` einsetzen, die ein Observable zurückgibt. Unser Effect ist nun ein Observable, das ein Array von Büchern liefert. Damit die Buchliste im Store verarbeitet werden kann, müssen wir sie in eine Action verpacken. Wir nutzen dazu den Operator `map()` und wandeln damit die Buchliste um in eine Action `loadBooksSuccess`, die die Liste enthält. -Ähnlich gehen wir für den Fehlerfall vor: Hier nutzen wir den Operator `catchError()`, um einen Fehler in der Ausführung abzufangen. Wir werfen den Fehler allerdings an der Stelle nicht weiter, sondern kapseln ihn in eine Action `loadBooksFailure`, die wir als reguläres Element des Datenstroms weitergeben. Der Fehler im `catchError()` ist als `unknown` typisiert – an dieser Stelle kann schließlich alles Mögliche ankommen (ein `Error`, bei einem echten Backend eine `HttpErrorResponse`, im Extremfall ein String). Statt blind auf eine Eigenschaft wie `message` zuzugreifen, prüfen wir die Form des Fehlers in einer kleinen Hilfsfunktion `toMessage()`: +Ähnlich gehen wir für den Fehlerfall vor: Hier nutzen wir den Operator `catchError()`, um einen Fehler in der Ausführung abzufangen. Wir werfen den Fehler allerdings an der Stelle nicht weiter, sondern kapseln ihn in eine Action `loadBooksFailure`, die wir als reguläres Element des Datenstroms weitergeben. Der Fehler im `catchError()` ist als `unknown` typisiert – an dieser Stelle kann schließlich alles Mögliche ankommen (ein `Error`, bei einem HTTP-Fehler eine `HttpErrorResponse`, im Extremfall ein String). Statt blind auf eine Eigenschaft wie `message` zuzugreifen, prüfen wir die Form des Fehlers in einer kleinen Hilfsfunktion `toMessage()`. Unsere API liefert ihre Fehlermeldungen als `{ error: string }` (z. B. HTTP 409 bei einer doppelten ISBN), verpackt in einer `HttpErrorResponse` – diesen Fall behandeln wir gezielt: ```ts // shared/error-message.ts +import { HttpErrorResponse } from '@angular/common/http'; + export function toMessage(error: unknown): string { + if (error instanceof HttpErrorResponse) { + return error.error?.error ?? error.message; + } return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; } ``` @@ -570,15 +608,15 @@ import { catchError, map, switchMap } from 'rxjs/operators'; import { of } from 'rxjs'; import * as BookActions from './book.actions'; -import { BookApi } from '../../shared/book-api'; +import { BookStore } from '../../shared/book-store'; import { toMessage } from '../../shared/error-message'; export const loadBooks$ = createEffect( - (actions$ = inject(Actions), service = inject(BookApi)) => { + (actions$ = inject(Actions), bookStore = inject(BookStore)) => { return actions$.pipe( ofType(BookActions.loadBooks), switchMap(() => - service.getAll().pipe( + bookStore.getAll().pipe( map(data => BookActions.loadBooksSuccess({ data })), catchError((error: unknown) => of(BookActions.loadBooksFailure({ error: toMessage(error) }))) @@ -600,11 +638,11 @@ provideEffects(bookEffects) Beide Varianten – klassenbasiert mit `inject()` und funktional – sind gleichwertig. Welche wir wählen, ist vor allem eine Frage des Stils. -### Bücher anlegen, ändern und löschen +### Bücher anlegen und löschen Bisher haben wir die Buchliste nur geladen. Eine echte Anwendung muss Daten auch verändern. Im Global Store durchläuft jede Mutation denselben Weg wie das Laden: Eine Komponente dispatcht eine Action, ein Effect erledigt den HTTP-Request, eine Erfolgs-Action landet im Reducer, und der Reducer passt den State an. -**Actions.** Für Anlegen, Ändern und Löschen definieren wir jeweils ein Trio aus Auslöser-, Erfolgs- und Fehler-Action: +**Actions.** Für Anlegen und Löschen definieren wir jeweils ein Trio aus Auslöser-, Erfolgs- und Fehler-Action: ```ts // books/store/book.actions.ts @@ -612,64 +650,47 @@ export const createBook = createAction('[Book] Create Book', props<{ book: Book export const createBookSuccess = createAction('[Book] Create Book Success', props<{ book: Book }>()); export const createBookFailure = createAction('[Book] Create Book Failure', props<{ error: string }>()); -export const updateBook = createAction('[Book] Update Book', props<{ book: Book }>()); -export const updateBookSuccess = createAction('[Book] Update Book Success', props<{ book: Book }>()); -export const updateBookFailure = createAction('[Book] Update Book Failure', props<{ error: string }>()); - export const deleteBook = createAction('[Book] Delete Book', props<{ isbn: string }>()); export const deleteBookSuccess = createAction('[Book] Delete Book Success', props<{ isbn: string }>()); export const deleteBookFailure = createAction('[Book] Delete Book Failure', props<{ error: string }>()); ``` -Schon an dieser Liste zeigt sich der Preis des Patterns: Drei CRUD-Operationen ergeben neun zusätzliche Actions. (Kompakter notieren lässt sich das mit `createActionGroup()`, siehe Abschnitt "Wie geht's weiter?".) +Schon an dieser Liste zeigt sich der Preis des Patterns: Zwei Operationen ergeben sechs zusätzliche Actions. (Kompakter notieren lässt sich das mit `createActionGroup()`, siehe Abschnitt "Wie geht's weiter?".) -**Effects.** Jede Mutation löst einen HTTP-Request aus. Ein wichtiger Unterschied zum Laden betrifft den Flattening-Operator: Beim Laden ist `switchMap()` richtig (eine neue Anfrage macht die alte überflüssig). Bei **schreibenden** Operationen wollen wir laufende Requests aber *nicht* abbrechen – sonst ginge womöglich ein Speichervorgang verloren. Hier ist `concatMap()` die sichere Wahl. Der `BookApi` bietet dazu die Methoden `create()`, `update()` und `remove()` an: +**Effects.** Jede Mutation löst einen HTTP-Request aus. Ein wichtiger Unterschied zum Laden betrifft den Flattening-Operator: Beim Laden ist `switchMap()` richtig (eine neue Anfrage macht die alte überflüssig). Bei **schreibenden** Operationen wollen wir laufende Requests aber *nicht* abbrechen – sonst ginge womöglich ein Speichervorgang verloren. Hier ist `concatMap()` die sichere Wahl. Der `BookStore` bietet dazu die Methoden `create()` und `remove()` an: ```ts // books/store/book.effects.ts (Ergänzung in der Klasse BookEffects) import { concatMap } from 'rxjs/operators'; -createBook$ = createEffect(() => { - return this.actions$.pipe( +createBook$ = createEffect(() => + this.actions$.pipe( ofType(BookActions.createBook), concatMap(({ book }) => - this.service.create(book).pipe( + this.bookStore.create(book).pipe( map(created => BookActions.createBookSuccess({ book: created })), catchError((error: unknown) => of(BookActions.createBookFailure({ error: toMessage(error) }))) ) ) - ); -}); - -updateBook$ = createEffect(() => { - return this.actions$.pipe( - ofType(BookActions.updateBook), - concatMap(({ book }) => - this.service.update(book).pipe( - map(updated => BookActions.updateBookSuccess({ book: updated })), - catchError((error: unknown) => - of(BookActions.updateBookFailure({ error: toMessage(error) }))) - ) - ) - ); -}); + ) +); -deleteBook$ = createEffect(() => { - return this.actions$.pipe( +deleteBook$ = createEffect(() => + this.actions$.pipe( ofType(BookActions.deleteBook), concatMap(({ isbn }) => - this.service.remove(isbn).pipe( + this.bookStore.remove(isbn).pipe( map(() => BookActions.deleteBookSuccess({ isbn })), catchError((error: unknown) => of(BookActions.deleteBookFailure({ error: toMessage(error) }))) ) ) - ); -}); + ) +); ``` -**Reducer.** Im Reducer passen wir den State immutabel an: anhängen mit dem Spread-Operator, ersetzen mit `map()`, entfernen mit `filter()`. Beim Auslösen eines Schreibvorgangs setzen wir – wie schon beim Laden – eine alte Fehlermeldung zurück. Auf die drei Fehlerfälle reagieren wir mit *einem* `on()` gleichzeitig (das ist möglich, weil ein Reducer für jede Action durchlaufen wird): +**Reducer.** Im Reducer passen wir den State immutabel an: anhängen mit dem Spread-Operator, entfernen mit `filter()`. Beim Auslösen eines Schreibvorgangs setzen wir – wie schon beim Laden – eine alte Fehlermeldung zurück. Auf die beiden Fehlerfälle reagieren wir mit *einem* `on()` gleichzeitig (das ist möglich, weil ein Reducer für jede Action durchlaufen wird): ```ts // books/store/book.reducer.ts (Ergänzung in createReducer) @@ -677,7 +698,6 @@ deleteBook$ = createEffect(() => { // Beim Auslösen eines Schreibvorgangs eine alte Fehlermeldung zurücksetzen: on( BookActions.createBook, - BookActions.updateBook, BookActions.deleteBook, (state): State => ({ ...state, error: null }) ), @@ -687,11 +707,6 @@ on(BookActions.createBookSuccess, (state, action): State => ({ books: [...state.books, action.book] })), -on(BookActions.updateBookSuccess, (state, action): State => ({ - ...state, - books: state.books.map(b => b.isbn === action.book.isbn ? action.book : b) -})), - on(BookActions.deleteBookSuccess, (state, action): State => ({ ...state, books: state.books.filter(b => b.isbn !== action.isbn) @@ -699,13 +714,12 @@ on(BookActions.deleteBookSuccess, (state, action): State => ({ on( BookActions.createBookFailure, - BookActions.updateBookFailure, BookActions.deleteBookFailure, (state, action): State => ({ ...state, error: action.error }) ) ``` -**Selektor und Komponente.** Für die Fehleranzeige ergänzen wir einen Selektor `selectBooksError` und lesen ihn – wie schon `books` und `loading` – per `selectSignal()`. Aktionen lösen wir aus, indem wir die Action Creators dispatchen: +**Selektor.** Für die Fehleranzeige ergänzen wir einen Selektor `selectBooksError` und lesen ihn – wie schon `books` und `loading` – per `selectSignal()`. Aktionen lösen wir aus, indem wir die Action Creators dispatchen: ```ts // books/store/book.selectors.ts @@ -725,17 +739,28 @@ export const clearError = createAction('[Book] Clear Error'); on(BookActions.clearError, (state): State => ({ ...state, error: null })) ``` +**Smarte und präsentationale Komponente.** Wir teilen die Oberfläche in zwei Komponenten auf: eine smarte Komponente `BooksOverview`, die den Store kennt und Actions dispatcht, und eine präsentationale Komponente `BookCard`, die nur ein einzelnes Buch darstellt und über Outputs meldet, was Nutzerinnen und Nutzer tun möchten. Diese Trennung hält die Darstellung frei von NgRx und macht die Karte beliebig wiederverwendbar. + +Die smarte Komponente `BooksOverview` bezieht alle benötigten Daten als Signale aus dem Store und stößt im Konstruktor das Laden an. Die Eingabefelder des Anlege-Formulars reichen wir bewusst ohne `FormsModule` als lokale Template-Referenzen (`#isbnEl`) direkt an die Methode weiter, die nach dem Dispatch die Felder leert – so bleibt der Fokus auf NgRx. Da die API ein vollständiges Buch erwartet, füllen wir die übrigen Felder beim Anlegen mit sinnvollen Defaults: + ```ts -// books/book-list/book-list.ts -import { Component, inject } from '@angular/core'; +// books/books-overview/books-overview.ts +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; import * as BookActions from '../store/book.actions'; import { selectAllBooks, selectBooksError, selectBooksLoading } from '../store/book.selectors'; import { Book } from '../../shared/book'; - -@Component({ /* ... */ }) -export class BookList { +import { BookCard } from '../book-card/book-card'; + +@Component({ + selector: 'app-books-overview', + imports: [BookCard], + templateUrl: './books-overview.html', + styleUrl: './books-overview.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BooksOverview { private store = inject(Store); books = this.store.selectSignal(selectAllBooks); @@ -750,16 +775,11 @@ export class BookList { if (!isbn.value || !title.value) { return; } - this.store.dispatch(BookActions.createBook({ book: { isbn: isbn.value, title: title.value, rating: 0 } })); + this.store.dispatch(BookActions.createBook({ book: this.#newBook(isbn.value, title.value) })); isbn.value = ''; title.value = ''; } - rateUp(book: Book): void { - const rating = Math.min((book.rating ?? 0) + 1, 5); - this.store.dispatch(BookActions.updateBook({ book: { ...book, rating } })); - } - deleteBook(isbn: string): void { this.store.dispatch(BookActions.deleteBook({ isbn })); } @@ -767,18 +787,85 @@ export class BookList { clearError(): void { this.store.dispatch(BookActions.clearError()); } + + // Ein vollständiges Buch mit sinnvollen Defaults, damit die echte API den POST annimmt. + #newBook(isbn: string, title: string): Book { + return { + isbn, + title, + authors: ['Unbekannt'], + description: 'Über die Demo angelegt.', + imageUrl: 'https://cdn.ng-buch.de/cover-placeholder.png', + createdAt: new Date().toISOString() + }; + } } ``` -Im Template zeigen wir die Fehlermeldung (mit einem Button zum Wegklicken) an, bieten ein kleines Formular zum Anlegen und lösen Ändern (Bewertung erhöhen) sowie Löschen über Buttons aus. Die Eingabefelder reichen wir bewusst ohne `FormsModule` als lokale Template-Referenzen (`#isbnEl`) direkt an die Methode weiter, die nach dem Dispatch die Felder leert – so bleibt der Fokus auf NgRx: +Die präsentationale Komponente `BookCard` kennt den Store nicht. Sie erhält ihr Buch über ein erforderliches Input-Signal `input.required()` und meldet Ereignisse über die Outputs `like` und `remove` nach außen: + +```ts +// books/book-card/book-card.ts +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { Book } from '../../shared/book'; + +@Component({ + selector: 'app-book-card', + templateUrl: './book-card.html', + styleUrl: './book-card.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BookCard { + readonly book = input.required(); + readonly like = output(); + readonly remove = output(); + + likeBook(): void { + this.like.emit(this.book()); + } + + removeBook(): void { + this.remove.emit(this.book().isbn); + } +} +``` + +Im Template der Karte stellen wir die Daten des Buchs dar und bieten zwei Buttons an, die die Outputs auslösen: + +```html + +@let b = book(); + +
                        + +
                        +

                        {{ b.title }}

                        + @if (b.subtitle) { +

                        {{ b.subtitle }}

                        + } +

                        {{ b.authors.join(', ') }}

                        +

                        ISBN {{ b.isbn }}

                        +
                        +
                        + + +
                        +
                        +``` + +Im Template der `BooksOverview` zeigen wir die Fehlermeldung (mit einem Button zum Wegklicken), bieten ein kleines Formular zum Anlegen und rendern für jedes Buch eine `BookCard`. Die Outputs der Karte verbinden wir mit den Methoden der smarten Komponente: ```html - + +@if (loading()) { +

                        Lädt …

                        +} + @if (error(); as error) { -
                        +

                        {{ error }} -

                        +

                        }
                        @@ -787,20 +874,107 @@ Im Template zeigen wir die Fehlermeldung (mit einem Button zum Wegklicken) an, b
                        -
                          +
                          @for (book of books(); track book.isbn) { -
                        • - {{ book.title }} – ★ {{ book.rating ?? 0 }} - - -
                        • + } -
                        +
                      +``` + +Den `like`-Output verdrahten wir hier mit einer Methode `likeBook()` der smarten Komponente. Was dahinter passiert, sehen wir gleich im Abschnitt zu den Favoriten – denn dieser Zustand kommt ganz ohne Effect aus. + +Damit haben wir ein vollständiges CRUD-Feature für Anlegen und Löschen. Für zwei Operationen waren sechs Actions, zwei Effects und zwei Erfolgs-Reducer nötig – genau diese Menge an Bausteinen zieht der SignalStore in [Teil 3](/material/ngrx-signal-store) auf wenige Methoden in einer einzigen Datei zusammen. + +> **Tipp:** Die wiederkehrenden Array-Operationen im Reducer (`[...state.books, …]`, `filter()`) übernimmt das Paket `@ngrx/entity` mit fertigen Adaptern (`addOne`, `removeOne`) – siehe den Abschnitt "Entity Management" weiter unten. + +### Favoriten: Client-State ohne Seiteneffekt + +Nicht jeder Zustand muss über das Netzwerk wandern. Im BookManager können Nutzerinnen und Nutzer Bücher als Favoriten markieren. Diese Markierung lebt nur im Browser – wir schicken sie nicht an den Server. In der Welt von NgRx ist das ein lehrreicher Kontrast: Das Laden, Anlegen und Löschen sind **Server-State** und laufen über Effects. Die Favoriten sind dagegen reiner **Client-State** und kommen ganz ohne Effect aus. Wir dispatchen eine Action, der Reducer passt den State an – fertig. + +**Actions.** Wir brauchen kein Trio aus Auslöser, Erfolg und Fehler, denn es gibt keinen asynchronen Vorgang, der fehlschlagen könnte. Eine Action zum Hinzufügen und eine zum Leeren genügen: + +```ts +// books/store/book.actions.ts +export const likeBook = createAction('[Book] Like Book', props<{ book: Book }>()); +export const clearLikedBooks = createAction('[Book] Clear Liked Books'); +``` + +**State und Reducer.** Wir erweitern den Feature-State um einen Slice `likedBooks: Book[]`, der initial leer ist: + +```ts +// books/store/book.reducer.ts +export interface State { + books: Book[]; + loading: boolean; + error: string | null; + likedBooks: Book[]; +} + +export const initialState: State = { + books: [], + loading: false, + error: null, + likedBooks: [] +}; +``` + +Im Reducer fügen wir ein Buch hinzu, sofern es nicht schon in der Liste steht. Die Deduplizierung anhand der ISBN ist hier nicht nur fachlich sinnvoll, sondern auch ein schönes Beispiel für eine Pure Function: Ist das Buch bereits ein Favorit, geben wir den **unveränderten** State zurück (dieselbe Referenz). So entsteht kein neuer State und nachgelagerte Selektoren bzw. die Oberfläche aktualisieren sich nicht unnötig. Das Leeren setzt die Liste auf ein leeres Array: + +```ts +// books/store/book.reducer.ts (Ergänzung in createReducer) +on(BookActions.likeBook, (state, action): State => + state.likedBooks.some(b => b.isbn === action.book.isbn) + ? state + : { ...state, likedBooks: [...state.likedBooks, action.book] } +), +on(BookActions.clearLikedBooks, (state): State => ({ ...state, likedBooks: [] })) ``` -Damit haben wir ein vollständiges CRUD-Feature. Für drei Operationen waren neun Actions, drei Effects und drei Reducer-Fälle nötig – genau diese Menge an Bausteinen zieht der SignalStore in [Teil 3](/material/ngrx-signal-store) auf wenige Methoden in einer einzigen Datei zusammen. +Es ist bemerkenswert, was hier **nicht** steht: Es gibt keinen Effect. Während `loadBooks`, `createBook` und `deleteBook` jeweils einen Effect benötigen, um mit dem `BookStore` zu sprechen, wandert die Favoriten-Action direkt vom Dispatch in den Reducer. Genau hier liegt die Grenze zwischen Server-State und Client-State: Ein Effect ist nur dann nötig, wenn ein Seiteneffekt auszuführen ist. -> **Tipp:** Die wiederkehrenden Array-Operationen im Reducer (`[...state.books, …]`, `map()`, `filter()`) übernimmt das Paket `@ngrx/entity` mit fertigen Adaptern (`addOne`, `updateOne`, `removeOne`) – siehe den Abschnitt "Entity Management" weiter unten. +**Selektor.** Zum Auslesen ergänzen wir einen Selektor `selectLikedBooks`: + +```ts +// books/store/book.selectors.ts +export const selectLikedBooks = createSelector( + selectBookState, + state => state.likedBooks +); +``` + +**Komponente.** In der `BooksOverview` lesen wir die Favoriten als Signal und dispatchen die beiden neuen Actions. Die zugehörigen Methoden ergänzen wir in der Klasse: + +```ts +// books/books-overview/books-overview.ts (Ergänzung) +likedBooks = this.store.selectSignal(selectLikedBooks); + +likeBook(book: Book): void { + this.store.dispatch(BookActions.likeBook({ book })); +} + +clearLikedBooks(): void { + this.store.dispatch(BookActions.clearLikedBooks()); +} +``` + +Im Template ergänzen wir eine Favoriten-Sektion oberhalb der Buchliste: + +```html + +
                      +

                      Favoriten ({{ likedBooks().length }})

                      + +
                        + @for (book of likedBooks(); track book.isbn) { +
                      • {{ book.title }}
                      • + } @empty { +
                      • Noch keine Favoriten.
                      • + } +
                      +
                      +``` + +Markiert nun jemand über die `BookCard` ein Buch als Favorit, so feuert deren `like`-Output, die `BooksOverview` dispatcht `likeBook`, und der Reducer pflegt die Liste – ohne dass ein einziger HTTP-Request entsteht. ### Geschafft! @@ -1033,74 +1207,162 @@ Auf diese Weise können wir die Verwaltung von Entitäten im State sehr effizien Alle auf Grundlage von NgRx entwickelten Bausteine sollten auch getestet werden. Dafür möchten wir in diesem Abschnitt einige Hinweise geben. Grundsätzlich werden bei der Initialisierung mit den Schematics von NgRx bereits Grundgerüste für die Unit-Tests angelegt – wir können also direkt loslegen. -> **Hinweis:** Die folgenden Listings zeigen die grundsätzlichen Testtechniken. Die lauffähige Beispiel-App im Ordner `demo/` enthält vollständige, mit **Vitest** umgesetzte Tests für Reducer, Selektoren, Effects und Komponente. Die `hot`/`cold`-Marbles und `spyOn()` aus den folgenden Beispielen setzen ein Jasmine-Setup voraus; in einem Vitest-Projekt würde man stattdessen `vi.spyOn()` und z. B. das Paket `vitest-marbles` verwenden. Die Beispiel-App selbst vergleicht die Datenströme bewusst direkt (ohne Marbles). +> **Hinweis:** Die folgenden Listings zeigen die grundsätzlichen Testtechniken. Die lauffähige Beispiel-App im Ordner `demo/` enthält vollständige, mit **Vitest** umgesetzte Tests für Service, Reducer, Selektoren, Effects und Komponente. Die `hot`/`cold`-Marbles und `spyOn()` aus den folgenden Beispielen setzen ein Jasmine-Setup voraus; in einem Vitest-Projekt würde man stattdessen `vi.spyOn()` und z. B. das Paket `vitest-marbles` verwenden. Die Beispiel-App selbst vergleicht die Datenströme bewusst direkt (ohne Marbles). + +In mehreren Tests benötigen wir Beispielbücher. Dafür definieren wir eine kleine Hilfsfunktion `b()`, die ein vollständiges `Book`-Objekt mit Beispieldaten erzeugt: + +```ts +import { Book } from '../shared/book'; + +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ + isbn, + title, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); +``` #### Actions Die Action Creators beinhalten keine spezifische Logik, die getestet werden muss. In manchen Blogartikeln wird vorgeschlagen, die korrekte Zusammensetzung der Objekte zu prüfen, damit eine Action stets den richtigen `type` besitzt. Wir sind der Meinung, dass solche trivialen Tests keinen Mehrwert liefern und deshalb nicht notwendig sind. +#### Service + +Unser Datenservice `BookStore` kapselt die HTTP-Kommunikation. Damit im Test keine echten Requests an die API gehen, ersetzen wir den `HttpClient` durch das Test-Backend von Angular. Wir registrieren dazu `provideHttpClient()` zusammen mit `provideHttpClientTesting()` und steuern die Requests über den `HttpTestingController`. So prüfen wir, dass die richtige URL mit der richtigen HTTP-Methode aufgerufen wird, und liefern die Antwort mit `flush()` selbst aus: + +```ts +// shared/book-store.spec.ts +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; + +import { BookStore } from './book-store'; +import { Book } from './book'; + +const API = 'https://api1.angular-buch.com'; + +describe('BookStore (HTTP)', () => { + let store: BookStore; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideHttpClientTesting()] + }); + store = TestBed.inject(BookStore); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => httpMock.verify()); + + it('getAll lädt die Bücher per GET', () => { + const books = [b('1'), b('2')]; + let result: Book[] | undefined; + store.getAll().subscribe(r => (result = r)); + + const req = httpMock.expectOne(`${API}/books`); + expect(req.request.method).toBe('GET'); + req.flush(books); + + expect(result).toEqual(books); + }); + + it('create reicht einen Fehler bei doppelter ISBN (HTTP 409) durch', () => { + let failed = false; + store.create(b('1')).subscribe({ error: () => (failed = true) }); + + httpMock + .expectOne(`${API}/books`) + .flush({ error: 'ISBN already exists' }, { status: 409, statusText: 'Conflict' }); + + expect(failed).toBe(true); + }); +}); +``` + +Der Aufruf `httpMock.verify()` im `afterEach()` stellt sicher, dass keine unerwarteten Requests offengeblieben sind. + #### Reducers -Ein Reducer ist eine Pure Function, liefert also für die gleiche Eingabe immer die gleiche klar vorhersehbare Ausgabe. Diese Eigenschaft kommt uns beim Testing zugute, denn wir können Reducers isoliert testen, ohne dass Angular dafür benötigt wird. Dazu rufen wir die Reducer-Funktion mit einem Startzustand und einer Action auf und prüfen den erzeugten neuen Zustand. +Ein Reducer ist eine Pure Function, liefert also für die gleiche Eingabe immer die gleiche klar vorhersehbare Ausgabe. Diese Eigenschaft kommt uns beim Testing zugute, denn wir können Reducers isoliert testen, ohne dass Angular dafür benötigt wird. Dazu rufen wir die Reducer-Funktion mit einem Startzustand und einer Action auf und prüfen den erzeugten neuen Zustand. Als Startzustand bietet sich der exportierte `initialState` an, den wir bei Bedarf mit dem Spread-Operator anpassen: ```ts // books/store/book.reducer.spec.ts -import { loadBooks } from './book.actions'; -import { reducer } from './book.reducer'; +import { initialState, reducer, State } from './book.reducer'; +import * as BookActions from './book.actions'; describe('Book Reducer', () => { - it('should enable the loading flag for loadBooks', () => { - const state = { - books: [], - loading: false, - error: null - }; - const action = loadBooks(); + it('loadBooks setzt loading=true und error=null', () => { + const state = reducer({ ...initialState, error: 'alt' }, BookActions.loadBooks()); + expect(state.loading).toBe(true); + expect(state.error).toBeNull(); + }); + + it('createBookSuccess hängt ein Buch immutabel an', () => { + const start: State = { ...initialState, books: [b('1')] }; + const state = reducer(start, BookActions.createBookSuccess({ book: b('2') })); + expect(state.books.map(x => x.isbn)).toEqual(['1', '2']); + expect(start.books.length).toBe(1); // Original unverändert + }); - const newState = reducer(state, action); - expect(newState.loading).toBe(true); + it('likeBook dedupliziert anhand der ISBN', () => { + const start: State = { ...initialState, likedBooks: [b('1')] }; + const state = reducer(start, BookActions.likeBook({ book: b('1') })); + expect(state.likedBooks.length).toBe(1); + expect(state).toBe(start); // unverändert, keine neue Referenz }); }); ``` +Besonders schön zeigt sich hier die Reinheit der Funktion: Der Test zu `createBookSuccess` prüft, dass das ursprüngliche State-Objekt unverändert bleibt, und der Test zu `likeBook` stellt sicher, dass der Reducer bei einem bereits vorhandenen Favoriten *dieselbe* Referenz zurückgibt. + #### Selektoren Zum Testen von Selektoren gibt es verschiedene Herangehensweisen. Auch wenn ein Selektor mithilfe von `createSelector()` erstellt wurde, so ist er weiterhin nur eine Funktion, die den State als Argument erhält und Berechnungen über die Daten ausführt. Wir können also auch Selektoren isoliert testen, ohne dass wir einen Store oder das Angular-Framework benötigen. Dazu erstellen wir im Test ein State-Objekt, das alle benötigten Daten enthält. Wir wenden den Selektor darauf an und prüfen das Ergebnis. Wichtig ist, dass wir in diesem Stub stets die Struktur des Root-States abbilden, denn der ist ja auch der Ausgangspunkt eines jeden Selektors. -Die Hilfsfunktion `book()` haben wir selbst definiert. Sie generiert einfache Buch-Objekte mit Beispieldaten, sodass wir schnell Testdaten erzeugen können. +Die Hilfsfunktion `b()` haben wir uns weiter oben definiert. Wichtig ist, dass wir im Stub stets die Struktur des Root-States abbilden – der Feature-State liegt unter dem Feature-Key `book`: ```ts // books/store/book.selectors.spec.ts -import { selectAllBooks } from './book.selectors'; -import { Book } from '../../shared/book'; - -// kleine Hilfsfunktion, um schnell Testdaten zu erzeugen -const book = (isbn: string): Book => ({ isbn, title: `Title ${isbn}`, rating: 0 }); +import { selectAllBooks, selectLikedBooks } from './book.selectors'; +import { bookFeatureKey, State } from './book.reducer'; describe('Book Selectors', () => { - it('should select all books', () => { - const books = [book('1'), book('2'), book('3')]; - const state = { - book: { books, loading: false, error: null } - }; + const bookState: State = { + books: [b('1'), b('2')], + loading: true, + error: 'x', + likedBooks: [b('1')] + }; + const rootState: Record = { [bookFeatureKey]: bookState }; + + it('selectAllBooks liefert die Buchliste', () => { + expect(selectAllBooks(rootState).map(x => x.isbn)).toEqual(['1', '2']); + }); - const result = selectAllBooks(state); - expect(result).toEqual(books); + it('selectLikedBooks liefert die Favoriten', () => { + expect(selectLikedBooks(rootState).map(x => x.isbn)).toEqual(['1']); }); }); ``` -Bei komplexeren Selektoren, die mehrere andere Selektoren als Grundlage verwenden, liegt die kritische Logik in der Projektionsfunktion, die im letzten Argument von `createSelector()` angegeben wird. Es ist deshalb in den meisten Fällen ausreichend, nur diese Projektion zu testen. Zugriff auf die Funktion erhalten wir mit `selector.projector`: +Bei komplexeren Selektoren, die mehrere andere Selektoren als Grundlage verwenden, liegt die kritische Logik in der Projektionsfunktion, die im letzten Argument von `createSelector()` angegeben wird. Es ist deshalb in den meisten Fällen ausreichend, nur diese Projektion zu testen. Zugriff auf die Funktion erhalten wir mit `selector.projector`. Dann übergeben wir nur den Feature-State (nicht den Root-State): ```ts // books/store/book.selectors.spec.ts describe('Book Selectors', () => { - it('should select all books', () => { - const books = [book('1'), book('2'), book('3')]; - const bookState = { books, loading: false, error: null }; + it('selectAllBooks projiziert den Feature-State', () => { + const bookState: State = { + books: [b('1'), b('2')], + loading: false, + error: null, + likedBooks: [] + }; const result = selectAllBooks.projector(bookState); - expect(result).toEqual(books); + expect(result.map(x => x.isbn)).toEqual(['1', '2']); }); }); ``` @@ -1109,99 +1371,80 @@ describe('Book Selectors', () => { Effects sind nur schwierig isoliert zu testen, denn sie greifen auf verschiedene Abhängigkeiten aus der Anwendung zu. Dazu ist nicht nur das Paket `@ngrx/effects` nötig, sondern auch alle verwendeten HTTP-Services. Außerdem muss das Observable `actions$: Actions` mit einem Strom von Actions versorgt werden, und wir wollen keinen vollständigen Store aufsetzen. -NgRx bietet dafür die Funktion `provideMockActions()`, die einen gemockten Strom von Actions bereitstellt. Tests für klassenbasierte Effects definieren wir mithilfe von `TestBed`, sodass wir die Dependency Injection von Angular nutzen können. Alle verwendeten Services müssen selbstverständlich auch durch Mocks oder Stubs ersetzt werden, damit keine echten HTTP-Requests ausgeführt werden. +NgRx bietet dafür die Funktion `provideMockActions()`, die einen gemockten Strom von Actions bereitstellt. Tests für klassenbasierte Effects definieren wir mithilfe von `TestBed`, sodass wir die Dependency Injection von Angular nutzen können. Den Datenservice `BookStore` ersetzen wir durch einen Stub, damit keine echten HTTP-Requests ausgeführt werden. Eine kleine `setup()`-Funktion kapselt das wiederkehrende Konfigurieren des `TestBed`: ```ts // books/store/book.effects.spec.ts +import { TestBed } from '@angular/core/testing'; +import { Observable, of, throwError } from 'rxjs'; +import { Action } from '@ngrx/store'; import { provideMockActions } from '@ngrx/effects/testing'; +import { BookEffects } from './book.effects'; +import { BookStore } from '../../shared/book-store'; +import * as BookActions from './book.actions'; + describe('BookEffects', () => { - let actions$: Observable; - let effects: BookEffects; + let actions$: Observable; - beforeEach(() => { + function setup(mock: Partial): BookEffects { TestBed.configureTestingModule({ providers: [ BookEffects, provideMockActions(() => actions$), + { provide: BookStore, useValue: mock } ] }); - - effects = TestBed.inject(BookEffects); - }); + return TestBed.inject(BookEffects); + } }); ``` -Für die konkreten Tests müssen wir ein Observable von Actions bereitstellen und das ausgegebene Observable sowie ggf. die ausgeführten Seiteneffekte prüfen. Das folgende Listing zeigt einen vollständigen Testaufbau. Hier stellen wir sicher, dass der Effect die gewünschte Action ausgibt und die Daten wie gewünscht über den `BookApi` bezieht. +Für die konkreten Tests stellen wir ein Observable von Actions bereit, abonnieren das vom Effect ausgegebene Observable und vergleichen die erzeugte Action. Den Stub für den `BookStore` reichen wir gezielt pro Test in `setup()` hinein – einmal mit einer erfolgreichen Antwort, einmal mit einem Fehler: ```ts // books/store/book.effects.spec.ts -describe('BookEffects', () => { - let actions$: Observable; - let effects: BookEffects; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - BookEffects, - provideMockActions(() => actions$), - { - provide: BookApi, - useValue: { getAll: () => of([]) } - } - ] - }); - - effects = TestBed.inject(BookEffects); - }); - - it('should fire loadBooksSuccess for loadBooks', () => { - const books = [book('1'), book('2'), book('3')]; - - // Implementierung von getAll() ersetzen - const bs = TestBed.inject(BookApi); - spyOn(bs, 'getAll').and.callFake(() => of(books)); - - // Action auslösen - actions$ = of(loadBooks()); - - // Actions aus Effect empfangen - let dispatchedAction: Action | undefined; - effects.loadBooks$.subscribe(action => { - dispatchedAction = action; - }); +it('loadBooks$ feuert loadBooksSuccess mit den geladenen Büchern', () => { + const books = [b('1'), b('2')]; + const effects = setup({ getAll: () => of(books) }); + actions$ = of(BookActions.loadBooks()); + + let result: Action | undefined; + effects.loadBooks$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.loadBooksSuccess({ data: books })); +}); - // Actions vergleichen - const expectedAction = loadBooksSuccess({ data: books }); - expect(dispatchedAction).toEqual(expectedAction); +it('createBook$ feuert createBookFailure bei einem Fehler', () => { + const effects = setup({ create: () => throwError(() => new Error('Doppelte ISBN')) }); + actions$ = of(BookActions.createBook({ book: b('3') })); - // Serviceaufruf prüfen - expect(bs.getAll).toHaveBeenCalled(); - }); + let result: Action | undefined; + effects.createBook$.subscribe(action => (result = action)); + expect(result).toEqual(BookActions.createBookFailure({ error: 'Doppelte ISBN' })); }); ``` > **Funktionale Effects testen:** Funktionale Effects lassen sich noch einfacher prüfen, denn wir können sie direkt als Funktion aufrufen und die benötigten Abhängigkeiten als Argumente übergeben – ganz ohne `TestBed` und `provideMockActions()`. Wir übergeben einfach ein Observable mit den Eingangs-Actions und einen Mock-Service. -Der Aufbau lässt sich auch vereinfachen, wenn wir nur die Datenströme miteinander vergleichen. Dazu eignet sich das Konzept des *Marble Testing*: Anstatt ein Observable wie üblich zu erzeugen und dann darauf zu subscriben, notieren wir den geplanten Datenstrom als Marble-Diagramm direkt im Test und definieren damit die Eingabe und die erwartete Ausgabe. Die technische Grundlage dafür bietet das Paket [jasmine-marbles](https://www.npmjs.com/package/jasmine-marbles). Das Projekt stellt auch den Matcher `toBeObservable()` zur Verfügung, mit dem wir in der Expectation direkt gegen das erzeugte Observable prüfen können: +Der Aufbau lässt sich auch vereinfachen, wenn wir nur die Datenströme miteinander vergleichen. Dazu eignet sich das Konzept des *Marble Testing*: Anstatt ein Observable wie üblich zu erzeugen und dann darauf zu subscriben, notieren wir den geplanten Datenstrom als Marble-Diagramm direkt im Test und definieren damit die Eingabe und die erwartete Ausgabe. Die technische Grundlage bietet im Jasmine-Umfeld das Paket [jasmine-marbles](https://www.npmjs.com/package/jasmine-marbles); in einem Vitest-Projekt gibt es mit `vitest-marbles` ein passendes Gegenstück. Das folgende Listing zeigt die Technik exemplarisch mit `jasmine-marbles`. Das Projekt stellt auch den Matcher `toBeObservable()` zur Verfügung, mit dem wir in der Expectation direkt gegen das erzeugte Observable prüfen können: ```ts -// books/store/book.effects.spec.ts +// books/store/book.effects.spec.ts (Marble-Variante, hier mit jasmine-marbles) import { hot, cold } from 'jasmine-marbles'; describe('BookEffects', () => { - let actions$: Observable; + let actions$: Observable; let effects: BookEffects; - beforeEach(() => { /* ... */ }); + beforeEach(() => { /* TestBed mit BookStore-Stub einrichten */ }); - it('should fire loadBooksSuccess for loadBooks', () => { - const books = [book('1'), book('2'), book('3')]; - const bs = TestBed.inject(BookApi); + it('feuert loadBooksSuccess für loadBooks', () => { + const books = [b('1'), b('2'), b('3')]; + const bs = TestBed.inject(BookStore); spyOn(bs, 'getAll').and.callFake(() => of(books)); - actions$ = hot('--a', { a: loadBooks() }); - const expected = cold('--b', { b: loadBooksSuccess({ data: books }) }); + actions$ = hot('--a', { a: BookActions.loadBooks() }); + const expected = cold('--b', { b: BookActions.loadBooksSuccess({ data: books }) }); expect(effects.loadBooks$).toBeObservable(expected); expect(bs.getAll).toHaveBeenCalled(); @@ -1231,7 +1474,7 @@ import { of } from 'rxjs'; @Injectable() export class BookEffects { private actions$ = inject(Actions); - private service = inject(BookApi); + private bookStore = inject(BookStore); private store = inject(Store); loadBooks$ = createEffect(() => { @@ -1240,7 +1483,7 @@ export class BookEffects { concatLatestFrom(() => this.store.select(selectAllBooks)), filter(([action, books]) => !books.length), switchMap(() => - this.service.getAll().pipe( + this.bookStore.getAll().pipe( map(data => BookActions.loadBooksSuccess({ data })), catchError((error: unknown) => of(BookActions.loadBooksFailure({ error: toMessage(error) }))) @@ -1256,10 +1499,10 @@ Für diesen Effect sollten wir sicherstellen, dass bei einem leeren Array auch t ```ts // books/store/book.effects.spec.ts import { provideMockStore, MockStore } from '@ngrx/store/testing'; -import { initialState } from './book.reducer'; +import { bookFeatureKey, initialState } from './book.reducer'; describe('BookEffects', () => { - let actions$: Actions; + let actions$: Observable; let effects: BookEffects; beforeEach(() => { @@ -1268,13 +1511,11 @@ describe('BookEffects', () => { BookEffects, provideMockActions(() => actions$), provideMockStore({ - initialState: { book: initialState } + initialState: { [bookFeatureKey]: initialState } }), { - provide: BookApi, - useValue: { - getAll: () => of([]) - } + provide: BookStore, + useValue: { getAll: () => of([]) } } ] }); @@ -1282,24 +1523,20 @@ describe('BookEffects', () => { effects = TestBed.inject(BookEffects); }); - it('should fire loadBooksSuccess for loadBooks if store is empty', () => { - - actions$ = hot('--a', { a: loadBooks() }); - const expected = cold('--b', { b: loadBooksSuccess({ data: [] }) }); + it('feuert loadBooksSuccess für loadBooks, wenn der Store leer ist', () => { + actions$ = hot('--a', { a: BookActions.loadBooks() }); + const expected = cold('--b', { b: BookActions.loadBooksSuccess({ data: [] }) }); expect(effects.loadBooks$).toBeObservable(expected); }); - it('should do nothing if store is already filled', () => { + it('tut nichts, wenn der Store bereits gefüllt ist', () => { const store = TestBed.inject(MockStore); store.setState({ - book: { - books: [book('1')], - loading: false - } + [bookFeatureKey]: { ...initialState, books: [b('1')] } }); - actions$ = hot('--a', { a: loadBooks() }); + actions$ = hot('--a', { a: BookActions.loadBooks() }); const expected = cold('---'); expect(effects.loadBooks$).toBeObservable(expected); @@ -1307,33 +1544,43 @@ describe('BookEffects', () => { }); ``` -Ebenso können wir mit dem `MockStore` die Rückgabewerte von Selektoren überschreiben. Unser letztes Beispiel zum Thema zeigt, wie wir sicherstellen können, dass die `BookList` tatsächlich den Text *"Loading ..."* anzeigt, wenn im State das Loading-Flag entsprechend gesetzt ist. Es ist dabei nicht notwendig, den State im `MockStore` zu setzen, da wir auf diesen nicht direkt zugreifen. Die Komponente bezieht ihre Daten über die beiden Selektoren `selectAllBooks` und `selectBooksLoading`, die wir entsprechend "ausmocken": +Ebenso können wir mit dem `MockStore` die Rückgabewerte von Selektoren überschreiben. Unser letztes Beispiel zum Thema zeigt, wie wir sicherstellen können, dass die `BooksOverview` tatsächlich den Ladeindikator anzeigt, wenn im State das Loading-Flag entsprechend gesetzt ist. Es ist dabei nicht notwendig, den State im `MockStore` zu setzen, da wir auf diesen nicht direkt zugreifen. Die Komponente bezieht ihre Daten über die Selektoren, die wir entsprechend "ausmocken". Zusätzlich können wir mit `vi.spyOn(store, 'dispatch')` prüfen, welche Actions die Komponente dispatcht: ```ts -// books/book-list/book-list.spec.ts -import { provideMockStore } from '@ngrx/store/testing'; +// books/books-overview/books-overview.spec.ts +import { MockStore, provideMockStore } from '@ngrx/store/testing'; -describe('BookList', () => { - beforeEach(async () => { +describe('BooksOverview', () => { + async function setup(loading = false) { await TestBed.configureTestingModule({ - imports: [ BookList ], + imports: [BooksOverview], providers: [ provideMockStore({ selectors: [ - { selector: selectBooksLoading, value: true }, { selector: selectAllBooks, value: [] }, + { selector: selectLikedBooks, value: [] }, + { selector: selectBooksLoading, value: loading }, + { selector: selectBooksError, value: null } ] }) ] - }) - .compileComponents(); + }).compileComponents(); + + const store = TestBed.inject(MockStore); + const dispatch = vi.spyOn(store, 'dispatch'); + const fixture = TestBed.createComponent(BooksOverview); + await fixture.whenStable(); + return { fixture, dispatch, el: fixture.nativeElement as HTMLElement }; + } + + it('dispatcht loadBooks beim Erzeugen', async () => { + const { dispatch } = await setup(); + expect(dispatch).toHaveBeenCalledWith(BookActions.loadBooks()); }); - it('should show a loading text', () => { - const fixture = TestBed.createComponent(BookList); - fixture.detectChanges(); - const element = fixture.nativeElement; - expect(element.querySelector('.loader').textContent).toBe('Loading ...'); + it('zeigt den Ladeindikator', async () => { + const { el } = await setup(true); + expect(el.querySelector('.loader')).toBeTruthy(); }); }); ``` @@ -1375,7 +1622,7 @@ In der Komponente nutzen wir die Facade, um die Daten aus den Properties zu lese ```ts @Component({ /* ... */ }) -export class BookList { +export class BooksOverview { private booksFacade = inject(BooksFacade); books = this.booksFacade.books; diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md index 373981c3..b1ddd0cb 100644 --- a/material/ngrx-intro/README.md +++ b/material/ngrx-intro/README.md @@ -26,7 +26,7 @@ Mit wachsender Größe der Anwendung ergeben sich immer wieder folgende Fragen: - Wie reagieren wir an verschiedenen Stellen auf Ereignisse, die in der Anwendung auftreten? - Wie verwalten wir die Daten, die über die gesamte Anwendung verteilt sind? -Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookApi`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter treiben: Bislang waren Komponenten die "Hüterinnen" der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine gegenüber den anderen Komponenten abgeschottete Einheit. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. +Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookStore`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter treiben: Bislang waren Komponenten die "Hüterinnen" der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine gegenüber den anderen Komponenten abgeschottete Einheit. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. Wir wollen in diesem Artikel besprechen, wie eine solche zentrale Zustandsverwaltung (engl. *State Management*) realisiert werden kann. Dabei lernen wir das Architekturmuster *Redux* kennen und nutzen das populäre Framework *Reactive State for Angular (NgRx)*, um den Anwendungszustand zu verwalten und unsere Prozesse zu koordinieren. diff --git a/material/ngrx-signal-store/README.md b/material/ngrx-signal-store/README.md index b9136c20..880f6f57 100644 --- a/material/ngrx-signal-store/README.md +++ b/material/ngrx-signal-store/README.md @@ -36,7 +36,9 @@ Kurz gesagt: Der Global Store ist *ein* großes, strenges System. Der SignalStor ## Der SignalStore im Detail -Schauen wir uns die Bausteine konkret an. Als durchgehendes Beispiel verwenden wir wieder den BookManager. Wir bauen Schritt für Schritt einen vollständigen `BookStore`, der die Buchliste vom Server lädt **und** Bücher anlegen, ändern und löschen kann – inklusive Lade- und Fehleranzeige. So lässt sich der direkte Vergleich zum Global Store aus Teil 2 ziehen. +Schauen wir uns die Bausteine konkret an. Als durchgehendes Beispiel verwenden wir wieder den BookManager. Wir bauen Schritt für Schritt einen vollständigen Store, der die Buchliste vom Server lädt **und** Bücher anlegen und löschen kann – inklusive Lade- und Fehleranzeige. So lässt sich der direkte Vergleich zum Global Store aus Teil 2 ziehen. + +Ein Wort zur Benennung, bevor wir loslegen: Im BookManager kümmert sich – wie schon in Teil 2 – ein Datenservice um die HTTP-Aufrufe gegen die API. Dieser Service heißt `BookStore` und liegt in `shared/book-store.ts`. Den SignalStore, den wir gleich aufbauen, nennen wir deshalb zur klaren Abgrenzung `BookSignalStore` (Datei `book.store.ts`). Der `BookSignalStore` verwaltet den Zustand und ruft für den Datenzugriff den `BookStore`-Service auf. ### Installation @@ -46,7 +48,18 @@ Der SignalStore wird über ein eigenes Paket installiert: ng add @ngrx/signals ``` -Anders als beim Global Store gibt es hier **nichts in der `app.config.ts` zu registrieren** – kein `provideStore()`, kein `provideEffects()`. Ein SignalStore ist ein gewöhnlicher Service und wird dort bereitgestellt, wo wir ihn brauchen. +Anders als beim Global Store gibt es für den SignalStore selbst **nichts in der `app.config.ts` zu registrieren** – kein `provideStore()`, kein `provideEffects()`. Ein SignalStore ist ein gewöhnlicher Service und wird dort bereitgestellt, wo wir ihn brauchen. Lediglich der `HttpClient` für den Datenzugriff gehört wie üblich in die `app.config.ts`: + +```ts +// app.config.ts +import { provideHttpClient, withFetch } from '@angular/common/http'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideHttpClient(withFetch()) + ] +}; +``` ### Ein Store als Komposition von Features @@ -71,12 +84,12 @@ const initialState: BookState = { error: null }; -export const BookStore = signalStore( +export const BookSignalStore = signalStore( withState(initialState) ); ``` -Allein dadurch besitzt eine Instanz von `BookStore` bereits drei Signale: `books: Signal`, `loading: Signal` und `error: Signal`. Verschachtelte Objekte werden übrigens zu einem `DeepSignal`, das zusätzlich für jede Eigenschaft ein eigenes Signal bereitstellt. +Allein dadurch besitzt eine Instanz von `BookSignalStore` bereits drei Signale: `books: Signal`, `loading: Signal` und `error: Signal`. Verschachtelte Objekte werden übrigens zu einem `DeepSignal`, das zusätzlich für jede Eigenschaft ein eigenes Signal bereitstellt. ### Den Store bereitstellen: lokal oder global @@ -85,7 +98,7 @@ Ein SignalStore ist standardmäßig **an keinen Injector gebunden**. Wir entsche **Global** (eine geteilte Instanz für die ganze Anwendung) – über die Option `{ providedIn: 'root' }` direkt bei der Definition: ```ts -export const BookStore = signalStore( +export const BookSignalStore = signalStore( { providedIn: 'root' }, withState(initialState) ); @@ -96,10 +109,10 @@ export const BookStore = signalStore( ```ts @Component({ // ... - providers: [BookStore] + providers: [BookSignalStore] }) -export class BookList { - private store = inject(BookStore); +export class BooksOverview { + private store = inject(BookSignalStore); } ``` @@ -108,12 +121,12 @@ Auch eine **Route** kann den Store bereitstellen – ideal für ein lazy geladen ```ts // books/books.routes.ts import { Routes } from '@angular/router'; -import { BookStore } from './book.store'; +import { BookSignalStore } from './book.store'; export const booksRoutes: Routes = [ { path: '', - providers: [BookStore], // eigener Store-Lebenszyklus für dieses Feature + providers: [BookSignalStore], // eigener Store-Lebenszyklus für dieses Feature children: [ // ... Routen des Features ] @@ -155,7 +168,7 @@ Berechnete Werte ergänzen wir mit dem Feature `withComputed()`. Es bekommt die import { computed } from '@angular/core'; import { signalStore, withComputed, withState } from '@ngrx/signals'; -export const BookStore = signalStore( +export const BookSignalStore = signalStore( { providedIn: 'root' }, withState(initialState), withComputed(({ books }) => ({ @@ -176,7 +189,7 @@ Verhalten fügen wir mit dem Feature `withMethods()` hinzu. Die Factory erhält import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; import { Book } from '../shared/book'; -export const BookStore = signalStore( +export const BookSignalStore = signalStore( { providedIn: 'root' }, withState(initialState), withMethods((store) => ({ @@ -198,7 +211,9 @@ Hier liegt der zentrale Unterschied zum Global Store: Es gibt keine Action und k ### Asynchrone Seiteneffekte mit `rxMethod`: Daten laden -Für das Laden der Bücher brauchen wir einen asynchronen Seiteneffekt – die Aufgabe, die im Global Store ein Effect übernommen hat. Eine Store-Methode darf eine ganz gewöhnliche Methode sein; einfache Abläufe ließen sich daher auch mit einer `async`-Methode und einem Promise erledigen, die das Ergebnis am Ende per `patchState()` in den State schreibt. Wir zeigen hier aber den reaktiven Weg mit RxJS, weil er sich nahtlos mit einem HttpClient kombinieren lässt: Dafür bietet das Interop-Plugin die Funktion `rxMethod()` aus `@ngrx/signals/rxjs-interop`. Sie nimmt eine Kette von RxJS-Operatoren entgegen und gibt eine reaktive Methode zurück. +Für das Laden der Bücher brauchen wir einen asynchronen Seiteneffekt – die Aufgabe, die im Global Store ein Effect übernommen hat. Den eigentlichen HTTP-Aufruf übernimmt der `BookStore`-Service: Er kapselt wie schon in Teil 2 die Aufrufe mit dem `HttpClient` gegen die BookManager-API (`https://api1.angular-buch.com`) und bietet die Methoden `getAll()`, `create()` und `remove()` an, die jeweils ein `Observable` zurückgeben. Wir fordern ihn in der `withMethods`-Factory per `inject()` an. + +Eine Store-Methode darf eine ganz gewöhnliche Methode sein; einfache Abläufe ließen sich daher auch mit einer `async`-Methode und einem Promise erledigen, die das Ergebnis am Ende per `patchState()` in den State schreibt. Wir zeigen hier aber den reaktiven Weg mit RxJS, weil er sich nahtlos mit den `Observable`s des `BookStore`-Service kombinieren lässt: Dafür bietet das Interop-Plugin die Funktion `rxMethod()` aus `@ngrx/signals/rxjs-interop`. Sie nimmt eine Kette von RxJS-Operatoren entgegen und gibt eine reaktive Methode zurück. Zum sicheren Verarbeiten der HTTP-Antwort nutzen wir den Operator `tapResponse` aus `@ngrx/operators`. Er ruft je nach Ausgang den `next`-, `error`- oder `finalize`-Callback auf – und ein Fehler im Service-Aufruf beendet dadurch nicht den reaktiven Strom der Methode: @@ -208,16 +223,16 @@ import { pipe, switchMap, tap } from 'rxjs'; import { patchState, withMethods } from '@ngrx/signals'; import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; -import { BookApi } from '../shared/book-api'; +import { BookStore } from '../shared/book-store'; import { toMessage } from '../shared/error-message'; // innerhalb von signalStore(...): -withMethods((store, service = inject(BookApi)) => ({ +withMethods((store, bookStore = inject(BookStore)) => ({ loadBooks: rxMethod( pipe( tap(() => patchState(store, { loading: true, error: null })), switchMap(() => - service.getAll().pipe( + bookStore.getAll().pipe( tapResponse({ next: books => patchState(store, { books }), error: (err: unknown) => patchState(store, { error: toMessage(err) }), @@ -230,34 +245,39 @@ withMethods((store, service = inject(BookApi)) => ({ })) ``` -Den Fehler behandeln wir wie in Teil 2 mit der kleinen Hilfsfunktion `toMessage()`. Der `error`-Callback von `tapResponse` liefert den Fehler als `unknown` – wir prüfen also die Form, statt blind auf eine Eigenschaft zuzugreifen: +Den Fehler behandeln wir wie in Teil 2 mit der kleinen Hilfsfunktion `toMessage()`. Der `error`-Callback von `tapResponse` liefert den Fehler als `unknown` – wir prüfen also die Form, statt blind auf eine Eigenschaft zuzugreifen. Die BookManager-API liefert ihre Fehlermeldungen als `{ error: string }`, verpackt in einer `HttpErrorResponse` (etwa HTTP 409 bei einer doppelten ISBN); diesen Fall fangen wir gesondert ab: ```ts // shared/error-message.ts +import { HttpErrorResponse } from '@angular/common/http'; + export function toMessage(error: unknown): string { + if (error instanceof HttpErrorResponse) { + return error.error?.error ?? error.message; + } return error instanceof Error ? error.message : 'Ein unbekannter Fehler ist aufgetreten.'; } ``` Die so erzeugte Methode rufen wir später einfach als `store.loadBooks()` auf. Wir haben hier `switchMap()` gewählt: Wird während eines laufenden Ladevorgangs erneut geladen, soll nur die letzte Anfrage zählen. -### Schreiben: Bücher anlegen, ändern und löschen +### Schreiben: Bücher anlegen und löschen -Jetzt zum Kern jeder echten Anwendung: Daten verändern. Wir definieren je eine Methode zum Anlegen, Ändern und Löschen. Jede löst einen HTTP-Request aus – über den `BookApi`, der wie schon in Teil 2 die HTTP-Aufrufe mit dem `HttpClient` kapselt und `create()`, `update()` und `remove()` anbietet (`provideHttpClient()` gehört dafür in die `app.config.ts`) – und schreibt das Ergebnis anschließend immutabel in den State. +Jetzt zum Kern jeder echten Anwendung: Daten verändern. Wir definieren je eine Methode zum Anlegen und zum Löschen. Jede löst einen HTTP-Request über den `BookStore`-Service aus – dessen `create()` bzw. `remove()` – und schreibt das Ergebnis anschließend immutabel in den State. Ein wichtiger Unterschied zum Laden betrifft den Flattening-Operator: Beim Laden ist `switchMap()` richtig (eine neue Anfrage macht die alte überflüssig). Bei **schreibenden** Operationen wollen wir laufende Requests aber *nicht* abbrechen – sonst ginge womöglich ein Speichervorgang verloren. Hier ist `concatMap()` die sichere Wahl: Die Anfragen werden der Reihe nach abgearbeitet. ```ts import { concatMap, pipe, tap } from 'rxjs'; -// innerhalb von withMethods((store, service = inject(BookApi)) => ({ ... })): +// innerhalb von withMethods((store, bookStore = inject(BookStore)) => ({ ... })): // Anlegen: an die Liste anhängen addBook: rxMethod( pipe( tap(() => patchState(store, { error: null })), concatMap(book => - service.create(book).pipe( + bookStore.create(book).pipe( tapResponse({ next: created => patchState(store, state => ({ books: [...state.books, created] })), @@ -268,30 +288,12 @@ addBook: rxMethod( ) ), -// Ändern: das passende Buch in der Liste ersetzen -updateBook: rxMethod( - pipe( - tap(() => patchState(store, { error: null })), - concatMap(book => - service.update(book).pipe( - tapResponse({ - next: updated => - patchState(store, state => ({ - books: state.books.map(b => (b.isbn === updated.isbn ? updated : b)) - })), - error: (err: unknown) => patchState(store, { error: toMessage(err) }) - }) - ) - ) - ) -), - // Löschen: das Buch anhand seiner ISBN aus der Liste filtern deleteBook: rxMethod( pipe( tap(() => patchState(store, { error: null })), concatMap(isbn => - service.remove(isbn).pipe( + bookStore.remove(isbn).pipe( tapResponse({ next: () => patchState(store, state => ({ @@ -305,11 +307,40 @@ deleteBook: rxMethod( ) ``` -An diesen drei Methoden sieht man das Muster für jede Mutation: zu Beginn eine alte Fehlermeldung zurücksetzen, den HTTP-Request auslösen, bei Erfolg den State **immutabel** anpassen (anhängen mit Spread, ersetzen mit `map()`, entfernen mit `filter()`) und bei Fehler die Meldung in `error` schreiben. Genau diese immutable Logik ist auch das Herz eines Reducers im Global Store – nur dass wir sie hier direkt in der Methode notieren, ohne Action und Reducer dazwischen. +An diesen beiden Methoden sieht man das Muster für jede Mutation: zu Beginn eine alte Fehlermeldung zurücksetzen, den HTTP-Request auslösen, bei Erfolg den State **immutabel** anpassen (anhängen mit Spread, entfernen mit `filter()`) und bei Fehler die Meldung in `error` schreiben. Genau diese immutable Logik ist auch das Herz eines Reducers im Global Store – nur dass wir sie hier direkt in der Methode notieren, ohne Action und Reducer dazwischen. + +### Reiner Client-State: Favoriten ohne `rxMethod` + +Bislang ging es um Zustand, der mit dem Server abgeglichen wird. Echte Anwendungen haben aber auch Zustand, der **nur im Client** lebt und nie zum Server wandert. Im BookManager ist das die Favoriten-Liste: Wer ein Buch mag, markiert es als Favorit – ein reines UI-Feature ohne HTTP-Aufruf. + +Genau hier zeigt sich ein didaktisch wichtiger Punkt: **Synchroner Client-State braucht keinen Seiteneffekt und damit auch kein `rxMethod`.** Wir ergänzen den State um einen Slice `likedBooks: Book[]`, leiten mit `withComputed` die Anzahl ab und ändern die Liste in gewöhnlichen Methoden direkt per `patchState()` – ganz ohne RxJS, ohne `tapResponse`, ohne Flattening-Operator. Das Anlegen dedupliziert dabei anhand der ISBN, damit ein Buch nicht doppelt in den Favoriten landet: + +```ts +// im State: +type BookState = { + // ... books, loading, error + likedBooks: Book[]; +}; + +// in withComputed (zusätzlich zu booksCount): +likedCount: computed(() => likedBooks().length), + +// in withMethods – reiner Client-State, dedupliziert, ganz ohne Seiteneffekt: +likeBook(book: Book): void { + if (!store.likedBooks().some(b => b.isbn === book.isbn)) { + patchState(store, state => ({ likedBooks: [...state.likedBooks, book] })); + } +}, +clearLikedBooks(): void { + patchState(store, { likedBooks: [] }); +} +``` + +Der Kontrast zu `loadBooks`, `addBook` und `deleteBook` ist lehrreich: Dort umhüllt `rxMethod` einen asynchronen HTTP-Aufruf, dessen Ausgang wir mit `tapResponse` abwarten. Hier passiert alles synchron im Speicher – eine schlichte Methode mit `patchState()` genügt. Eine Store-Methode muss eben *nicht* reaktiv sein; sie wird es nur dort, wo tatsächlich ein Seiteneffekt im Spiel ist. ### Das vollständige Beispiel -Setzen wir alle Bausteine zusammen, ergibt sich der komplette, kopierbare `BookStore`. Über `withHooks()` lädt er seine Daten beim ersten Verwenden gleich selbst (`onInit`), sodass die Komponente nur noch liest und Aktionen auslöst: +Setzen wir alle Bausteine zusammen, ergibt sich der komplette, kopierbare `BookSignalStore`. Über `withHooks()` lädt er seine Daten beim ersten Verwenden gleich selbst (`onInit`), sodass die Komponente nur noch liest und Aktionen auslöst: ```ts // books/book.store.ts @@ -327,36 +358,51 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop'; import { tapResponse } from '@ngrx/operators'; import { Book } from '../shared/book'; -import { BookApi } from '../shared/book-api'; +import { BookStore } from '../shared/book-store'; import { toMessage } from '../shared/error-message'; type BookState = { books: Book[]; loading: boolean; error: string | null; + likedBooks: Book[]; }; const initialState: BookState = { books: [], loading: false, - error: null + error: null, + likedBooks: [] }; -export const BookStore = signalStore( +export const BookSignalStore = signalStore( { providedIn: 'root' }, withState(initialState), - withComputed(({ books }) => ({ - booksCount: computed(() => books().length) + withComputed(({ books, likedBooks }) => ({ + booksCount: computed(() => books().length), + likedCount: computed(() => likedBooks().length) })), - withMethods((store, service = inject(BookApi)) => ({ + withMethods((store, bookStore = inject(BookStore)) => ({ clearError(): void { patchState(store, { error: null }); }, + + // Favoriten – reiner Client-State, dedupliziert, ganz ohne Seiteneffekt. + likeBook(book: Book): void { + if (!store.likedBooks().some(b => b.isbn === book.isbn)) { + patchState(store, state => ({ likedBooks: [...state.likedBooks, book] })); + } + }, + clearLikedBooks(): void { + patchState(store, { likedBooks: [] }); + }, + + // Lesen: switchMap – eine neue Anfrage macht die alte überflüssig. loadBooks: rxMethod( pipe( tap(() => patchState(store, { loading: true, error: null })), switchMap(() => - service.getAll().pipe( + bookStore.getAll().pipe( tapResponse({ next: books => patchState(store, { books }), error: (err: unknown) => patchState(store, { error: toMessage(err) }), @@ -366,11 +412,13 @@ export const BookStore = signalStore( ) ) ), + + // Schreiben: concatMap – laufende Requests werden nicht abgebrochen. addBook: rxMethod( pipe( tap(() => patchState(store, { error: null })), concatMap(book => - service.create(book).pipe( + bookStore.create(book).pipe( tapResponse({ next: created => patchState(store, state => ({ books: [...state.books, created] })), @@ -380,27 +428,11 @@ export const BookStore = signalStore( ) ) ), - updateBook: rxMethod( - pipe( - tap(() => patchState(store, { error: null })), - concatMap(book => - service.update(book).pipe( - tapResponse({ - next: updated => - patchState(store, state => ({ - books: state.books.map(b => (b.isbn === updated.isbn ? updated : b)) - })), - error: (err: unknown) => patchState(store, { error: toMessage(err) }) - }) - ) - ) - ) - ), deleteBook: rxMethod( pipe( tap(() => patchState(store, { error: null })), concatMap(isbn => - service.remove(isbn).pipe( + bookStore.remove(isbn).pipe( tapResponse({ next: () => patchState(store, state => ({ @@ -421,85 +453,161 @@ export const BookStore = signalStore( ); ``` -Das ist der gesamte State-Management-Code für ein vollständiges CRUD-Feature – eine Datei, ohne Actions, Reducer, Selektoren-Dateien oder Effects-Klassen. +Das ist der gesamte State-Management-Code für das Feature – eine Datei, ohne Actions, Reducer, Selektoren-Dateien oder Effects-Klassen. Sie umfasst das Laden, Anlegen und Löschen der Bücher sowie die Favoriten als reinen Client-State. -### Die Komponente: lesen und Aktionen auslösen +### Die Komponenten: lesen und Aktionen auslösen -Die Komponente injiziert den Store, liest die Signale im Template und ruft für Nutzeraktionen die Store-Methoden auf. Da der Store sich über `onInit` selbst lädt, braucht die Komponente keinen Lade-Code mehr: +Für die Oberfläche teilen wir die Verantwortung wie üblich in eine **smarte** und eine **präsentationale** Komponente auf. Die smarte `BooksOverview` injiziert den Store, liest die Signale im Template und ruft für Nutzeraktionen die Store-Methoden auf. Da der Store sich über `onInit` selbst lädt, braucht sie keinen Lade-Code mehr: ```ts -// books/book-list/book-list.ts +// books/books-overview/books-overview.ts import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { BookStore } from '../book.store'; + import { Book } from '../shared/book'; +import { BookSignalStore } from '../book.store'; +import { BookCard } from '../book-card/book-card'; @Component({ - selector: 'bm-book-list', - templateUrl: './book-list.html', + selector: 'app-books-overview', + imports: [BookCard], + templateUrl: './books-overview.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class BookList { - protected store = inject(BookStore); +export class BooksOverview { + protected store = inject(BookSignalStore); addBook(isbn: HTMLInputElement, title: HTMLInputElement): void { if (!isbn.value || !title.value) { return; } - this.store.addBook({ isbn: isbn.value, title: title.value, rating: 0 }); + this.store.addBook(this.#newBook(isbn.value, title.value)); isbn.value = ''; title.value = ''; } - rateUp(book: Book): void { - const rating = Math.min((book.rating ?? 0) + 1, 5); - this.store.updateBook({ ...book, rating }); - } - deleteBook(isbn: string): void { this.store.deleteBook(isbn); } + + likeBook(book: Book): void { + this.store.likeBook(book); + } + + // Ein vollständiges Buch mit sinnvollen Defaults, damit die echte API den POST annimmt. + #newBook(isbn: string, title: string): Book { + return { + isbn, + title, + authors: ['Unbekannt'], + description: 'Über die Demo angelegt.', + imageUrl: 'https://cdn.ng-buch.de/cover-placeholder.png', + createdAt: new Date().toISOString() + }; + } } ``` -Im Template lesen wir die Signale und lösen über Events die Methoden aus: ein kleines Formular zum Anlegen, pro Buch ein Button zum Erhöhen der Bewertung und ein Löschen-Button sowie eine Fehlermeldung aus dem `error`-Signal: +Da ein `Book` mehrere Pflichtfelder hat (`authors`, `description`, `imageUrl`, `createdAt`), füllt die kleine Hilfe `#newBook()` sie mit sinnvollen Defaults, damit die echte API den POST akzeptiert. + +Im Template lesen wir die Signale und lösen über Events die Methoden aus. Es gibt zwei Sektionen: oben die Favoriten (mit `likedCount` und einem Button zum Leeren), darunter die Bücherliste mit Ladeindikator, Fehlermeldung, einem kleinen Anlege-Formular und je einer Karte pro Buch: ```html - -

                      Books ({{ store.booksCount() }})

                      + +
                      +

                      Favoriten ({{ store.likedCount() }})

                      + +
                        + @for (book of store.likedBooks(); track book.isbn) { +
                      • {{ book.title }}
                      • + } @empty { +
                      • Noch keine Favoriten.
                      • + } +
                      +
                      -@if (store.loading()) { -
                      Loading ...
                      -} +
                      +

                      Bücher ({{ store.booksCount() }})

                      -@if (store.error(); as error) { -
                      - {{ error }} - + @if (store.loading()) { +

                      Lädt …

                      + } + + @if (store.error(); as error) { +

                      + {{ error }} + +

                      + } + +
                      + + +
                      -} -
                      - - - -
                      +
                      + @for (book of store.books(); track book.isbn) { + + } +
                      +
                      +``` -
                        - @for (book of store.books(); track book.isbn) { -
                      • - {{ book.title }} – ★ {{ book.rating ?? 0 }} - - -
                      • +Die Eingabefelder reichen wir bewusst ohne `FormsModule` als lokale Template-Referenzen (`#isbnEl`) direkt an die Methode weiter, die nach dem Anlegen die Felder leert – so bleibt der Fokus auf dem SignalStore. + +Die einzelne Buchkarte ist eine rein **präsentationale** Komponente: Sie kennt den Store gar nicht, sondern bekommt das Buch über ein `input` und meldet Nutzeraktionen über `output`s zurück nach oben: + +```ts +// books/book-card/book-card.ts +import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; +import { Book } from '../shared/book'; + +@Component({ + selector: 'app-book-card', + templateUrl: './book-card.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BookCard { + readonly book = input.required(); + readonly like = output(); + readonly remove = output(); + + likeBook(): void { + this.like.emit(this.book()); } -
                      + + removeBook(): void { + this.remove.emit(this.book().isbn); + } +} ``` -Die Eingabefelder reichen wir bewusst ohne `FormsModule` als lokale Template-Referenzen (`#isbnEl`) direkt an die Methode weiter, die nach dem Anlegen die Felder leert – so bleibt der Fokus auf dem SignalStore. Weil der Store global (`providedIn: 'root'`) bereitsteht, teilen sich alle Komponenten dieselbe Instanz: Eine separate Detail- oder Formularkomponente, die `store.addBook()` aufruft, verändert denselben State, den die Liste anzeigt – die Oberfläche aktualisiert sich dank der Signale automatisch. +```html + +@let b = book(); + +
                      + +
                      +

                      {{ b.title }}

                      + @if (b.subtitle) { +

                      {{ b.subtitle }}

                      + } +

                      {{ b.authors.join(', ') }}

                      +

                      ISBN {{ b.isbn }}

                      +
                      +
                      + + +
                      +
                      +``` + +Weil der Store global (`providedIn: 'root'`) bereitsteht, teilen sich alle Komponenten dieselbe Instanz: Eine separate Detail- oder Formularkomponente, die `store.addBook()` aufruft, verändert denselben State, den die Übersicht anzeigt – die Oberfläche aktualisiert sich dank der Signale automatisch. ### Weniger Boilerplate: Entitäten mit `withEntities` -Die immutablen Array-Operationen aus unseren CRUD-Methoden (`[...state.books, created]`, `map(...)`, `filter(...)`) wiederholen sich in jeder Anwendung. Genau diese Routine nimmt uns das Plugin `@ngrx/signals/entities` mit dem Feature `withEntities()` ab – das Gegenstück zu `@ngrx/entity` aus Teil 2. Es legt die Signale `entityMap`, `ids` und `entities` an und bringt fertige Updater mit: `addEntity`, `updateEntity`, `removeEntity`, `setAllEntities` und weitere. +Die immutablen Array-Operationen aus unseren Methoden (`[...state.books, created]`, `filter(...)`) wiederholen sich in jeder Anwendung. Genau diese Routine nimmt uns das Plugin `@ngrx/signals/entities` mit dem Feature `withEntities()` ab – das Gegenstück zu `@ngrx/entity` aus Teil 2. Es legt die Signale `entityMap`, `ids` und `entities` an und bringt fertige Updater mit: `addEntity`, `updateEntity`, `removeEntity`, `setAllEntities` und weitere. Standardmäßig erwartet `withEntities` ein Property `id`. Da ein Buch im BookManager über seine `isbn` identifiziert wird, geben wir – wie schon bei `@ngrx/entity` – einen eigenen ID-Selektor an. Bei `add*`-, `set*`- und `update*`-Updatern übergeben wir ihn als zweites Argument; die `remove*`-Updater ermitteln die ID automatisch: @@ -515,22 +623,21 @@ import { addEntity, removeEntity, setAllEntities, - updateEntity, withEntities } from '@ngrx/signals/entities'; import { Book } from '../shared/book'; -import { BookApi } from '../shared/book-api'; +import { BookStore } from '../shared/book-store'; const selectId: SelectEntityId = book => book.isbn; -export const BookStore = signalStore( +export const BookSignalStore = signalStore( { providedIn: 'root' }, withEntities(), - withMethods((store, service = inject(BookApi)) => ({ + withMethods((store, bookStore = inject(BookStore)) => ({ loadBooks: rxMethod( switchMap(() => - service.getAll().pipe( + bookStore.getAll().pipe( tapResponse({ next: books => patchState(store, setAllEntities(books, { selectId })), error: console.error @@ -540,7 +647,7 @@ export const BookStore = signalStore( ), addBook: rxMethod( concatMap(book => - service.create(book).pipe( + bookStore.create(book).pipe( tapResponse({ next: created => patchState(store, addEntity(created, { selectId })), error: console.error @@ -548,20 +655,9 @@ export const BookStore = signalStore( ) ) ), - updateBook: rxMethod( - concatMap(book => - service.update(book).pipe( - tapResponse({ - next: updated => - patchState(store, updateEntity({ id: updated.isbn, changes: updated }, { selectId })), - error: console.error - }) - ) - ) - ), deleteBook: rxMethod( concatMap(isbn => - service.remove(isbn).pipe( + bookStore.remove(isbn).pipe( tapResponse({ next: () => patchState(store, removeEntity(isbn)), error: console.error @@ -599,34 +695,96 @@ Dieses Feature lässt sich anschließend in beliebig vielen Stores einsetzen: `s ### Testing -Da ein SignalStore ein gewöhnlicher Service ist, testen wir ihn auch wie einen Service: Instanz beziehen, Methoden aufrufen, Signale auslesen. Ein vollständiges Store-Setup wie beim Global Store ist nicht nötig. Den HTTP-Service ersetzen wir wie gewohnt per `{ provide: …, useValue: … }`. Weil unser Store sich über `onInit` beim Injizieren selbst lädt, müssen wir `loadBooks()` im Test nicht eigens aufrufen: +Da ein SignalStore ein gewöhnlicher Service ist, testen wir ihn auch wie einen Service: Instanz beziehen, Methoden aufrufen, Signale auslesen. Ein vollständiges Store-Setup wie beim Global Store ist nicht nötig. Den `BookStore`-Service ersetzen wir wie gewohnt per `{ provide: BookStore, useValue: … }` durch ein Mock mit `Observable`-Rückgaben. Weil unser `BookSignalStore` sich über `onInit` beim Injizieren selbst lädt, müssen wir `loadBooks()` im Test nicht eigens aufrufen: ```ts // books/book.store.spec.ts import { TestBed } from '@angular/core/testing'; -import { of } from 'rxjs'; -import { BookStore } from './book.store'; -import { BookApi } from '../shared/book-api'; +import { of, throwError } from 'rxjs'; + +import { BookSignalStore } from './book.store'; +import { BookStore } from '../shared/book-store'; import { Book } from '../shared/book'; -// kleine Hilfsfunktion, um schnell Testdaten zu erzeugen -const b = (isbn: string, title = `Title ${isbn}`): Book => ({ isbn, title }); +// kleine Hilfsfunktion, um schnell vollständige Testdaten zu erzeugen +const b = (isbn: string, title = `Titel ${isbn}`): Book => ({ + isbn, + title, + authors: ['Autor'], + description: 'Beschreibung', + imageUrl: 'https://example.com/cover.png', + createdAt: '2026-01-01T00:00:00.000Z' +}); + +/** Erzeugt einen frischen Store mit gemocktem BookStore-Service. */ +function createStore(mock: Partial) { + TestBed.configureTestingModule({ + providers: [{ provide: BookStore, useValue: mock }] + }); + return TestBed.inject(BookSignalStore); +} + +describe('BookSignalStore', () => { + it('lädt beim Init die Bücher und zählt sie', () => { + // onInit löst loadBooks() bereits beim Injizieren aus: + const store = createStore({ getAll: () => of([b('1'), b('2'), b('3')]) }); + expect(store.booksCount()).toBe(3); + }); + + it('addBook hängt ein Buch an die Liste an', () => { + const store = createStore({ getAll: () => of([b('1')]), create: book => of(book) }); + store.addBook(b('2', 'Neu')); + expect(store.books().map(x => x.isbn)).toEqual(['1', '2']); + }); + + it('schreibt eine Fehlermeldung in den State, wenn das Laden fehlschlägt', () => { + const store = createStore({ getAll: () => throwError(() => new Error('Netzwerkfehler')) }); + expect(store.error()).toBe('Netzwerkfehler'); + expect(store.loading()).toBe(false); + }); + + it('likeBook fügt einen Favoriten hinzu und dedupliziert anhand der ISBN', () => { + const store = createStore({ getAll: () => of([]) }); + store.likeBook(b('1')); + store.likeBook(b('1')); + expect(store.likedCount()).toBe(1); + }); +}); +``` + +Den `BookStore`-Service selbst – also den echten HTTP-Zugriff – prüfen wir getrennt mit dem `HttpTestingController`: + +```ts +// books/shared/book-store.spec.ts +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; + +import { BookStore } from './book-store'; -describe('BookStore', () => { - it('lädt Bücher und zählt sie', () => { +describe('BookStore (HTTP)', () => { + let store: BookStore; + let httpMock: HttpTestingController; + + beforeEach(() => { TestBed.configureTestingModule({ - providers: [ - { - provide: BookApi, - useValue: { getAll: () => of([b('1'), b('2'), b('3')]) } - } - ] + providers: [provideHttpClient(), provideHttpClientTesting()] }); + store = TestBed.inject(BookStore); + httpMock = TestBed.inject(HttpTestingController); + }); - // onInit löst loadBooks() bereits beim Injizieren aus: - const store = TestBed.inject(BookStore); + afterEach(() => httpMock.verify()); - expect(store.booksCount()).toBe(3); + it('getAll lädt die Bücher per GET', () => { + let result: unknown; + store.getAll().subscribe(r => (result = r)); + + const req = httpMock.expectOne('https://api1.angular-buch.com/books'); + expect(req.request.method).toBe('GET'); + req.flush([]); + + expect(result).toEqual([]); }); }); ``` @@ -651,7 +809,7 @@ Beide Bausteine stammen aus dem NgRx-Projekt und lösen dieselbe Aufgabe, setzen | Lesen | Selektoren + `selectSignal()` | State *ist* direkt ein Signal: `store.books()` | | Schreiben | Action `dispatch()` → Reducer | Methode aufrufen → `patchState()` | | Indirektion | hoch (Action/Reducer/Effect getrennt) | gering (Methode ändert State direkt) | -| Seiteneffekte | Effects (`@ngrx/effects`) | `rxMethod` (oder einfache `async`-Methoden) | +| Seiteneffekte | Effects (`@ngrx/effects`) | `rxMethod` für Async; synchroner Client-State (z. B. Favoriten) ganz ohne | | Boilerplate | viel (Actions, Reducer, Selektoren, Effects) | wenig (`with…`-Features in einer Datei) | | Entitäten | `@ngrx/entity` | `withEntities` (`@ngrx/signals/entities`) | | DevTools / Time Travel | ja (Redux DevTools) | nicht offiziell (Angular DevTools / Community) |

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�-9TRpA<_h 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 Q-bHND+p#f6$MVGg0gsR+DgXcg literal 0 HcmV?d00001 diff --git a/material/ngrx-intro/README.md b/material/ngrx-intro/README.md new file mode 100644 index 00000000..4d6a7022 --- /dev/null +++ b/material/ngrx-intro/README.md @@ -0,0 +1,279 @@ +--- +title: "State Management mit NgRx – Teil 1: Wie kommen wir zu zentralem State Management?" +published: "2026-06-11" +lastModified: "2026-06-11" +hidden: true +--- + +**Zusatzmaterial zum Buch *Angular: Das große Praxisbuch (4. Auflage)* von Ferdinand Malcher, Danny Koppenhagen und Johannes Hoppe.** + +Dieser Artikel ist **Teil 1** einer zweiteiligen Serie zum Thema State Management mit NgRx. + +- Teil 1: Wie kommen wir zu zentralem State Management? (dieser Artikel) +- Teil 2: Global Store mit NgRx → [zum Artikel](/material/ngrx-global-store) + +[[toc]] + +> NgRx provides robust state management for small and large projects. It enforces proper separation of concerns. Using it from the start reduces the risk of spaghetti when the project evolves. +> +> — Minko Gechev, Mitglied des Angular-Teams + +Wir haben unsere Anwendung bisher stets komponentenzentriert und serviceorientiert aufgebaut: Die Komponenten unserer Anwendung kommunizieren auf klar definierten Wegen über Property Bindings und Event Bindings. Um Daten zu erhalten und zu senden, nutzen die Komponenten verschiedene Services, in denen die HTTP-Kommunikation gekapselt ist oder über die wir Daten austauschen können. + +Diese Herangehensweise funktioniert im Prinzip sehr gut, und wir haben so eine vollständige Anwendung entwickeln können. Unsere Beispielanwendung ist allerdings vergleichsweise klein und übersichtlich – in der Praxis werden die Anwendungen wesentlich größer: Viele Komponenten greifen dann gleichzeitig auf geteilte Daten zu und nutzen dieselben Services. Auch die Performance spielt eine immer größere Rolle, je komplexer die Anwendung wird. Wir erreichen mit der bisher vorgestellten Herangehensweise schnell einen Punkt, an dem wir den Überblick über die Kommunikationswege verlieren. Es kommt immer häufiger zu unerklärlichen Konstellationen, da man nicht mehr nachvollziehen kann, welche Komponente andere Komponenten oder Services aufruft und in welcher Reihenfolge dies geschieht. Gleichzeitig führen die vielen Kommunikationswege zu entsprechend vielen Änderungen an den Daten, die von der Change Detection erkannt und verarbeitet werden müssen. Kurzum: Die Anwendung wird zunehmend schwerfälliger. + +Mit wachsender Größe der Anwendung ergeben sich immer wieder folgende Fragen: + +- Wie können wir Daten cachen und wiederverwenden, die über HTTP abgerufen wurden? +- Wie machen wir Daten für mehrere Komponenten gleichzeitig verfügbar? +- Wie reagieren wir an verschiedenen Stellen auf Ereignisse, die in der Anwendung auftreten? +- Wie verwalten wir die Daten, die über die gesamte Anwendung verteilt sind? + +Eine häufige Lösung für all diese Herausforderungen ist die *Zentralisierung*. Liegen die Daten an einem zentralen Ort in der Anwendung vor, so können sie von überall aus genutzt und verändert werden. Diesen Schritt geht man häufig ganz selbstverständlich, indem man etwa an einer geeigneten Stelle (z. B. im `BookStoreService`) einen Cache einbaut. Doch die Idee der Zentralisierung kann man noch viel weiter gehen: Bislang waren Komponenten die „Hüterinnen“ der Daten. Jede Komponente hatte ihren eigenen Zustand und bildete eine abgeschottete Einheit zu den anderen Komponenten. Diese Idee wollen wir nun auf den Kopf stellen. Die Komponenten sollen dazu ihre bisherige Kontrolle über die Daten und die Koordination der Prozesse an eine zentrale Stelle abgeben. Die Aufgabe der Komponenten ist es dann nur noch, Daten für die Anzeige zu lesen, neue Daten zu erfassen und Events an die zentrale Stelle zu senden. Diese Art der Zentralisierung ist ein entscheidender Unterschied zum bisherigen Vorgehen, wo alle Zustände über den gesamten Komponentenbaum hinweg verteilt waren. + +Wir wollen in diesem Kapitel besprechen, wie eine solche zentrale Zustandsverwaltung (engl. *State Management*) realisiert werden kann. Dabei lernen wir das Architekturmuster *Redux* kennen und nutzen das populäre Framework *Reactive Extensions for Angular (NgRx)*, um den Anwendungszustand zu verwalten und unsere Prozesse zu koordinieren. + +## Ein Modell für zentrales State Management + +Um uns der Idee des zentralen State Managements von Redux zu nähern, wollen wir zunächst ein eigenes Modell ohne den Einsatz eines Frameworks entwickeln. Wir beginnen mit einem einfachen Beispiel, verfeinern die Implementierung schrittweise und nähern uns so der finalen Lösung an. + +### Objekt in einem Service + +Um alle Daten und Zustände zu zentralisieren, legen wir in einem zentralen Service ein Zustandsobjekt ab. Wir definieren die Struktur dieses Objekts mit einem Interface, um von einer starken Typisierung zu profitieren. Als möglichst einfaches Beispiel dient uns eine Zahl, die man mithilfe einer Methode hochzählen kann. Dieser *State* kann natürlich noch weitere Eigenschaften besitzen; wir haben dies mit dem Property `anotherProperty` angedeutet. + +```ts +// Service mit zentralem Zustand +export interface State { + counter: number; + anotherProperty: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class StateService { + state: State = { + counter: 0, + anotherProperty: 'foo' + }; + + incrementCounter() { + this.state.counter++; + } +} +``` + +Unser Service hält ein Objekt mit einem initialen Zustand, das über die Methode `incrementCounter()` manipuliert werden kann. Alle Komponenten können diesen Service anfordern und die Daten aus dem Objekt nutzen und verändern. Die Change Detection von Angular hilft uns dabei, automatisch bei Änderungen die Views der Komponenten zu aktualisieren. + +```ts +// Zentralen Zustand in der Komponente verwenden +@Component({ ... }) +export class MyComponent { + constructor(private stateService: StateService) {} + + get counter() { + return this.stateService.state.counter; + } + + increment() { + this.stateService.incrementCounter(); + } +} +``` + +Den injizierten `StateService` können wir dann im Template nutzen, um die Daten anzuzeigen und die Methode `incrementCounter()` auszulösen: + +```html +