feat(angular): add ngrx root store generator (#16811)
This commit is contained in:
parent
279154436a
commit
e59c930ff9
@ -4246,6 +4246,14 @@
|
||||
"isExternal": 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",
|
||||
"path": "/packages/angular/generators/pipe",
|
||||
|
||||
@ -267,6 +267,15 @@
|
||||
"path": "/packages/angular/generators/ngrx-feature-store",
|
||||
"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": {
|
||||
"description": "Generate an Angular Pipe",
|
||||
"file": "generated/packages/angular/generators/pipe.json",
|
||||
|
||||
@ -262,6 +262,15 @@
|
||||
"path": "angular/generators/ngrx-feature-store",
|
||||
"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",
|
||||
"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",
|
||||
"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": {
|
||||
"factory": "./src/generators/pipe/pipe",
|
||||
"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/ngrx/ngrx';
|
||||
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/remote/remote';
|
||||
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