feat(angular): add ngrx root store generator (#16811)
This commit is contained in:
parent
279154436a
commit
e59c930ff9
@ -4246,6 +4246,14 @@
|
|||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "ngrx-root-store",
|
||||||
|
"path": "/packages/angular/generators/ngrx-root-store",
|
||||||
|
"name": "ngrx-root-store",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "pipe",
|
"id": "pipe",
|
||||||
"path": "/packages/angular/generators/pipe",
|
"path": "/packages/angular/generators/pipe",
|
||||||
|
|||||||
@ -267,6 +267,15 @@
|
|||||||
"path": "/packages/angular/generators/ngrx-feature-store",
|
"path": "/packages/angular/generators/ngrx-feature-store",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
},
|
},
|
||||||
|
"/packages/angular/generators/ngrx-root-store": {
|
||||||
|
"description": "Adds an NgRx Root Store to an application.",
|
||||||
|
"file": "generated/packages/angular/generators/ngrx-root-store.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "ngrx-root-store",
|
||||||
|
"originalFilePath": "/packages/angular/src/generators/ngrx-root-store/schema.json",
|
||||||
|
"path": "/packages/angular/generators/ngrx-root-store",
|
||||||
|
"type": "generator"
|
||||||
|
},
|
||||||
"/packages/angular/generators/pipe": {
|
"/packages/angular/generators/pipe": {
|
||||||
"description": "Generate an Angular Pipe",
|
"description": "Generate an Angular Pipe",
|
||||||
"file": "generated/packages/angular/generators/pipe.json",
|
"file": "generated/packages/angular/generators/pipe.json",
|
||||||
|
|||||||
@ -262,6 +262,15 @@
|
|||||||
"path": "angular/generators/ngrx-feature-store",
|
"path": "angular/generators/ngrx-feature-store",
|
||||||
"type": "generator"
|
"type": "generator"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Adds an NgRx Root Store to an application.",
|
||||||
|
"file": "generated/packages/angular/generators/ngrx-root-store.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "ngrx-root-store",
|
||||||
|
"originalFilePath": "/packages/angular/src/generators/ngrx-root-store/schema.json",
|
||||||
|
"path": "angular/generators/ngrx-root-store",
|
||||||
|
"type": "generator"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Generate an Angular Pipe",
|
"description": "Generate an Angular Pipe",
|
||||||
"file": "generated/packages/angular/generators/pipe.json",
|
"file": "generated/packages/angular/generators/pipe.json",
|
||||||
|
|||||||
@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "ngrx-root-store",
|
||||||
|
"factory": "./src/generators/ngrx-root-store/ngrx-root-store",
|
||||||
|
"schema": {
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"$id": "NxNgrxRootStoreGenerator",
|
||||||
|
"title": "Add NgRx support to an application.",
|
||||||
|
"description": "Adds NgRx support to an application.",
|
||||||
|
"cli": "nx",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the application to generate the NgRx configuration for.",
|
||||||
|
"$default": { "$source": "argv", "index": 0 },
|
||||||
|
"x-prompt": "What app would you like to generate a NgRx configuration for?",
|
||||||
|
"x-dropdown": "projects"
|
||||||
|
},
|
||||||
|
"minimal": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Only register the root state management setup or also generate a global feature state.",
|
||||||
|
"x-priority": "important"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the NgRx state, such as `products` or `users`. Recommended to use the plural form of the name.",
|
||||||
|
"x-priority": "important"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The route that the Standalone NgRx Providers should be added to. _Note: This is only supported in Angular versions >= 14.1.0_.",
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"directory": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "+state",
|
||||||
|
"description": "The name of the folder used to contain/group the generated NgRx files."
|
||||||
|
},
|
||||||
|
"facade": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Create a Facade class for the the feature.",
|
||||||
|
"x-prompt": "Would you like to use a Facade with your NgRx state?"
|
||||||
|
},
|
||||||
|
"skipImport": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Generate NgRx feature files without registering the feature in the NgModule."
|
||||||
|
},
|
||||||
|
"skipFormat": {
|
||||||
|
"description": "Skip formatting files.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"x-priority": "internal"
|
||||||
|
},
|
||||||
|
"skipPackageJson": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Do not update the `package.json` with NgRx dependencies.",
|
||||||
|
"x-priority": "internal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["project"],
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "Adds an NgRx Root Store to an application.",
|
||||||
|
"implementation": "/packages/angular/src/generators/ngrx-root-store/ngrx-root-store.ts",
|
||||||
|
"aliases": [],
|
||||||
|
"hidden": false,
|
||||||
|
"path": "/packages/angular/src/generators/ngrx-root-store/schema.json",
|
||||||
|
"type": "generator"
|
||||||
|
}
|
||||||
@ -259,6 +259,11 @@
|
|||||||
"schema": "./src/generators/ngrx-feature-store/schema.json",
|
"schema": "./src/generators/ngrx-feature-store/schema.json",
|
||||||
"description": "Adds an NgRx Feature Store to an application or library."
|
"description": "Adds an NgRx Feature Store to an application or library."
|
||||||
},
|
},
|
||||||
|
"ngrx-root-store": {
|
||||||
|
"factory": "./src/generators/ngrx-root-store/ngrx-root-store",
|
||||||
|
"schema": "./src/generators/ngrx-root-store/schema.json",
|
||||||
|
"description": "Adds an NgRx Root Store to an application."
|
||||||
|
},
|
||||||
"pipe": {
|
"pipe": {
|
||||||
"factory": "./src/generators/pipe/pipe",
|
"factory": "./src/generators/pipe/pipe",
|
||||||
"schema": "./src/generators/pipe/schema.json",
|
"schema": "./src/generators/pipe/schema.json",
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export * from './src/generators/library/library';
|
|||||||
export * from './src/generators/move/move';
|
export * from './src/generators/move/move';
|
||||||
export * from './src/generators/ngrx/ngrx';
|
export * from './src/generators/ngrx/ngrx';
|
||||||
export * from './src/generators/ngrx-feature-store/ngrx-feature-store';
|
export * from './src/generators/ngrx-feature-store/ngrx-feature-store';
|
||||||
|
export * from './src/generators/ngrx-root-store/ngrx-root-store';
|
||||||
export * from './src/generators/pipe/pipe';
|
export * from './src/generators/pipe/pipe';
|
||||||
export * from './src/generators/remote/remote';
|
export * from './src/generators/remote/remote';
|
||||||
export * from './src/generators/scam-directive/scam-directive';
|
export * from './src/generators/scam-directive/scam-directive';
|
||||||
|
|||||||
@ -0,0 +1,740 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add a facade when --facade=true 1`] = `
|
||||||
|
"import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||||
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
import { StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
|
import * as fromUsers from './+state/users.reducer';
|
||||||
|
import { UsersEffects } from './+state/users.effects';
|
||||||
|
import { UsersFacade } from './+state/users.facade';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AppComponent, NxWelcomeComponent],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' }),
|
||||||
|
StoreModule.forRoot(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
metaReducers: [],
|
||||||
|
runtimeChecks: {
|
||||||
|
strictActionImmutability: true,
|
||||||
|
strictStateImmutability: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
EffectsModule.forRoot([]),
|
||||||
|
StoreRouterConnectingModule.forRoot(),
|
||||||
|
StoreModule.forFeature(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer),
|
||||||
|
EffectsModule.forFeature([UsersEffects]),
|
||||||
|
],
|
||||||
|
providers: [UsersFacade],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add a facade when --facade=true 2`] = `
|
||||||
|
"import { Injectable, inject } from '@angular/core';
|
||||||
|
import { select, Store, Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
import * as UsersActions from './users.actions';
|
||||||
|
import * as UsersFeature from './users.reducer';
|
||||||
|
import * as UsersSelectors from './users.selectors';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersFacade {
|
||||||
|
private readonly store = inject(Store);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine pieces of state using createSelector,
|
||||||
|
* and expose them as observables through the facade.
|
||||||
|
*/
|
||||||
|
loaded$ = this.store.pipe(select(UsersSelectors.selectUsersLoaded));
|
||||||
|
allUsers$ = this.store.pipe(select(UsersSelectors.selectAllUsers));
|
||||||
|
selectedUsers$ = this.store.pipe(select(UsersSelectors.selectEntity));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the initialization action to perform one
|
||||||
|
* or more tasks in your Effects.
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.store.dispatch(UsersActions.initUsers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add a root module and root state when --minimal=false 1`] = `
|
||||||
|
"import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||||
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
import { StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
|
import * as fromUsers from './+state/users.reducer';
|
||||||
|
import { UsersEffects } from './+state/users.effects';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AppComponent, NxWelcomeComponent],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' }),
|
||||||
|
StoreModule.forRoot(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
metaReducers: [],
|
||||||
|
runtimeChecks: {
|
||||||
|
strictActionImmutability: true,
|
||||||
|
strictStateImmutability: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
EffectsModule.forRoot([]),
|
||||||
|
StoreRouterConnectingModule.forRoot(),
|
||||||
|
StoreModule.forFeature(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer),
|
||||||
|
EffectsModule.forFeature([UsersEffects]),
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add a root module and root state when --minimal=false 2`] = `
|
||||||
|
"import { createAction, props } from '@ngrx/store';
|
||||||
|
import { UsersEntity } from './users.models';
|
||||||
|
|
||||||
|
export const initUsers = createAction('[Users Page] Init');
|
||||||
|
|
||||||
|
export const loadUsersSuccess = createAction(
|
||||||
|
'[Users/API] Load Users Success',
|
||||||
|
props<{ users: UsersEntity[] }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadUsersFailure = createAction(
|
||||||
|
'[Users/API] Load Users Failure',
|
||||||
|
props<{ error: any }>()
|
||||||
|
);
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add a root module and root state when --minimal=false 3`] = `
|
||||||
|
"import { Injectable, inject } from '@angular/core';
|
||||||
|
import { createEffect, Actions, ofType } from '@ngrx/effects';
|
||||||
|
import { switchMap, catchError, of } from 'rxjs';
|
||||||
|
import * as UsersActions from './users.actions';
|
||||||
|
import * as UsersFeature from './users.reducer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersEffects {
|
||||||
|
private actions$ = inject(Actions);
|
||||||
|
|
||||||
|
init$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(UsersActions.initUsers),
|
||||||
|
switchMap(() => of(UsersActions.loadUsersSuccess({ users: [] }))),
|
||||||
|
catchError((error) => {
|
||||||
|
console.error('Error', error);
|
||||||
|
return of(UsersActions.loadUsersFailure({ error }));
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add a root module and root state when --minimal=false 4`] = `
|
||||||
|
"import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { hot } from 'jasmine-marbles';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import * as UsersActions from './users.actions';
|
||||||
|
import { UsersEffects } from './users.effects';
|
||||||
|
|
||||||
|
describe('UsersEffects', () => {
|
||||||
|
let actions: Observable<Action>;
|
||||||
|
let effects: UsersEffects;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
UsersEffects,
|
||||||
|
provideMockActions(() => actions),
|
||||||
|
provideMockStore(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
effects = TestBed.inject(UsersEffects);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('init$', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
actions = hot('-a-|', { a: UsersActions.initUsers() });
|
||||||
|
|
||||||
|
const expected = hot('-a-|', {
|
||||||
|
a: UsersActions.loadUsersSuccess({ users: [] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(effects.init$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add a root module and root state when --minimal=false 5`] = `
|
||||||
|
"import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
|
||||||
|
import { createReducer, on, Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
import * as UsersActions from './users.actions';
|
||||||
|
import { UsersEntity } from './users.models';
|
||||||
|
|
||||||
|
export const USERS_FEATURE_KEY = 'users';
|
||||||
|
|
||||||
|
export interface UsersState extends EntityState<UsersEntity> {
|
||||||
|
selectedId?: string | number; // which Users record has been selected
|
||||||
|
loaded: boolean; // has the Users list been loaded
|
||||||
|
error?: string | null; // last known error (if any)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsersPartialState {
|
||||||
|
readonly [USERS_FEATURE_KEY]: UsersState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usersAdapter: EntityAdapter<UsersEntity> =
|
||||||
|
createEntityAdapter<UsersEntity>();
|
||||||
|
|
||||||
|
export const initialUsersState: UsersState = usersAdapter.getInitialState({
|
||||||
|
// set initial required properties
|
||||||
|
loaded: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reducer = createReducer(
|
||||||
|
initialUsersState,
|
||||||
|
on(UsersActions.initUsers, (state) => ({
|
||||||
|
...state,
|
||||||
|
loaded: false,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
on(UsersActions.loadUsersSuccess, (state, { users }) =>
|
||||||
|
usersAdapter.setAll(users, { ...state, loaded: true })
|
||||||
|
),
|
||||||
|
on(UsersActions.loadUsersFailure, (state, { error }) => ({ ...state, error }))
|
||||||
|
);
|
||||||
|
|
||||||
|
export function usersReducer(state: UsersState | undefined, action: Action) {
|
||||||
|
return reducer(state, action);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add a root module and root state when --minimal=false 6`] = `
|
||||||
|
"import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||||
|
import { USERS_FEATURE_KEY, UsersState, usersAdapter } from './users.reducer';
|
||||||
|
|
||||||
|
// Lookup the 'Users' feature state managed by NgRx
|
||||||
|
export const selectUsersState =
|
||||||
|
createFeatureSelector<UsersState>(USERS_FEATURE_KEY);
|
||||||
|
|
||||||
|
const { selectAll, selectEntities } = usersAdapter.getSelectors();
|
||||||
|
|
||||||
|
export const selectUsersLoaded = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => state.loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectUsersError = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => state.error
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectAllUsers = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => selectAll(state)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectUsersEntities = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => selectEntities(state)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectSelectedId = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => state.selectedId
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectEntity = createSelector(
|
||||||
|
selectUsersEntities,
|
||||||
|
selectSelectedId,
|
||||||
|
(entities, selectedId) => (selectedId ? entities[selectedId] : undefined)
|
||||||
|
);
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add a root module and root state when --minimal=false 7`] = `
|
||||||
|
"import { UsersEntity } from './users.models';
|
||||||
|
import {
|
||||||
|
usersAdapter,
|
||||||
|
UsersPartialState,
|
||||||
|
initialUsersState,
|
||||||
|
} from './users.reducer';
|
||||||
|
import * as UsersSelectors from './users.selectors';
|
||||||
|
|
||||||
|
describe('Users Selectors', () => {
|
||||||
|
const ERROR_MSG = 'No Error Available';
|
||||||
|
const getUsersId = (it: UsersEntity) => it.id;
|
||||||
|
const createUsersEntity = (id: string, name = '') =>
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
name: name || \`name-\${id}\`,
|
||||||
|
} as UsersEntity);
|
||||||
|
|
||||||
|
let state: UsersPartialState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state = {
|
||||||
|
users: usersAdapter.setAll(
|
||||||
|
[
|
||||||
|
createUsersEntity('PRODUCT-AAA'),
|
||||||
|
createUsersEntity('PRODUCT-BBB'),
|
||||||
|
createUsersEntity('PRODUCT-CCC'),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
...initialUsersState,
|
||||||
|
selectedId: 'PRODUCT-BBB',
|
||||||
|
error: ERROR_MSG,
|
||||||
|
loaded: true,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Users Selectors', () => {
|
||||||
|
it('selectAllUsers() should return the list of Users', () => {
|
||||||
|
const results = UsersSelectors.selectAllUsers(state);
|
||||||
|
const selId = getUsersId(results[1]);
|
||||||
|
|
||||||
|
expect(results.length).toBe(3);
|
||||||
|
expect(selId).toBe('PRODUCT-BBB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selectEntity() should return the selected Entity', () => {
|
||||||
|
const result = UsersSelectors.selectEntity(state) as UsersEntity;
|
||||||
|
const selId = getUsersId(result);
|
||||||
|
|
||||||
|
expect(selId).toBe('PRODUCT-BBB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selectUsersLoaded() should return the current "loaded" status', () => {
|
||||||
|
const result = UsersSelectors.selectUsersLoaded(state);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selectUsersError() should return the current "error" state', () => {
|
||||||
|
const result = UsersSelectors.selectUsersError(state);
|
||||||
|
|
||||||
|
expect(result).toBe(ERROR_MSG);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator NgModule should add an empty root module when --minimal=true 1`] = `
|
||||||
|
"import { NgModule } from '@angular/core';
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||||
|
import { StoreModule } from '@ngrx/store';
|
||||||
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
import { StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AppComponent, NxWelcomeComponent],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
RouterModule.forRoot(appRoutes, { initialNavigation: 'enabledBlocking' }),
|
||||||
|
StoreModule.forRoot(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
metaReducers: [],
|
||||||
|
runtimeChecks: {
|
||||||
|
strictActionImmutability: true,
|
||||||
|
strictStateImmutability: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
EffectsModule.forRoot([]),
|
||||||
|
StoreRouterConnectingModule.forRoot(),
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
bootstrap: [AppComponent],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add a facade when --facade=true 1`] = `
|
||||||
|
"import { ApplicationConfig } from '@angular/core';
|
||||||
|
import {
|
||||||
|
provideRouter,
|
||||||
|
withEnabledBlockingInitialNavigation,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { provideStore, provideState } from '@ngrx/store';
|
||||||
|
import { provideEffects } from '@ngrx/effects';
|
||||||
|
import * as fromUsers from './+state/users.reducer';
|
||||||
|
import { UsersEffects } from './+state/users.effects';
|
||||||
|
import { UsersFacade } from './+state/users.facade';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideEffects(UsersEffects),
|
||||||
|
provideState(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer),
|
||||||
|
UsersFacade,
|
||||||
|
provideEffects(),
|
||||||
|
provideStore(),
|
||||||
|
provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add a facade when --facade=true 2`] = `
|
||||||
|
"import { Injectable, inject } from '@angular/core';
|
||||||
|
import { select, Store, Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
import * as UsersActions from './users.actions';
|
||||||
|
import * as UsersFeature from './users.reducer';
|
||||||
|
import * as UsersSelectors from './users.selectors';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersFacade {
|
||||||
|
private readonly store = inject(Store);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine pieces of state using createSelector,
|
||||||
|
* and expose them as observables through the facade.
|
||||||
|
*/
|
||||||
|
loaded$ = this.store.pipe(select(UsersSelectors.selectUsersLoaded));
|
||||||
|
allUsers$ = this.store.pipe(select(UsersSelectors.selectAllUsers));
|
||||||
|
selectedUsers$ = this.store.pipe(select(UsersSelectors.selectEntity));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the initialization action to perform one
|
||||||
|
* or more tasks in your Effects.
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
this.store.dispatch(UsersActions.initUsers());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add a root module and root state when --minimal=false 1`] = `
|
||||||
|
"import { ApplicationConfig } from '@angular/core';
|
||||||
|
import {
|
||||||
|
provideRouter,
|
||||||
|
withEnabledBlockingInitialNavigation,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { provideStore, provideState } from '@ngrx/store';
|
||||||
|
import { provideEffects } from '@ngrx/effects';
|
||||||
|
import * as fromUsers from './+state/users.reducer';
|
||||||
|
import { UsersEffects } from './+state/users.effects';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideEffects(UsersEffects),
|
||||||
|
provideState(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer),
|
||||||
|
provideEffects(),
|
||||||
|
provideStore(),
|
||||||
|
provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add a root module and root state when --minimal=false 2`] = `
|
||||||
|
"import { createAction, props } from '@ngrx/store';
|
||||||
|
import { UsersEntity } from './users.models';
|
||||||
|
|
||||||
|
export const initUsers = createAction('[Users Page] Init');
|
||||||
|
|
||||||
|
export const loadUsersSuccess = createAction(
|
||||||
|
'[Users/API] Load Users Success',
|
||||||
|
props<{ users: UsersEntity[] }>()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const loadUsersFailure = createAction(
|
||||||
|
'[Users/API] Load Users Failure',
|
||||||
|
props<{ error: any }>()
|
||||||
|
);
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add a root module and root state when --minimal=false 3`] = `
|
||||||
|
"import { Injectable, inject } from '@angular/core';
|
||||||
|
import { createEffect, Actions, ofType } from '@ngrx/effects';
|
||||||
|
import { switchMap, catchError, of } from 'rxjs';
|
||||||
|
import * as UsersActions from './users.actions';
|
||||||
|
import * as UsersFeature from './users.reducer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersEffects {
|
||||||
|
private actions$ = inject(Actions);
|
||||||
|
|
||||||
|
init$ = createEffect(() =>
|
||||||
|
this.actions$.pipe(
|
||||||
|
ofType(UsersActions.initUsers),
|
||||||
|
switchMap(() => of(UsersActions.loadUsersSuccess({ users: [] }))),
|
||||||
|
catchError((error) => {
|
||||||
|
console.error('Error', error);
|
||||||
|
return of(UsersActions.loadUsersFailure({ error }));
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add a root module and root state when --minimal=false 4`] = `
|
||||||
|
"import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { hot } from 'jasmine-marbles';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import * as UsersActions from './users.actions';
|
||||||
|
import { UsersEffects } from './users.effects';
|
||||||
|
|
||||||
|
describe('UsersEffects', () => {
|
||||||
|
let actions: Observable<Action>;
|
||||||
|
let effects: UsersEffects;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
UsersEffects,
|
||||||
|
provideMockActions(() => actions),
|
||||||
|
provideMockStore(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
effects = TestBed.inject(UsersEffects);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('init$', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
actions = hot('-a-|', { a: UsersActions.initUsers() });
|
||||||
|
|
||||||
|
const expected = hot('-a-|', {
|
||||||
|
a: UsersActions.loadUsersSuccess({ users: [] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(effects.init$).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add a root module and root state when --minimal=false 5`] = `
|
||||||
|
"import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
|
||||||
|
import { createReducer, on, Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
import * as UsersActions from './users.actions';
|
||||||
|
import { UsersEntity } from './users.models';
|
||||||
|
|
||||||
|
export const USERS_FEATURE_KEY = 'users';
|
||||||
|
|
||||||
|
export interface UsersState extends EntityState<UsersEntity> {
|
||||||
|
selectedId?: string | number; // which Users record has been selected
|
||||||
|
loaded: boolean; // has the Users list been loaded
|
||||||
|
error?: string | null; // last known error (if any)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsersPartialState {
|
||||||
|
readonly [USERS_FEATURE_KEY]: UsersState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usersAdapter: EntityAdapter<UsersEntity> =
|
||||||
|
createEntityAdapter<UsersEntity>();
|
||||||
|
|
||||||
|
export const initialUsersState: UsersState = usersAdapter.getInitialState({
|
||||||
|
// set initial required properties
|
||||||
|
loaded: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reducer = createReducer(
|
||||||
|
initialUsersState,
|
||||||
|
on(UsersActions.initUsers, (state) => ({
|
||||||
|
...state,
|
||||||
|
loaded: false,
|
||||||
|
error: null,
|
||||||
|
})),
|
||||||
|
on(UsersActions.loadUsersSuccess, (state, { users }) =>
|
||||||
|
usersAdapter.setAll(users, { ...state, loaded: true })
|
||||||
|
),
|
||||||
|
on(UsersActions.loadUsersFailure, (state, { error }) => ({ ...state, error }))
|
||||||
|
);
|
||||||
|
|
||||||
|
export function usersReducer(state: UsersState | undefined, action: Action) {
|
||||||
|
return reducer(state, action);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add a root module and root state when --minimal=false 6`] = `
|
||||||
|
"import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||||
|
import { USERS_FEATURE_KEY, UsersState, usersAdapter } from './users.reducer';
|
||||||
|
|
||||||
|
// Lookup the 'Users' feature state managed by NgRx
|
||||||
|
export const selectUsersState =
|
||||||
|
createFeatureSelector<UsersState>(USERS_FEATURE_KEY);
|
||||||
|
|
||||||
|
const { selectAll, selectEntities } = usersAdapter.getSelectors();
|
||||||
|
|
||||||
|
export const selectUsersLoaded = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => state.loaded
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectUsersError = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => state.error
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectAllUsers = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => selectAll(state)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectUsersEntities = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => selectEntities(state)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectSelectedId = createSelector(
|
||||||
|
selectUsersState,
|
||||||
|
(state: UsersState) => state.selectedId
|
||||||
|
);
|
||||||
|
|
||||||
|
export const selectEntity = createSelector(
|
||||||
|
selectUsersEntities,
|
||||||
|
selectSelectedId,
|
||||||
|
(entities, selectedId) => (selectedId ? entities[selectedId] : undefined)
|
||||||
|
);
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add a root module and root state when --minimal=false 7`] = `
|
||||||
|
"import { UsersEntity } from './users.models';
|
||||||
|
import {
|
||||||
|
usersAdapter,
|
||||||
|
UsersPartialState,
|
||||||
|
initialUsersState,
|
||||||
|
} from './users.reducer';
|
||||||
|
import * as UsersSelectors from './users.selectors';
|
||||||
|
|
||||||
|
describe('Users Selectors', () => {
|
||||||
|
const ERROR_MSG = 'No Error Available';
|
||||||
|
const getUsersId = (it: UsersEntity) => it.id;
|
||||||
|
const createUsersEntity = (id: string, name = '') =>
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
name: name || \`name-\${id}\`,
|
||||||
|
} as UsersEntity);
|
||||||
|
|
||||||
|
let state: UsersPartialState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
state = {
|
||||||
|
users: usersAdapter.setAll(
|
||||||
|
[
|
||||||
|
createUsersEntity('PRODUCT-AAA'),
|
||||||
|
createUsersEntity('PRODUCT-BBB'),
|
||||||
|
createUsersEntity('PRODUCT-CCC'),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
...initialUsersState,
|
||||||
|
selectedId: 'PRODUCT-BBB',
|
||||||
|
error: ERROR_MSG,
|
||||||
|
loaded: true,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Users Selectors', () => {
|
||||||
|
it('selectAllUsers() should return the list of Users', () => {
|
||||||
|
const results = UsersSelectors.selectAllUsers(state);
|
||||||
|
const selId = getUsersId(results[1]);
|
||||||
|
|
||||||
|
expect(results.length).toBe(3);
|
||||||
|
expect(selId).toBe('PRODUCT-BBB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selectEntity() should return the selected Entity', () => {
|
||||||
|
const result = UsersSelectors.selectEntity(state) as UsersEntity;
|
||||||
|
const selId = getUsersId(result);
|
||||||
|
|
||||||
|
expect(selId).toBe('PRODUCT-BBB');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selectUsersLoaded() should return the current "loaded" status', () => {
|
||||||
|
const result = UsersSelectors.selectUsersLoaded(state);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('selectUsersError() should return the current "error" state', () => {
|
||||||
|
const result = UsersSelectors.selectUsersError(state);
|
||||||
|
|
||||||
|
expect(result).toBe(ERROR_MSG);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`NgRxRootStoreGenerator Standalone APIs should add an empty root module when --minimal=true 1`] = `
|
||||||
|
"import { ApplicationConfig } from '@angular/core';
|
||||||
|
import {
|
||||||
|
provideRouter,
|
||||||
|
withEnabledBlockingInitialNavigation,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
import { provideStore } from '@ngrx/store';
|
||||||
|
import { provideEffects } from '@ngrx/effects';
|
||||||
|
|
||||||
|
export const appConfig: ApplicationConfig = {
|
||||||
|
providers: [
|
||||||
|
provideEffects(),
|
||||||
|
provideStore(),
|
||||||
|
provideRouter(appRoutes, withEnabledBlockingInitialNavigation()),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
"
|
||||||
|
`;
|
||||||
@ -0,0 +1,167 @@
|
|||||||
|
import type { Tree } from '@nx/devkit';
|
||||||
|
import type { SourceFile } from 'typescript';
|
||||||
|
import {
|
||||||
|
addImportToModule,
|
||||||
|
addProviderToAppConfig,
|
||||||
|
addProviderToBootstrapApplication,
|
||||||
|
} from '../../../utils/nx-devkit/ast-utils';
|
||||||
|
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
|
||||||
|
import { insertImport } from '@nx/js';
|
||||||
|
import { NormalizedNgRxRootStoreGeneratorOptions } from './normalize-options';
|
||||||
|
|
||||||
|
let tsModule: typeof import('typescript');
|
||||||
|
|
||||||
|
function addRootStoreImport(
|
||||||
|
tree: Tree,
|
||||||
|
isParentStandalone: boolean,
|
||||||
|
sourceFile: SourceFile,
|
||||||
|
parentPath: string,
|
||||||
|
provideRootStore: string,
|
||||||
|
storeForRoot: string
|
||||||
|
) {
|
||||||
|
if (isParentStandalone) {
|
||||||
|
if (tree.read(parentPath, 'utf-8').includes('ApplicationConfig')) {
|
||||||
|
addProviderToAppConfig(tree, parentPath, provideRootStore);
|
||||||
|
} else {
|
||||||
|
addProviderToBootstrapApplication(tree, parentPath, provideRootStore);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sourceFile = addImportToModule(tree, sourceFile, parentPath, storeForRoot);
|
||||||
|
}
|
||||||
|
return sourceFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRootEffectsImport(
|
||||||
|
tree: Tree,
|
||||||
|
isParentStandalone: boolean,
|
||||||
|
sourceFile: SourceFile,
|
||||||
|
parentPath: string,
|
||||||
|
provideRootEffects: string,
|
||||||
|
effectsForEmptyRoot: string
|
||||||
|
) {
|
||||||
|
if (isParentStandalone) {
|
||||||
|
if (tree.read(parentPath, 'utf-8').includes('ApplicationConfig')) {
|
||||||
|
addProviderToAppConfig(tree, parentPath, provideRootEffects);
|
||||||
|
} else {
|
||||||
|
addProviderToBootstrapApplication(tree, parentPath, provideRootEffects);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sourceFile = addImportToModule(
|
||||||
|
tree,
|
||||||
|
sourceFile,
|
||||||
|
parentPath,
|
||||||
|
effectsForEmptyRoot
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sourceFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRouterStoreImport(
|
||||||
|
tree: Tree,
|
||||||
|
sourceFile: SourceFile,
|
||||||
|
addImport: (
|
||||||
|
source: SourceFile,
|
||||||
|
symbolName: string,
|
||||||
|
fileName: string,
|
||||||
|
isDefault?: boolean
|
||||||
|
) => SourceFile,
|
||||||
|
parentPath: string,
|
||||||
|
storeRouterModule: string
|
||||||
|
) {
|
||||||
|
sourceFile = addImport(
|
||||||
|
sourceFile,
|
||||||
|
'StoreRouterConnectingModule',
|
||||||
|
'@ngrx/router-store'
|
||||||
|
);
|
||||||
|
return addImportToModule(tree, sourceFile, parentPath, storeRouterModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addImportsToModule(
|
||||||
|
tree: Tree,
|
||||||
|
options: NormalizedNgRxRootStoreGeneratorOptions
|
||||||
|
): void {
|
||||||
|
if (!tsModule) {
|
||||||
|
tsModule = ensureTypescript();
|
||||||
|
}
|
||||||
|
const parentPath = options.parent;
|
||||||
|
const sourceText = tree.read(parentPath, 'utf-8');
|
||||||
|
let sourceFile = tsModule.createSourceFile(
|
||||||
|
parentPath,
|
||||||
|
sourceText,
|
||||||
|
tsModule.ScriptTarget.Latest,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const isParentStandalone = !sourceText.includes('@NgModule');
|
||||||
|
|
||||||
|
const addImport = (
|
||||||
|
source: SourceFile,
|
||||||
|
symbolName: string,
|
||||||
|
fileName: string,
|
||||||
|
isDefault = false
|
||||||
|
): SourceFile => {
|
||||||
|
return insertImport(
|
||||||
|
tree,
|
||||||
|
source,
|
||||||
|
parentPath,
|
||||||
|
symbolName,
|
||||||
|
fileName,
|
||||||
|
isDefault
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeMetaReducers = `metaReducers: []`;
|
||||||
|
|
||||||
|
const storeForRoot = `StoreModule.forRoot({}, {
|
||||||
|
${storeMetaReducers},
|
||||||
|
runtimeChecks: {
|
||||||
|
strictActionImmutability: true,
|
||||||
|
strictStateImmutability: true
|
||||||
|
}
|
||||||
|
})`;
|
||||||
|
const effectsForEmptyRoot = `EffectsModule.forRoot([])`;
|
||||||
|
const storeRouterModule = 'StoreRouterConnectingModule.forRoot()';
|
||||||
|
|
||||||
|
const provideRootStore = `provideStore()`;
|
||||||
|
const provideRootEffects = `provideEffects()`;
|
||||||
|
|
||||||
|
if (isParentStandalone) {
|
||||||
|
sourceFile = addImport(sourceFile, 'provideStore', '@ngrx/store');
|
||||||
|
sourceFile = addImport(sourceFile, 'provideEffects', '@ngrx/effects');
|
||||||
|
} else {
|
||||||
|
sourceFile = addImport(sourceFile, 'StoreModule', '@ngrx/store');
|
||||||
|
sourceFile = addImport(sourceFile, 'EffectsModule', '@ngrx/effects');
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceFile = addRootStoreImport(
|
||||||
|
tree,
|
||||||
|
isParentStandalone,
|
||||||
|
sourceFile,
|
||||||
|
parentPath,
|
||||||
|
provideRootStore,
|
||||||
|
storeForRoot
|
||||||
|
);
|
||||||
|
|
||||||
|
sourceFile = addRootEffectsImport(
|
||||||
|
tree,
|
||||||
|
isParentStandalone,
|
||||||
|
sourceFile,
|
||||||
|
parentPath,
|
||||||
|
provideRootEffects,
|
||||||
|
effectsForEmptyRoot
|
||||||
|
);
|
||||||
|
|
||||||
|
// this is just a heuristic
|
||||||
|
const hasRouter = sourceText.indexOf('RouterModule') > -1;
|
||||||
|
|
||||||
|
if (hasRouter && !isParentStandalone) {
|
||||||
|
sourceFile = addRouterStoreImport(
|
||||||
|
tree,
|
||||||
|
sourceFile,
|
||||||
|
addImport,
|
||||||
|
parentPath,
|
||||||
|
storeRouterModule
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import type { GeneratorCallback, Tree } from '@nx/devkit';
|
||||||
|
import { addDependenciesToPackageJson } from '@nx/devkit';
|
||||||
|
import { gte } from 'semver';
|
||||||
|
import { versions } from '../../utils/version-utils';
|
||||||
|
import { NormalizedNgRxRootStoreGeneratorOptions } from './normalize-options';
|
||||||
|
|
||||||
|
export function addNgRxToPackageJson(
|
||||||
|
tree: Tree,
|
||||||
|
options: NormalizedNgRxRootStoreGeneratorOptions
|
||||||
|
): GeneratorCallback {
|
||||||
|
const jasmineMarblesVersion = gte(options.rxjsVersion, '7.0.0')
|
||||||
|
? '~0.9.1'
|
||||||
|
: '~0.8.3';
|
||||||
|
const ngrxVersion = versions(tree).ngrxVersion;
|
||||||
|
|
||||||
|
process.env.npm_config_legacy_peer_deps ??= 'true';
|
||||||
|
|
||||||
|
return addDependenciesToPackageJson(
|
||||||
|
tree,
|
||||||
|
{
|
||||||
|
'@ngrx/store': ngrxVersion,
|
||||||
|
'@ngrx/effects': ngrxVersion,
|
||||||
|
'@ngrx/entity': ngrxVersion,
|
||||||
|
'@ngrx/router-store': ngrxVersion,
|
||||||
|
'@ngrx/component-store': ngrxVersion,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@ngrx/schematics': ngrxVersion,
|
||||||
|
'@ngrx/store-devtools': ngrxVersion,
|
||||||
|
'jasmine-marbles': jasmineMarblesVersion,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
export * from './add-imports';
|
||||||
|
export * from './add-ngrx-to-package-json';
|
||||||
|
export * from './normalize-options';
|
||||||
|
export * from './validate-options';
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
import type { Tree } from '@nx/devkit';
|
||||||
|
import {
|
||||||
|
joinPathFragments,
|
||||||
|
names,
|
||||||
|
readJson,
|
||||||
|
readProjectConfiguration,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { checkAndCleanWithSemver } from '@nx/devkit/src/utils/semver';
|
||||||
|
import { rxjsVersion as defaultRxjsVersion } from '../../../utils/versions';
|
||||||
|
import type { Schema } from '../schema';
|
||||||
|
import { isNgStandaloneApp } from '../../../utils/nx-devkit/ast-utils';
|
||||||
|
|
||||||
|
export type NormalizedNgRxRootStoreGeneratorOptions = Schema & {
|
||||||
|
parent: string;
|
||||||
|
rxjsVersion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeOptions(
|
||||||
|
tree: Tree,
|
||||||
|
options: Schema
|
||||||
|
): NormalizedNgRxRootStoreGeneratorOptions {
|
||||||
|
let rxjsVersion: string;
|
||||||
|
try {
|
||||||
|
rxjsVersion = checkAndCleanWithSemver(
|
||||||
|
'rxjs',
|
||||||
|
readJson(tree, 'package.json').dependencies['rxjs']
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
rxjsVersion = checkAndCleanWithSemver('rxjs', defaultRxjsVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, options.project);
|
||||||
|
const isStandalone = isNgStandaloneApp(tree, options.project);
|
||||||
|
const appConfigPath = joinPathFragments(
|
||||||
|
project.sourceRoot,
|
||||||
|
'app/app.config.ts'
|
||||||
|
);
|
||||||
|
const appMainPath = project.targets.build.options.main;
|
||||||
|
|
||||||
|
/** If NgModule App
|
||||||
|
* -> Use App Module
|
||||||
|
* If Standalone
|
||||||
|
* -> Check Config File exists (v16+)
|
||||||
|
* --> If so, use that
|
||||||
|
* --> If not, use main.ts
|
||||||
|
*/
|
||||||
|
const parent = !isStandalone
|
||||||
|
? joinPathFragments(project.sourceRoot, 'app/app.module.ts')
|
||||||
|
: tree.exists(appConfigPath)
|
||||||
|
? appConfigPath
|
||||||
|
: appMainPath;
|
||||||
|
|
||||||
|
options.directory = options.directory ?? '+state';
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
parent,
|
||||||
|
directory: names(options.directory).fileName,
|
||||||
|
rxjsVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
import type { Tree } from '@nx/devkit';
|
||||||
|
import { getProjects, readProjectConfiguration } from '@nx/devkit';
|
||||||
|
import { Schema } from '../schema';
|
||||||
|
import {
|
||||||
|
getInstalledAngularVersionInfo,
|
||||||
|
getInstalledPackageVersionInfo,
|
||||||
|
} from '../../utils/version-utils';
|
||||||
|
import { getPkgVersionForAngularMajorVersion } from '../../../utils/version-utils';
|
||||||
|
import { coerce, lt, major } from 'semver';
|
||||||
|
import { isNgStandaloneApp } from '../../../utils/nx-devkit/ast-utils';
|
||||||
|
|
||||||
|
export function validateOptions(tree: Tree, options: Schema): void {
|
||||||
|
if (!getProjects(tree).has(options.project)) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find project '${options.project}'. Please ensure the project name is correct and exists.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = readProjectConfiguration(tree, options.project);
|
||||||
|
if (project.projectType !== 'application') {
|
||||||
|
throw new Error(
|
||||||
|
`NgRx Root Stores can only be added to applications, please ensure the project you use is an application.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.minimal && !options.name) {
|
||||||
|
throw new Error(
|
||||||
|
`If generating a global feature state with your root store, you must provide a name for it with '--name'.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const angularVersionInfo = getInstalledAngularVersionInfo(tree);
|
||||||
|
const intendedNgRxVersionForAngularMajor =
|
||||||
|
getPkgVersionForAngularMajorVersion(
|
||||||
|
'ngrxVersion',
|
||||||
|
angularVersionInfo.major
|
||||||
|
);
|
||||||
|
|
||||||
|
const ngrxMajorVersion =
|
||||||
|
getInstalledPackageVersionInfo(tree, '@ngrx/store')?.major ??
|
||||||
|
major(coerce(intendedNgRxVersionForAngularMajor));
|
||||||
|
|
||||||
|
const isStandalone = isNgStandaloneApp(tree, options.project);
|
||||||
|
|
||||||
|
if (lt(angularVersionInfo.version, '14.1.0') || ngrxMajorVersion < 15) {
|
||||||
|
if (isStandalone) {
|
||||||
|
throw new Error(
|
||||||
|
`The provided project '${options.project}' is set up to use Standalone APIs, however your workspace is not configured to support Standalone APIs. ` +
|
||||||
|
'Please make sure to provide a path to an "NgModule" where the state will be registered. '
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,381 @@
|
|||||||
|
import type { Tree } from '@nx/devkit';
|
||||||
|
import { readJson } from '@nx/devkit';
|
||||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||||
|
import applicationGenerator from '../application/application';
|
||||||
|
import ngrxRootStoreGenerator from './ngrx-root-store';
|
||||||
|
import { ngrxVersion } from '../../utils/versions';
|
||||||
|
|
||||||
|
describe('NgRxRootStoreGenerator', () => {
|
||||||
|
describe('NgModule', () => {
|
||||||
|
it('should error when project does not exist', async () => {
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'non-exist',
|
||||||
|
minimal: true,
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
).rejects.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when minimal false, but name is undefined or falsy', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createNgModuleApp(tree);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
await expect(
|
||||||
|
ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: false,
|
||||||
|
name: undefined,
|
||||||
|
})
|
||||||
|
).rejects.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an empty root module when --minimal=true', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createNgModuleApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(
|
||||||
|
tree.read('my-app/src/app/app.module.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.actions.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.effects.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.effects.spec.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.reducer.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.selectors.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tree.exists('/my-app/src/app/+state/users.selectors.spec.ts')
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a root module and root state when --minimal=false', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createNgModuleApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: false,
|
||||||
|
name: 'users',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(
|
||||||
|
tree.read('my-app/src/app/app.module.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.actions.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.effects.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.effects.spec.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.reducer.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.selectors.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.selectors.spec.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a facade when --facade=true', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createNgModuleApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: false,
|
||||||
|
name: 'users',
|
||||||
|
facade: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(
|
||||||
|
tree.read('my-app/src/app/app.module.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.facade.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update package.json', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createNgModuleApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
const packageJson = readJson(tree, 'package.json');
|
||||||
|
expect(packageJson.dependencies['@ngrx/store']).toEqual(ngrxVersion);
|
||||||
|
expect(packageJson.dependencies['@ngrx/effects']).toEqual(ngrxVersion);
|
||||||
|
expect(packageJson.dependencies['@ngrx/entity']).toEqual(ngrxVersion);
|
||||||
|
expect(packageJson.dependencies['@ngrx/router-store']).toEqual(
|
||||||
|
ngrxVersion
|
||||||
|
);
|
||||||
|
expect(packageJson.dependencies['@ngrx/component-store']).toEqual(
|
||||||
|
ngrxVersion
|
||||||
|
);
|
||||||
|
expect(packageJson.devDependencies['@ngrx/schematics']).toEqual(
|
||||||
|
ngrxVersion
|
||||||
|
);
|
||||||
|
expect(packageJson.devDependencies['@ngrx/store-devtools']).toEqual(
|
||||||
|
ngrxVersion
|
||||||
|
);
|
||||||
|
expect(packageJson.devDependencies['jasmine-marbles']).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update package.json when --skipPackageJson=true', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createNgModuleApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: true,
|
||||||
|
skipPackageJson: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
const packageJson = readJson(tree, 'package.json');
|
||||||
|
expect(packageJson.dependencies['@ngrx/store']).toBeUndefined();
|
||||||
|
expect(packageJson.dependencies['@ngrx/effects']).toBeUndefined();
|
||||||
|
expect(packageJson.dependencies['@ngrx/entity']).toBeUndefined();
|
||||||
|
expect(packageJson.dependencies['@ngrx/router-store']).toBeUndefined();
|
||||||
|
expect(packageJson.dependencies['@ngrx/component-store']).toBeUndefined();
|
||||||
|
expect(packageJson.devDependencies['@ngrx/schematics']).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
packageJson.devDependencies['@ngrx/store-devtools']
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(packageJson.devDependencies['jasmine-marbles']).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Standalone APIs', () => {
|
||||||
|
it('should error when project does not exist', async () => {
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'non-exist',
|
||||||
|
minimal: true,
|
||||||
|
name: '',
|
||||||
|
})
|
||||||
|
).rejects.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should error when minimal false, but name is undefined or falsy', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createStandaloneApp(tree);
|
||||||
|
|
||||||
|
// ACT & ASSERT
|
||||||
|
await expect(
|
||||||
|
ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: false,
|
||||||
|
name: undefined,
|
||||||
|
})
|
||||||
|
).rejects.toThrowError();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an empty root module when --minimal=true', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createStandaloneApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(
|
||||||
|
tree.read('my-app/src/app/app.config.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.actions.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.effects.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.effects.spec.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.reducer.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(tree.exists('/my-app/src/app/+state/users.selectors.ts')).toBe(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tree.exists('/my-app/src/app/+state/users.selectors.spec.ts')
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a root module and root state when --minimal=false', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createStandaloneApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: false,
|
||||||
|
name: 'users',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(
|
||||||
|
tree.read('my-app/src/app/app.config.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.actions.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.effects.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.effects.spec.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.reducer.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.selectors.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.selectors.spec.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a facade when --facade=true', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createStandaloneApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: false,
|
||||||
|
name: 'users',
|
||||||
|
facade: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
expect(
|
||||||
|
tree.read('my-app/src/app/app.config.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
expect(
|
||||||
|
tree.read('/my-app/src/app/+state/users.facade.ts', 'utf-8')
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update package.json', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createStandaloneApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
const packageJson = readJson(tree, 'package.json');
|
||||||
|
expect(packageJson.dependencies['@ngrx/store']).toEqual(ngrxVersion);
|
||||||
|
expect(packageJson.dependencies['@ngrx/effects']).toEqual(ngrxVersion);
|
||||||
|
expect(packageJson.dependencies['@ngrx/entity']).toEqual(ngrxVersion);
|
||||||
|
expect(packageJson.dependencies['@ngrx/router-store']).toEqual(
|
||||||
|
ngrxVersion
|
||||||
|
);
|
||||||
|
expect(packageJson.dependencies['@ngrx/component-store']).toEqual(
|
||||||
|
ngrxVersion
|
||||||
|
);
|
||||||
|
expect(packageJson.devDependencies['@ngrx/schematics']).toEqual(
|
||||||
|
ngrxVersion
|
||||||
|
);
|
||||||
|
expect(packageJson.devDependencies['@ngrx/store-devtools']).toEqual(
|
||||||
|
ngrxVersion
|
||||||
|
);
|
||||||
|
expect(packageJson.devDependencies['jasmine-marbles']).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update package.json when --skipPackageJson=true', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
const tree = createTreeWithEmptyWorkspace();
|
||||||
|
await createStandaloneApp(tree);
|
||||||
|
|
||||||
|
// ACT
|
||||||
|
await ngrxRootStoreGenerator(tree, {
|
||||||
|
project: 'my-app',
|
||||||
|
minimal: true,
|
||||||
|
skipPackageJson: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ASSERT
|
||||||
|
const packageJson = readJson(tree, 'package.json');
|
||||||
|
expect(packageJson.dependencies['@ngrx/store']).toBeUndefined();
|
||||||
|
expect(packageJson.dependencies['@ngrx/effects']).toBeUndefined();
|
||||||
|
expect(packageJson.dependencies['@ngrx/entity']).toBeUndefined();
|
||||||
|
expect(packageJson.dependencies['@ngrx/router-store']).toBeUndefined();
|
||||||
|
expect(packageJson.dependencies['@ngrx/component-store']).toBeUndefined();
|
||||||
|
expect(packageJson.devDependencies['@ngrx/schematics']).toBeUndefined();
|
||||||
|
expect(
|
||||||
|
packageJson.devDependencies['@ngrx/store-devtools']
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(packageJson.devDependencies['jasmine-marbles']).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createNgModuleApp(tree: Tree, name = 'my-app') {
|
||||||
|
await applicationGenerator(tree, {
|
||||||
|
name,
|
||||||
|
standalone: false,
|
||||||
|
routing: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createStandaloneApp(tree: Tree, name = 'my-app') {
|
||||||
|
await applicationGenerator(tree, {
|
||||||
|
name,
|
||||||
|
standalone: true,
|
||||||
|
routing: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import type { Tree } from '@nx/devkit';
|
||||||
|
import { formatFiles, GeneratorCallback } from '@nx/devkit';
|
||||||
|
import type { Schema } from './schema';
|
||||||
|
|
||||||
|
import {
|
||||||
|
addImportsToModule,
|
||||||
|
addNgRxToPackageJson,
|
||||||
|
normalizeOptions,
|
||||||
|
validateOptions,
|
||||||
|
} from './lib';
|
||||||
|
|
||||||
|
import ngrxFeatureStoreGenerator from '../ngrx-feature-store/ngrx-feature-store';
|
||||||
|
|
||||||
|
export async function ngrxRootStoreGenerator(tree: Tree, schema: Schema) {
|
||||||
|
validateOptions(tree, schema);
|
||||||
|
const options = normalizeOptions(tree, schema);
|
||||||
|
|
||||||
|
if (!options.skipImport) {
|
||||||
|
addImportsToModule(tree, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.minimal) {
|
||||||
|
await ngrxFeatureStoreGenerator(tree, {
|
||||||
|
name: options.name,
|
||||||
|
parent: options.parent,
|
||||||
|
directory: options.directory,
|
||||||
|
minimal: false,
|
||||||
|
facade: options.facade,
|
||||||
|
barrels: false,
|
||||||
|
skipImport: false,
|
||||||
|
skipPackageJson: true,
|
||||||
|
skipFormat: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let packageInstallationTask: GeneratorCallback = () => {};
|
||||||
|
if (!options.skipPackageJson) {
|
||||||
|
packageInstallationTask = addNgRxToPackageJson(tree, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.skipFormat) {
|
||||||
|
await formatFiles(tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageInstallationTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ngrxRootStoreGenerator;
|
||||||
10
packages/angular/src/generators/ngrx-root-store/schema.d.ts
vendored
Normal file
10
packages/angular/src/generators/ngrx-root-store/schema.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface Schema {
|
||||||
|
project: string;
|
||||||
|
minimal: boolean;
|
||||||
|
name?: string;
|
||||||
|
directory?: string;
|
||||||
|
facade?: boolean;
|
||||||
|
skipFormat?: boolean;
|
||||||
|
skipImport?: boolean;
|
||||||
|
skipPackageJson?: boolean;
|
||||||
|
}
|
||||||
66
packages/angular/src/generators/ngrx-root-store/schema.json
Normal file
66
packages/angular/src/generators/ngrx-root-store/schema.json
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/schema",
|
||||||
|
"$id": "NxNgrxRootStoreGenerator",
|
||||||
|
"title": "Add NgRx support to an application.",
|
||||||
|
"description": "Adds NgRx support to an application.",
|
||||||
|
"cli": "nx",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the application to generate the NgRx configuration for.",
|
||||||
|
"$default": {
|
||||||
|
"$source": "argv",
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
"x-prompt": "What app would you like to generate a NgRx configuration for?",
|
||||||
|
"x-dropdown": "projects"
|
||||||
|
},
|
||||||
|
"minimal": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Only register the root state management setup or also generate a global feature state.",
|
||||||
|
"x-priority": "important"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the NgRx state, such as `products` or `users`. Recommended to use the plural form of the name.",
|
||||||
|
"x-priority": "important"
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The route that the Standalone NgRx Providers should be added to. _Note: This is only supported in Angular versions >= 14.1.0_.",
|
||||||
|
"default": "''"
|
||||||
|
},
|
||||||
|
"directory": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "+state",
|
||||||
|
"description": "The name of the folder used to contain/group the generated NgRx files."
|
||||||
|
},
|
||||||
|
"facade": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Create a Facade class for the the feature.",
|
||||||
|
"x-prompt": "Would you like to use a Facade with your NgRx state?"
|
||||||
|
},
|
||||||
|
"skipImport": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Generate NgRx feature files without registering the feature in the NgModule."
|
||||||
|
},
|
||||||
|
"skipFormat": {
|
||||||
|
"description": "Skip formatting files.",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"x-priority": "internal"
|
||||||
|
},
|
||||||
|
"skipPackageJson": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Do not update the `package.json` with NgRx dependencies.",
|
||||||
|
"x-priority": "internal"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["project"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user