cleanup(angular): migrate ngrx generator to nx devkit (#6057)

This commit is contained in:
Leosvel Pérez Espinosa 2021-06-21 14:18:15 +01:00 committed by GitHub
parent 8915c6ba4e
commit 5cac8ba9ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 2632 additions and 1330 deletions

View File

@ -1,6 +1,6 @@
# ngrx
Add an ngrx config to a project
Add NgRx support to an application or library.
## Usage
@ -66,23 +66,27 @@ The path to NgModule where the feature state will be registered. The host direct
Type: `string`
Name of the NgRx feature state, such as "products" or "users"). Recommended to use the plural form of the name.
Name of the NgRx feature state, such as `products` or `users`. Recommended to use the plural form of the name.
### onlyAddFiles
### ~~onlyAddFiles~~
Default: `false`
Type: `boolean`
**Deprecated**, use `skipImport`. Only add new NgRx files, without changing the module file (e.g., --onlyAddFiles).
**Deprecated:** Use the `skipImport` option instead.
### onlyEmptyRoot
Only add new NgRx files, without changing the module file (e.g., --onlyAddFiles).
### ~~onlyEmptyRoot~~
Default: `false`
Type: `boolean`
**Deprecated**, use `minimal`. Do not generate any files. Only generate StoreModule.forRoot and EffectsModule.forRoot (e.g., --onlyEmptyRoot).
**Deprecated:** Use the `minimal` option instead.
Do not generate any files. Only generate StoreModule.forRoot and EffectsModule.forRoot (e.g., --onlyEmptyRoot).
### root

View File

@ -1,6 +1,6 @@
# ngrx
Add an ngrx config to a project
Add NgRx support to an application or library.
## Usage
@ -66,23 +66,27 @@ The path to NgModule where the feature state will be registered. The host direct
Type: `string`
Name of the NgRx feature state, such as "products" or "users"). Recommended to use the plural form of the name.
Name of the NgRx feature state, such as `products` or `users`. Recommended to use the plural form of the name.
### onlyAddFiles
### ~~onlyAddFiles~~
Default: `false`
Type: `boolean`
**Deprecated**, use `skipImport`. Only add new NgRx files, without changing the module file (e.g., --onlyAddFiles).
**Deprecated:** Use the `skipImport` option instead.
### onlyEmptyRoot
Only add new NgRx files, without changing the module file (e.g., --onlyAddFiles).
### ~~onlyEmptyRoot~~
Default: `false`
Type: `boolean`
**Deprecated**, use `minimal`. Do not generate any files. Only generate StoreModule.forRoot and EffectsModule.forRoot (e.g., --onlyEmptyRoot).
**Deprecated:** Use the `minimal` option instead.
Do not generate any files. Only generate StoreModule.forRoot and EffectsModule.forRoot (e.g., --onlyEmptyRoot).
### root

View File

@ -1,6 +1,6 @@
# ngrx
Add an ngrx config to a project
Add NgRx support to an application or library.
## Usage
@ -66,23 +66,27 @@ The path to NgModule where the feature state will be registered. The host direct
Type: `string`
Name of the NgRx feature state, such as "products" or "users"). Recommended to use the plural form of the name.
Name of the NgRx feature state, such as `products` or `users`. Recommended to use the plural form of the name.
### onlyAddFiles
### ~~onlyAddFiles~~
Default: `false`
Type: `boolean`
**Deprecated**, use `skipImport`. Only add new NgRx files, without changing the module file (e.g., --onlyAddFiles).
**Deprecated:** Use the `skipImport` option instead.
### onlyEmptyRoot
Only add new NgRx files, without changing the module file (e.g., --onlyAddFiles).
### ~~onlyEmptyRoot~~
Default: `false`
Type: `boolean`
**Deprecated**, use `minimal`. Do not generate any files. Only generate StoreModule.forRoot and EffectsModule.forRoot (e.g., --onlyEmptyRoot).
**Deprecated:** Use the `minimal` option instead.
Do not generate any files. Only generate StoreModule.forRoot and EffectsModule.forRoot (e.g., --onlyEmptyRoot).
### root

View File

@ -38,9 +38,9 @@
},
"ngrx": {
"factory": "./src/schematics/ngrx/ngrx",
"schema": "./src/schematics/ngrx/schema.json",
"description": "Add an ngrx config to a project"
"factory": "./src/generators/ngrx/compat",
"schema": "./src/generators/ngrx/schema.json",
"description": "Add NgRx support to an application or library."
},
"downgrade-module": {
@ -166,6 +166,11 @@
"aliases": ["mv"],
"description": "Move an Angular application or library to another folder."
},
"ngrx": {
"factory": "./src/generators/ngrx/ngrx",
"schema": "./src/generators/ngrx/schema.json",
"description": "Add NgRx support to an application or library."
},
"stories": {
"factory": "./src/generators/stories/stories",
"schema": "./src/generators/stories/schema.json",

View File

@ -1,11 +1,11 @@
export * from './src/generators/application/application';
export * from './src/generators/convert-tslint-to-eslint/convert-tslint-to-eslint';
export * from './src/generators/karma/karma';
export * from './src/generators/karma-project/karma-project';
export * from './src/generators/library/library';
export * from './src/generators/move/move';
export * from './src/generators/ngrx/ngrx';
export * from './src/generators/stories/stories';
export * from './src/generators/application/application';
export * from './src/generators/storybook-configuration/storybook-configuration';
export * from './src/generators/storybook-migrate-defaults-5-to-6/storybook-migrate-defaults-5-to-6';
export * from './src/generators/storybook-migrate-stories-to-6-2/migrate-stories-to-6-2';
export * from './src/schematics/generators';

View File

@ -0,0 +1,537 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NgRx generator classes syntax should generate the ngrx actions 1`] = `
"import {Action} from '@ngrx/store';
import {Entity} from './users.reducer';
export enum UsersActionTypes {
LoadUsers = '[Users] Load Users',
UsersLoaded = '[Users] Users Loaded',
UsersLoadError = '[Users] Users Load Error'
}
export class LoadUsers implements Action {
readonly type = UsersActionTypes.LoadUsers;
}
export class UsersLoadError implements Action {
readonly type = UsersActionTypes.UsersLoadError;
constructor(public payload: any) {}
}
export class UsersLoaded implements Action {
readonly type = UsersActionTypes.UsersLoaded;
constructor(public payload: Entity[]) {}
}
export type UsersAction = LoadUsers | UsersLoaded | UsersLoadError;
export const fromUsersActions = {
LoadUsers,
UsersLoaded,
UsersLoadError
};
"
`;
exports[`NgRx generator classes syntax should generate the ngrx effects 1`] = `
"import { Injectable } from '@angular/core';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { fetch } from '@nrwl/angular';
import { LoadUsers, UsersLoaded, UsersLoadError, UsersActionTypes } from './users.actions';
import { UsersPartialState } from './users.reducer';
@Injectable()
export class UsersEffects {
@Effect() loadUsers$ = this.actions$.pipe(
ofType(UsersActionTypes.LoadUsers),
fetch({
run: (action: LoadUsers, state: UsersPartialState) => {
// Your custom REST 'load' logic goes here. For now just return an empty list...
return new UsersLoaded([]);
},
onError: (action: LoadUsers, error) => {
console.error('Error', error);
return new UsersLoadError(error);
}
})
);
constructor(
private readonly actions$: Actions
) {}
}
"
`;
exports[`NgRx generator classes syntax should generate the ngrx facade 1`] = `
"import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { UsersPartialState } from './users.reducer';
import { usersQuery } from './users.selectors';
import { LoadUsers } from './users.actions';
@Injectable()
export class UsersFacade {
loaded$ = this.store.pipe(select(usersQuery.getLoaded));
allUsers$ = this.store.pipe(select(usersQuery.getAllUsers));
selectedUsers$ = this.store.pipe(select(usersQuery.getSelectedUsers));
constructor(private readonly store: Store<UsersPartialState>) {}
loadAll() {
this.store.dispatch(new LoadUsers());
}
}
"
`;
exports[`NgRx generator classes syntax should generate the ngrx reducer 1`] = `
"import { UsersAction, UsersActionTypes } from './users.actions';
export const USERS_FEATURE_KEY = 'users';
/**
* Interface for the 'Users' data used in
* - UsersState, and the reducer function
*
* Note: replace if already defined in another module
*/
export interface Entity {
id: string;
name: string;
}
export interface UsersState {
list: Entity[]; // list of Users; analogous to a sql normalized table
selectedId?: string | number; // which Users record has been selected
loaded: boolean; // has the Users list been loaded
error?: any; // last none error (if any)
};
export interface UsersPartialState {
readonly [USERS_FEATURE_KEY]: UsersState;
}
export const initialState: UsersState = {
list: [],
loaded: false
};
export function reducer(
state: UsersState = initialState,
action: UsersAction): UsersState
{
switch (action.type) {
case UsersActionTypes.UsersLoaded: {
state = {
...state,
list: action.payload,
loaded: true
};
break;
}
}
return state;
}
"
`;
exports[`NgRx generator classes syntax should generate the ngrx selectors 1`] = `
"import { createFeatureSelector, createSelector } from '@ngrx/store';
import { USERS_FEATURE_KEY, UsersState } from './users.reducer';
// Lookup the 'Users' feature state managed by NgRx
const getUsersState = createFeatureSelector<UsersState>(USERS_FEATURE_KEY);
const getLoaded = createSelector( getUsersState, (state:UsersState) => state.loaded );
const getError = createSelector( getUsersState, (state:UsersState) => state.error );
const getAllUsers = createSelector( getUsersState, getLoaded, (state:UsersState, isLoaded) => {
return isLoaded ? state.list : [ ];
});
const getSelectedId = createSelector( getUsersState, (state:UsersState) => state.selectedId );
const getSelectedUsers = createSelector( getAllUsers, getSelectedId, (users, id) => {
const result = users.find(it => it['id'] === id);
return result ? Object.assign({}, result) : undefined;
});
export const usersQuery = {
getLoaded,
getError,
getAllUsers,
getSelectedUsers
};
"
`;
exports[`NgRx generator classes syntax should update the entry point file correctly when barrels is true 1`] = `
"import * as SuperUsersActions from './lib/+state/super-users.actions';
import * as SuperUsersFeature from './lib/+state/super-users.reducer';
import * as SuperUsersSelectors from './lib/+state/super-users.selectors';
export * from './lib/+state/super-users.facade';
export { SuperUsersActions, SuperUsersFeature, SuperUsersSelectors };
export * from './lib/flights.module';
"
`;
exports[`NgRx generator classes syntax should update the entry point file with no facade 1`] = `
"export * from './lib/+state/super-users.selectors';
export * from './lib/+state/super-users.reducer';
export * from './lib/+state/super-users.actions';
export * from './lib/flights.module';
"
`;
exports[`NgRx generator classes syntax should update the entry point file with the right exports 1`] = `
"export * from './lib/+state/super-users.facade';
export * from './lib/+state/super-users.selectors';
export * from './lib/+state/super-users.reducer';
export * from './lib/+state/super-users.actions';
export * from './lib/flights.module';
"
`;
exports[`NgRx generator classes syntax should use DataPersistence when useDataPersistence is true 1`] = `
"import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/angular';
import { LoadUsers, UsersLoaded, UsersLoadError, UsersActionTypes } from './users.actions';
import { UsersPartialState } from './users.reducer';
@Injectable()
export class UsersEffects {
@Effect() loadUsers$ = this.dataPersistence.fetch(UsersActionTypes.LoadUsers, {
run: (action: LoadUsers, state: UsersPartialState) => {
// Your custom REST 'load' logic goes here. For now just return an empty list...
return new UsersLoaded([]);
},
onError: (action: LoadUsers, error) => {
console.error('Error', error);
return new UsersLoadError(error);
}
});
constructor(
private readonly actions$: Actions,
private readonly dataPersistence: DataPersistence<UsersPartialState>
) {}
}
"
`;
exports[`NgRx generator classes syntax unit tests should generate specs for the ngrx effects 1`] = `
"import { TestBed } from '@angular/core/testing';
import { EffectsModule } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { Action, StoreModule } from '@ngrx/store';
import { NxModule } from '@nrwl/angular';
import { hot } from '@nrwl/angular/testing';
import { Observable } from 'rxjs';
import { SuperUsersEffects } from './super-users.effects';
import { LoadSuperUsers, SuperUsersLoaded } from './super-users.actions';
describe('SuperUsersEffects', () => {
let actions: Observable<Action>;
let effects: SuperUsersEffects;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
NxModule.forRoot(),
StoreModule.forRoot({}),
EffectsModule.forRoot([])
],
providers: [
SuperUsersEffects,
provideMockActions(() => actions)
],
});
effects = TestBed.inject(SuperUsersEffects);
});
describe('loadSuperUsers$', () => {
it('should work', () => {
actions = hot('-a-|', { a: new LoadSuperUsers() });
expect(effects.loadSuperUsers$).toBeObservable(
hot('-a-|', { a: new SuperUsersLoaded([]) })
);
});
});
});
"
`;
exports[`NgRx generator classes syntax unit tests should generate specs for the ngrx effects correctly when useDataPersistence is true 1`] = `
"import { TestBed } from '@angular/core/testing';
import { EffectsModule } from '@ngrx/effects';
import { provideMockActions } from '@ngrx/effects/testing';
import { Action, StoreModule } from '@ngrx/store';
import { NxModule, DataPersistence } from '@nrwl/angular';
import { hot } from '@nrwl/angular/testing';
import { Observable } from 'rxjs';
import { SuperUsersEffects } from './super-users.effects';
import { LoadSuperUsers, SuperUsersLoaded } from './super-users.actions';
describe('SuperUsersEffects', () => {
let actions: Observable<Action>;
let effects: SuperUsersEffects;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
NxModule.forRoot(),
StoreModule.forRoot({}),
EffectsModule.forRoot([])
],
providers: [
SuperUsersEffects,
DataPersistence,
provideMockActions(() => actions)
],
});
effects = TestBed.inject(SuperUsersEffects);
});
describe('loadSuperUsers$', () => {
it('should work', () => {
actions = hot('-a-|', { a: new LoadSuperUsers() });
expect(effects.loadSuperUsers$).toBeObservable(
hot('-a-|', { a: new SuperUsersLoaded([]) })
);
});
});
});
"
`;
exports[`NgRx generator classes syntax unit tests should generate specs for the ngrx facade 1`] = `
"import { NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule, Store } from '@ngrx/store';
import { NxModule } from '@nrwl/angular';
import { readFirst } from '@nrwl/angular/testing';
import { LoadSuperUsers, SuperUsersLoaded } from './super-users.actions';
import { SuperUsersEffects } from './super-users.effects';
import { SuperUsersFacade } from './super-users.facade';
import {
SuperUsersState,
Entity,
initialState,
reducer
} from './super-users.reducer';
import { superUsersQuery } from './super-users.selectors';
interface TestSchema {
superUsers: SuperUsersState;
}
describe('SuperUsersFacade', () => {
let facade: SuperUsersFacade;
let store: Store<TestSchema>;
const createSuperUsers = (id: string, name?: string): Entity => ({
id,
name: name || \`name-\${id}\`
});
describe('used in NgModule', () => {
beforeEach(() => {
@NgModule({
imports: [
StoreModule.forFeature('superUsers', reducer, { initialState }),
EffectsModule.forFeature([SuperUsersEffects])
],
providers: [SuperUsersFacade]
})
class CustomFeatureModule {}
@NgModule({
imports: [
NxModule.forRoot(),
StoreModule.forRoot({}),
EffectsModule.forRoot([]),
CustomFeatureModule,
]
})
class RootModule {}
TestBed.configureTestingModule({ imports: [RootModule] });
store = TestBed.inject(Store);
facade = TestBed.inject(SuperUsersFacade);
});
/**
* The initially generated facade::loadAll() returns empty array
*/
it('loadAll() should return empty list with loaded == true', async (done) => {
try {
let list = await readFirst(facade.allSuperUsers$);
let isLoaded = await readFirst(facade.loaded$);
expect(list.length).toBe(0);
expect(isLoaded).toBe(false);
facade.loadAll();
list = await readFirst(facade.allSuperUsers$);
isLoaded = await readFirst(facade.loaded$);
expect(list.length).toBe(0);
expect(isLoaded).toBe(true);
done();
} catch (err) {
done.fail(err);
}
});
/**
* Use \`SuperUsersLoaded\` to manually submit list for state management
*/
it('allSuperUsers$ should return the loaded list; and loaded flag == true', async (done) => {
try {
let list = await readFirst(facade.allSuperUsers$);
let isLoaded = await readFirst(facade.loaded$);
expect(list.length).toBe(0);
expect(isLoaded).toBe(false);
store.dispatch(new SuperUsersLoaded([
createSuperUsers('AAA'),
createSuperUsers('BBB')
]));
list = await readFirst(facade.allSuperUsers$);
isLoaded = await readFirst(facade.loaded$);
expect(list.length).toBe(2);
expect(isLoaded).toBe(true);
done();
} catch (err) {
done.fail(err);
}
});
});
});
"
`;
exports[`NgRx generator classes syntax unit tests should generate specs for the ngrx reducer 1`] = `
"import { SuperUsersLoaded } from './super-users.actions';
import { SuperUsersState, Entity, initialState, reducer } from './super-users.reducer';
describe('SuperUsers Reducer', () => {
const getSuperUsersId = (it: Entity) => it.id;
const createSuperUsers = (id: string, name = ''): Entity => ({
id,
name: name || \`name-\${id}\`
});
describe('valid SuperUsers actions', () => {
it('should return the list of known SuperUsers', () => {
const superUsers = [createSuperUsers('PRODUCT-AAA'), createSuperUsers('PRODUCT-zzz')];
const action = new SuperUsersLoaded(superUsers);
const result: SuperUsersState = reducer(initialState, action);
const selId: string = getSuperUsersId(result.list[1]);
expect(result.loaded).toBe(true);
expect(result.list.length).toBe(2);
expect(selId).toBe('PRODUCT-zzz');
});
});
describe('unknown action', () => {
it('should return the previous state', () => {
const action = {} as any;
const result = reducer(initialState, action);
expect(result).toBe(initialState);
});
});
});
"
`;
exports[`NgRx generator classes syntax unit tests should generate specs for the ngrx selectors 1`] = `
"import { Entity, SuperUsersPartialState } from './super-users.reducer';
import { superUsersQuery } from './super-users.selectors';
describe('SuperUsers Selectors', () => {
const ERROR_MSG = 'No Error Available';
const getSuperUsersId = (it: Entity) => it.id;
let storeState: SuperUsersPartialState;
beforeEach(() => {
const createSuperUsers = (id: string, name = ''): Entity => ({
id,
name: name || \`name-\${id}\`
});
storeState = {
superUsers: {
list : [
createSuperUsers('PRODUCT-AAA'),
createSuperUsers('PRODUCT-BBB'),
createSuperUsers('PRODUCT-CCC')
],
selectedId: 'PRODUCT-BBB',
error: ERROR_MSG,
loaded: true
}
};
});
describe('SuperUsers Selectors', () => {
it('getAllSuperUsers() should return the list of SuperUsers', () => {
const results = superUsersQuery.getAllSuperUsers(storeState);
const selId = getSuperUsersId(results[1]);
expect(results.length).toBe(3);
expect(selId).toBe('PRODUCT-BBB');
});
it('getSelectedSuperUsers() should return the selected Entity', () => {
const result = superUsersQuery.getSelectedSuperUsers(storeState) as Entity;
const selId = getSuperUsersId(result);
expect(selId).toBe('PRODUCT-BBB');
});
it('getLoaded() should return the current \\"loaded\\" status', () => {
const result = superUsersQuery.getLoaded(storeState);
expect(result).toBe(true);
});
it('getError() should return the current \\"error\\" storeState', () => {
const result = superUsersQuery.getError(storeState);
expect(result).toBe(ERROR_MSG);
});
});
});
"
`;

View File

@ -0,0 +1,556 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NgRx generator creators syntax should generate a models file for the feature 1`] = `
"/**
* Interface for the 'Users' data
*/
export interface UsersEntity {
id: string | number; // Primary ID
name: string;
};"
`;
exports[`NgRx generator creators syntax should generate the ngrx actions 1`] = `
"import { createAction, props } from '@ngrx/store';
import { UsersEntity } from './users.models';
export const init = 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[`NgRx generator creators syntax should generate the ngrx effects 1`] = `
"import { Injectable } from '@angular/core';
import { createEffect, Actions, ofType } from '@ngrx/effects';
import { fetch } from '@nrwl/angular';
import * as UsersActions from './users.actions';
import * as UsersFeature from './users.reducer';
@Injectable()
export class UsersEffects {
init$ = createEffect(() => this.actions$.pipe(
ofType(UsersActions.init),
fetch({
run: action => {
// Your custom service 'load' logic goes here. For now just return a success action...
return UsersActions.loadUsersSuccess({ users: [] });
},
onError: (action, error) => {
console.error('Error', error);
return UsersActions.loadUsersFailure({ error });
}
})
));
constructor(
private readonly actions$: Actions
) {}
}
"
`;
exports[`NgRx generator creators syntax should generate the ngrx facade 1`] = `
"import { Injectable } 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 {
/**
* Combine pieces of state using createSelector,
* and expose them as observables through the facade.
*/
loaded$ = this.store.pipe(select(UsersSelectors.getUsersLoaded));
allUsers$ = this.store.pipe(select(UsersSelectors.getAllUsers));
selectedUsers$ = this.store.pipe(select(UsersSelectors.getSelected));
constructor(private readonly store: Store) {}
/**
* Use the initialization action to perform one
* or more tasks in your Effects.
*/
init() {
this.store.dispatch(UsersActions.init());
}
}
"
`;
exports[`NgRx generator creators syntax should generate the ngrx reducer 1`] = `
"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 State 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]: State;
}
export const usersAdapter: EntityAdapter<UsersEntity> = createEntityAdapter<UsersEntity>();
export const initialState: State = usersAdapter.getInitialState({
// set initial required properties
loaded: false
});
const usersReducer = createReducer(
initialState,
on(UsersActions.init,
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 reducer(state: State | undefined, action: Action) {
return usersReducer(state, action);
}
"
`;
exports[`NgRx generator creators syntax should generate the ngrx selectors 1`] = `
"import { createFeatureSelector, createSelector } from '@ngrx/store';
import { USERS_FEATURE_KEY, State, usersAdapter } from './users.reducer';
// Lookup the 'Users' feature state managed by NgRx
export const getUsersState = createFeatureSelector<State>(USERS_FEATURE_KEY);
const { selectAll, selectEntities } = usersAdapter.getSelectors();
export const getUsersLoaded = createSelector(
getUsersState,
(state: State) => state.loaded
);
export const getUsersError = createSelector(
getUsersState,
(state: State) => state.error
);
export const getAllUsers = createSelector(
getUsersState,
(state: State) => selectAll(state)
);
export const getUsersEntities = createSelector(
getUsersState,
(state: State) => selectEntities(state)
);
export const getSelectedId = createSelector(
getUsersState,
(state: State) => state.selectedId
);
export const getSelected = createSelector(
getUsersEntities,
getSelectedId,
(entities, selectedId) => (selectedId ? entities[selectedId] : undefined)
);
"
`;
exports[`NgRx generator creators syntax should update the entry point file correctly when barrels is true 1`] = `
"import * as SuperUsersActions from './lib/+state/super-users.actions';
import * as SuperUsersFeature from './lib/+state/super-users.reducer';
import * as SuperUsersSelectors from './lib/+state/super-users.selectors';
export * from './lib/+state/super-users.facade';
export * from './lib/+state/super-users.models';
export { SuperUsersActions, SuperUsersFeature, SuperUsersSelectors };
export * from './lib/flights.module';
"
`;
exports[`NgRx generator creators syntax should update the entry point file with no facade 1`] = `
"export * from './lib/+state/super-users.models';
export * from './lib/+state/super-users.selectors';
export * from './lib/+state/super-users.reducer';
export * from './lib/+state/super-users.actions';
export * from './lib/flights.module';
"
`;
exports[`NgRx generator creators syntax should update the entry point file with the right exports 1`] = `
"export * from './lib/+state/super-users.facade';
export * from './lib/+state/super-users.models';
export * from './lib/+state/super-users.selectors';
export * from './lib/+state/super-users.reducer';
export * from './lib/+state/super-users.actions';
export * from './lib/flights.module';
"
`;
exports[`NgRx generator creators syntax should use DataPersistence when useDataPersistence is true 1`] = `
"import { Injectable } from '@angular/core';
import { createEffect, Actions, ofType } from '@ngrx/effects';
import { DataPersistence } from '@nrwl/angular';
import * as UsersActions from './users.actions';
import * as UsersFeature from './users.reducer';
@Injectable()
export class UsersEffects {
init$ = createEffect(() => this.dataPersistence.fetch(UsersActions.init, {
run: (action: ReturnType<typeof UsersActions.init>, state: UsersFeature.UsersPartialState) => {
// Your custom service 'load' logic goes here. For now just return a success action...
return UsersActions.loadUsersSuccess({ users: [] });
},
onError: (action: ReturnType<typeof UsersActions.init>, error) => {
console.error('Error', error);
return UsersActions.loadUsersFailure({ error });
}
}));
constructor(
private readonly actions$: Actions,
private readonly dataPersistence: DataPersistence<UsersFeature.UsersPartialState>
) {}
}
"
`;
exports[`NgRx generator creators syntax unit tests should generate specs for the ngrx effects 1`] = `
"import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Action } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing';
import { NxModule } from '@nrwl/angular';
import { hot } from '@nrwl/angular/testing';
import { Observable } from 'rxjs';
import * as SuperUsersActions from './super-users.actions';
import { SuperUsersEffects } from './super-users.effects';
describe('SuperUsersEffects', () => {
let actions: Observable<Action>;
let effects: SuperUsersEffects;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
NxModule.forRoot(),
],
providers: [
SuperUsersEffects,
provideMockActions(() => actions),
provideMockStore()
],
});
effects = TestBed.inject(SuperUsersEffects);
});
describe('init$', () => {
it('should work', () => {
actions = hot('-a-|', { a: SuperUsersActions.init() });
const expected = hot('-a-|', { a: SuperUsersActions.loadSuperUsersSuccess({ superUsers: [] }) });
expect(effects.init$).toBeObservable(expected);
});
});
});
"
`;
exports[`NgRx generator creators syntax unit tests should generate specs for the ngrx effects correctly when useDataPersistence is true 1`] = `
"import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Action } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing';
import { NxModule, DataPersistence } from '@nrwl/angular';
import { hot } from '@nrwl/angular/testing';
import { Observable } from 'rxjs';
import * as SuperUsersActions from './super-users.actions';
import { SuperUsersEffects } from './super-users.effects';
describe('SuperUsersEffects', () => {
let actions: Observable<Action>;
let effects: SuperUsersEffects;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
NxModule.forRoot(),
],
providers: [
SuperUsersEffects,
DataPersistence,
provideMockActions(() => actions),
provideMockStore()
],
});
effects = TestBed.inject(SuperUsersEffects);
});
describe('init$', () => {
it('should work', () => {
actions = hot('-a-|', { a: SuperUsersActions.init() });
const expected = hot('-a-|', { a: SuperUsersActions.loadSuperUsersSuccess({ superUsers: [] }) });
expect(effects.init$).toBeObservable(expected);
});
});
});
"
`;
exports[`NgRx generator creators syntax unit tests should generate specs for the ngrx facade 1`] = `
"import { NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule, Store } from '@ngrx/store';
import { NxModule } from '@nrwl/angular';
import { readFirst } from '@nrwl/angular/testing';
import * as SuperUsersActions from './super-users.actions';
import { SuperUsersEffects } from './super-users.effects';
import { SuperUsersFacade } from './super-users.facade';
import { SuperUsersEntity } from './super-users.models';
import {
SUPER_USERS_FEATURE_KEY,
State,
initialState,
reducer
} from './super-users.reducer';
import * as SuperUsersSelectors from './super-users.selectors';
interface TestSchema {
superUsers: State;
}
describe('SuperUsersFacade', () => {
let facade: SuperUsersFacade;
let store: Store<TestSchema>;
const createSuperUsersEntity = (id: string, name = ''): SuperUsersEntity => ({
id,
name: name || \`name-\${id}\`
});
describe('used in NgModule', () => {
beforeEach(() => {
@NgModule({
imports: [
StoreModule.forFeature(SUPER_USERS_FEATURE_KEY, reducer),
EffectsModule.forFeature([SuperUsersEffects])
],
providers: [SuperUsersFacade]
})
class CustomFeatureModule {}
@NgModule({
imports: [
NxModule.forRoot(),
StoreModule.forRoot({}),
EffectsModule.forRoot([]),
CustomFeatureModule,
]
})
class RootModule {}
TestBed.configureTestingModule({ imports: [RootModule] });
store = TestBed.inject(Store);
facade = TestBed.inject(SuperUsersFacade);
});
/**
* The initially generated facade::loadAll() returns empty array
*/
it('loadAll() should return empty list with loaded == true', async (done) => {
try {
let list = await readFirst(facade.allSuperUsers$);
let isLoaded = await readFirst(facade.loaded$);
expect(list.length).toBe(0);
expect(isLoaded).toBe(false);
facade.init();
list = await readFirst(facade.allSuperUsers$);
isLoaded = await readFirst(facade.loaded$);
expect(list.length).toBe(0);
expect(isLoaded).toBe(true);
done();
} catch (err) {
done.fail(err);
}
});
/**
* Use \`loadSuperUsersSuccess\` to manually update list
*/
it('allSuperUsers$ should return the loaded list; and loaded flag == true', async (done) => {
try {
let list = await readFirst(facade.allSuperUsers$);
let isLoaded = await readFirst(facade.loaded$);
expect(list.length).toBe(0);
expect(isLoaded).toBe(false);
store.dispatch(SuperUsersActions.loadSuperUsersSuccess({
superUsers: [
createSuperUsersEntity('AAA'),
createSuperUsersEntity('BBB')
]})
);
list = await readFirst(facade.allSuperUsers$);
isLoaded = await readFirst(facade.loaded$);
expect(list.length).toBe(2);
expect(isLoaded).toBe(true);
done();
} catch (err) {
done.fail(err);
}
});
});
});
"
`;
exports[`NgRx generator creators syntax unit tests should generate specs for the ngrx reducer 1`] = `
"import { Action } from '@ngrx/store';
import * as SuperUsersActions from './super-users.actions';
import { SuperUsersEntity } from './super-users.models';
import { State, initialState, reducer } from './super-users.reducer';
describe('SuperUsers Reducer', () => {
const createSuperUsersEntity = (id: string, name = ''): SuperUsersEntity => ({
id,
name: name || \`name-\${id}\`
});
describe('valid SuperUsers actions', () => {
it('loadSuperUsersSuccess should return the list of known SuperUsers', () => {
const superUsers = [
createSuperUsersEntity('PRODUCT-AAA'),
createSuperUsersEntity('PRODUCT-zzz')
];
const action = SuperUsersActions.loadSuperUsersSuccess({ superUsers });
const result: State = reducer(initialState, action);
expect(result.loaded).toBe(true);
expect(result.ids.length).toBe(2);
});
});
describe('unknown action', () => {
it('should return the previous state', () => {
const action = {} as Action;
const result = reducer(initialState, action);
expect(result).toBe(initialState);
});
});
});
"
`;
exports[`NgRx generator creators syntax unit tests should generate specs for the ngrx selectors 1`] = `
"import { SuperUsersEntity } from './super-users.models';
import { superUsersAdapter, SuperUsersPartialState, initialState } from './super-users.reducer';
import * as SuperUsersSelectors from './super-users.selectors';
describe('SuperUsers Selectors', () => {
const ERROR_MSG = 'No Error Available';
const getSuperUsersId = (it: SuperUsersEntity) => it.id;
const createSuperUsersEntity = (id: string, name = '') => ({
id,
name: name || \`name-\${id}\`
}) as SuperUsersEntity;
let state: SuperUsersPartialState;
beforeEach(() => {
state = {
superUsers: superUsersAdapter.setAll([
createSuperUsersEntity('PRODUCT-AAA'),
createSuperUsersEntity('PRODUCT-BBB'),
createSuperUsersEntity('PRODUCT-CCC')
], {
...initialState,
selectedId : 'PRODUCT-BBB',
error: ERROR_MSG,
loaded: true
})
};
});
describe('SuperUsers Selectors', () => {
it('getAllSuperUsers() should return the list of SuperUsers', () => {
const results = SuperUsersSelectors.getAllSuperUsers(state);
const selId = getSuperUsersId(results[1]);
expect(results.length).toBe(3);
expect(selId).toBe('PRODUCT-BBB');
});
it('getSelected() should return the selected Entity', () => {
const result = SuperUsersSelectors.getSelected(state) as SuperUsersEntity;
const selId = getSuperUsersId(result);
expect(selId).toBe('PRODUCT-BBB');
});
it('getSuperUsersLoaded() should return the current \\"loaded\\" status', () => {
const result = SuperUsersSelectors.getSuperUsersLoaded(state);
expect(result).toBe(true);
});
it('getSuperUsersError() should return the current \\"error\\" state', () => {
const result = SuperUsersSelectors.getSuperUsersError(state);
expect(result).toBe(ERROR_MSG);
});
});
});
"
`;

View File

@ -0,0 +1,112 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ngrx should add a root module with feature module when minimal and onlyEmptyRoot are false 1`] = `
"
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import * as fromUsers from './+state/users.reducer';
import { UsersEffects } from './+state/users.effects';
import { NxModule } from '@nrwl/angular';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
@NgModule({
imports: [BrowserModule, RouterModule.forRoot([]), NxModule.forRoot(), StoreModule.forRoot({}, {
metaReducers: !environment.production ? [] : [],
runtimeChecks: {
strictActionImmutability: true,
strictStateImmutability: true
}
}), EffectsModule.forRoot([UsersEffects]), !environment.production ? StoreDevtoolsModule.instrument() : [], StoreRouterConnectingModule.forRoot(), StoreModule.forFeature(fromUsers.USERS_FEATURE_KEY, fromUsers.reducer)],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
"
`;
exports[`ngrx should add an empty root module when minimal is true 1`] = `
"
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
@NgModule({
imports: [BrowserModule, RouterModule.forRoot([]), StoreModule.forRoot({}, {
metaReducers: !environment.production ? [] : [],
runtimeChecks: {
strictActionImmutability: true,
strictStateImmutability: true
}
}), EffectsModule.forRoot([]), !environment.production ? StoreDevtoolsModule.instrument() : [], StoreRouterConnectingModule.forRoot()],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
"
`;
exports[`ngrx should add an empty root module when onlyEmptyRoot is true 1`] = `
"
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { environment } from '../environments/environment';
import { StoreRouterConnectingModule } from '@ngrx/router-store';
@NgModule({
imports: [BrowserModule, RouterModule.forRoot([]), StoreModule.forRoot({}, {
metaReducers: !environment.production ? [] : [],
runtimeChecks: {
strictActionImmutability: true,
strictStateImmutability: true
}
}), EffectsModule.forRoot([]), !environment.production ? StoreDevtoolsModule.instrument() : [], StoreRouterConnectingModule.forRoot()],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
"
`;
exports[`ngrx should only add files when onlyAddFiles is true 1`] = `
"
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
@NgModule({
imports: [BrowserModule, RouterModule.forRoot([])],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
"
`;
exports[`ngrx should only add files when skipImport is true 1`] = `
"
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
@NgModule({
imports: [BrowserModule, RouterModule.forRoot([])],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule {}
"
`;

View File

@ -0,0 +1,4 @@
import { convertNxGenerator } from '@nrwl/devkit';
import { ngrxGenerator } from './ngrx';
export default convertNxGenerator(ngrxGenerator);

View File

@ -13,11 +13,13 @@ export class Load<%= className %> implements Action {
export class <%= className %>LoadError implements Action {
readonly type = <%= className %>ActionTypes.<%= className %>LoadError;
constructor(public payload: any) {}
}
export class <%= className %>Loaded implements Action {
readonly type = <%= className %>ActionTypes.<%= className %>Loaded;
constructor(public payload: Entity[]) {}
}

View File

@ -1,13 +1,10 @@
import { TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs';
import { EffectsModule } from '@ngrx/effects';
import { Action, StoreModule } from '@ngrx/store';
import { provideMockActions } from '@ngrx/effects/testing';
import { Action, StoreModule } from '@ngrx/store';
import { NxModule<% if (useDataPersistence) { %>, DataPersistence<% } %> } from '@nrwl/angular';
import { hot } from '@nrwl/angular/testing';
import { Observable } from 'rxjs';
import { <%= className %>Effects } from './<%= fileName %>.effects';
import { Load<%= className %>, <%= className %>Loaded } from './<%= fileName %>.actions';
@ -24,8 +21,8 @@ describe('<%= className %>Effects', () => {
EffectsModule.forRoot([])
],
providers: [
<%= className %>Effects,
<% if (useDataPersistence) { %>DataPersistence,<% } %>
<%= className %>Effects,<% if (useDataPersistence) { %>
DataPersistence,<% } %>
provideMockActions(() => actions)
],
});
@ -36,6 +33,7 @@ describe('<%= className %>Effects', () => {
describe('load<%= className %>$', () => {
it('should work', () => {
actions = hot('-a-|', { a: new Load<%= className %>() });
expect(effects.load<%= className %>$).toBeObservable(
hot('-a-|', { a: new <%= className %>Loaded([]) })
);

View File

@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import { Effect, Actions<% if (!useDataPersistence) { %>, ofType<% }%> } from '@ngrx/effects';
import { <% if (useDataPersistence) { %>DataPersistence<% } else { %>fetch<% } %> } from '@nrwl/angular';
import { Load<%= className %>, <%= className %>Loaded, <%= className %>LoadError, <%= className %>ActionTypes } from './<%= fileName %>.actions';
import { <%= className %>PartialState } from './<%= fileName %>.reducer';
@Injectable()
export class <%= className %>Effects {
@Effect() load<%= className %>$ = <% if (useDataPersistence) { %>this.dataPersistence.fetch(<%= className %>ActionTypes.Load<%= className %>, {
run: (action: Load<%= className %>, state: <%= className %>PartialState) => {
// Your custom REST 'load' logic goes here. For now just return an empty list...
return new <%= className %>Loaded([]);
},
onError: (action: Load<%= className %>, error) => {
console.error('Error', error);
return new <%= className %>LoadError(error);
}
}); <% } else { %> this.actions$.pipe(
ofType(<%= className %>ActionTypes.Load<%= className %>),
fetch({
run: (action: Load<%= className %>, state: <%= className %>PartialState) => {
// Your custom REST 'load' logic goes here. For now just return an empty list...
return new <%= className %>Loaded([]);
},
onError: (action: Load<%= className %>, error) => {
console.error('Error', error);
return new <%= className %>LoadError(error);
}
})
);<% } %>
constructor(
private readonly actions$: Actions<% if (useDataPersistence) { %>,
private readonly dataPersistence: DataPersistence<<%= className %>PartialState><% } %>
) {}
}

View File

@ -1,26 +1,23 @@
import { NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { readFirst } from '@nrwl/angular/testing';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule, Store } from '@ngrx/store';
import { NxModule } from '@nrwl/angular';
import { readFirst } from '@nrwl/angular/testing';
import { Load<%= className %>, <%= className %>Loaded } from './<%= fileName %>.actions';
import { <%= className %>Effects } from './<%= fileName %>.effects';
import { <%= className %>Facade } from './<%= fileName %>.facade';
import { <%= propertyName %>Query } from './<%= fileName %>.selectors';
import { Load<%= className %>, <%= className %>Loaded } from './<%= fileName %>.actions';
import {
<%= className %>State,
Entity,
initialState,
reducer
} from './<%= fileName %>.reducer';
import { <%= propertyName %>Query } from './<%= fileName %>.selectors';
interface TestSchema {
'<%= propertyName %>': <%= className %>State
<%= propertyName %>: <%= className %>State;
}
describe('<%= className %>Facade', () => {

View File

@ -1,5 +1,4 @@
import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { <%= className %>PartialState } from './<%= fileName %>.reducer';
@ -8,12 +7,11 @@ import { Load<%= className %> } from './<%= fileName %>.actions';
@Injectable()
export class <%= className %>Facade {
loaded$ = this.store.pipe(select(<%= propertyName %>Query.getLoaded));
all<%= className %>$ = this.store.pipe(select(<%= propertyName %>Query.getAll<%= className %>));
selected<%= className %>$ = this.store.pipe(select(<%= propertyName %>Query.getSelected<%= className %>));
constructor(private store: Store<<%= className %>PartialState>) { }
constructor(private readonly store: Store<<%= className %>PartialState>) {}
loadAll() {
this.store.dispatch(new Load<%= className %>());

View File

@ -1,5 +1,3 @@
import { Action } from '@ngrx/store';
import { <%= className %>Loaded } from './<%= fileName %>.actions';
import { <%= className %>State, Entity, initialState, reducer } from './<%= fileName %>.reducer';
@ -11,9 +9,9 @@ describe('<%= className %> Reducer', () => {
});
describe('valid <%= className %> actions', () => {
it('should return set the list of known <%= className %>', () => {
const <%= propertyName %>s = [create<%= className %>( 'PRODUCT-AAA' ),create<%= className %>( 'PRODUCT-zzz' )];
const action = new <%= className %>Loaded(<%= propertyName %>s);
it('should return the list of known <%= className %>', () => {
const <%= propertyName %> = [create<%= className %>('PRODUCT-AAA'), create<%= className %>('PRODUCT-zzz')];
const action = new <%= className %>Loaded(<%= propertyName %>);
const result: <%= className %>State = reducer(initialState, action);
const selId: string = get<%= className %>Id(result.list[1]);
@ -25,7 +23,7 @@ describe('<%= className %> Reducer', () => {
describe('unknown action', () => {
it('should return the previous state', () => {
const action = {} as Action;
const action = {} as any;
const result = reducer(initialState, action);
expect(result).toBe(initialState);

View File

@ -42,13 +42,13 @@ describe('<%= className %> Selectors', () => {
expect(selId).toBe('PRODUCT-BBB');
});
it('getLoaded() should return the current \'loaded\' status', () => {
it('getLoaded() should return the current "loaded" status', () => {
const result = <%= propertyName %>Query.getLoaded(storeState);
expect(result).toBe(true);
});
it('getError() should return the current \'error\' storeState', () => {
it('getError() should return the current "error" storeState', () => {
const result = <%= propertyName %>Query.getError(storeState);
expect(result).toBe(ERROR_MSG);

View File

@ -1,18 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Action } from '@ngrx/store';
import { provideMockStore } from '@ngrx/store/testing';
import { NxModule<% if (useDataPersistence) { %>, DataPersistence<% } %> } from '@nrwl/angular';
import { hot } from '@nrwl/angular/testing';
import { Observable } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { NxModule, DataPersistence } from '@nrwl/angular';
import { hot } from '@nrwl/angular/testing';
import { <%= className %>Effects } from './<%= fileName %>.effects';
import * as <%= className %>Actions from './<%= fileName %>.actions';
import { <%= className %>Effects } from './<%= fileName %>.effects';
describe('<%= className %>Effects', () => {
let actions: Observable<any>;
let actions: Observable<Action>;
let effects: <%= className %>Effects;
beforeEach(() => {
@ -21,8 +19,8 @@ describe('<%= className %>Effects', () => {
NxModule.forRoot(),
],
providers: [
<%= className %>Effects,
DataPersistence,
<%= className %>Effects,<% if (useDataPersistence) { %>
DataPersistence,<% } %>
provideMockActions(() => actions),
provideMockStore()
],

View File

@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
import { createEffect, Actions, ofType } from '@ngrx/effects';
import { <% if (useDataPersistence) { %>DataPersistence<% } %><% if (!useDataPersistence) { %>fetch<% } %> } from '@nrwl/angular';
import * as <%= className %>Feature from './<%= fileName %>.reducer';
import * as <%= className %>Actions from './<%= fileName %>.actions';
import * as <%= className %>Feature from './<%= fileName %>.reducer';
@Injectable()
export class <%= className %>Effects {
@ -12,30 +12,26 @@ export class <%= className %>Effects {
// Your custom service 'load' logic goes here. For now just return a success action...
return <%= className %>Actions.load<%= className %>Success({ <%= propertyName %>: [] });
},
onError: (action: ReturnType<typeof <%= className %>Actions.init>, error) => {
console.error('Error', error);
return <%= className %>Actions.load<%= className %>Failure({ error });
}
}));
<% } else { %> init$ = createEffect(() => this.actions$.pipe(
}));<% } else { %>init$ = createEffect(() => this.actions$.pipe(
ofType(<%= className %>Actions.init),
fetch({
run: action => {
// Your custom service 'load' logic goes here. For now just return a success action...
return <%= className %>Actions.load<%= className %>Success({ <%= propertyName %>: [] });
},
onError: (action, error) => {
console.error('Error', error);
return <%= className %>Actions.load<%= className %>Failure({ error });
}
}))
);
<% } %>
})
));<% } %>
constructor(
private actions$: Actions,
<% if (useDataPersistence) { %> private dataPersistence: DataPersistence<<%= className %>Feature.<%= className %>PartialState>, <% } %>
private readonly actions$: Actions<% if (useDataPersistence) { %>,
private readonly dataPersistence: DataPersistence<<%= className %>Feature.<%= className %>PartialState><% } %>
) {}
}

View File

@ -1,27 +1,24 @@
import { NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { readFirst } from '@nrwl/angular/testing';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule, Store } from '@ngrx/store';
import { NxModule } from '@nrwl/angular';
import { readFirst } from '@nrwl/angular/testing';
import { <%= className %>Entity } from './<%= fileName %>.models';
import * as <%= className %>Actions from './<%= fileName %>.actions';
import { <%= className %>Effects } from './<%= fileName %>.effects';
import { <%= className %>Facade } from './<%= fileName %>.facade';
import * as <%= className %>Selectors from './<%= fileName %>.selectors';
import * as <%= className %>Actions from './<%= fileName %>.actions';
import { <%= className %>Entity } from './<%= fileName %>.models';
import {
<%= constantName %>_FEATURE_KEY,
State,
initialState,
reducer
} from './<%= fileName %>.reducer';
import * as <%= className %>Selectors from './<%= fileName %>.selectors';
interface TestSchema {
'<%= propertyName %>': State
<%= propertyName %>: State;
}
describe('<%= className %>Facade', () => {

View File

@ -1,5 +1,4 @@
import { Injectable } from '@angular/core';
import { select, Store, Action } from '@ngrx/store';
import * as <%= className %>Actions from './<%= fileName %>.actions';
@ -16,7 +15,7 @@ export class <%= className %>Facade {
all<%= className %>$ = this.store.pipe(select(<%= className %>Selectors.getAll<%= className %>));
selected<%= className %>$ = this.store.pipe(select(<%= className %>Selectors.getSelected));
constructor(private store: Store) { }
constructor(private readonly store: Store) {}
/**
* Use the initialization action to perform one

View File

@ -1,5 +1,7 @@
import { <%= className %>Entity } from './<%= fileName %>.models';
import { Action } from '@ngrx/store';
import * as <%= className %>Actions from './<%= fileName %>.actions';
import { <%= className %>Entity } from './<%= fileName %>.models';
import { State, initialState, reducer } from './<%= fileName %>.reducer';
describe('<%= className %> Reducer', () => {
@ -9,7 +11,7 @@ describe('<%= className %> Reducer', () => {
});
describe('valid <%= className %> actions', () => {
it('load<%= className %>Success should return set the list of known <%= className %>', () => {
it('load<%= className %>Success should return the list of known <%= className %>', () => {
const <%= propertyName %> = [
create<%= className %>Entity('PRODUCT-AAA'),
create<%= className %>Entity('PRODUCT-zzz')
@ -25,7 +27,7 @@ describe('<%= className %> Reducer', () => {
describe('unknown action', () => {
it('should return the previous state', () => {
const action = {} as any;
const action = {} as Action;
const result = reducer(initialState, action);

View File

@ -1,5 +1,5 @@
import { createReducer, on, Action } from '@ngrx/store';
import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity';
import { createReducer, on, Action } from '@ngrx/store';
import * as <%= className %>Actions from './<%= fileName %>.actions';
import { <%= className %>Entity } from './<%= fileName %>.models';

View File

@ -43,13 +43,13 @@ describe('<%= className %> Selectors', () => {
expect(selId).toBe('PRODUCT-BBB');
});
it('get<%= className %>Loaded() should return the current \'loaded\' status', () => {
it('get<%= className %>Loaded() should return the current "loaded" status', () => {
const result = <%= className %>Selectors.get<%= className %>Loaded(state);
expect(result).toBe(true);
});
it('get<%= className %>Error() should return the current \'error\' state', () => {
it('get<%= className %>Error() should return the current "error" state', () => {
const result = <%= className %>Selectors.get<%= className %>Error(state);
expect(result).toBe(ERROR_MSG);

View File

@ -0,0 +1,92 @@
import type { Tree } from '@nrwl/devkit';
import { joinPathFragments, names } from '@nrwl/devkit';
import { addGlobal } from '@nrwl/workspace/src/utilities/ast-utils';
import { dirname } from 'path';
import { createSourceFile, ScriptTarget } from 'typescript';
import type { NgRxGeneratorOptions } from '../schema';
/**
* Add ngrx feature exports to the public barrel in the feature library
*/
export function addExportsToBarrel(
tree: Tree,
options: NgRxGeneratorOptions
): void {
// Don't update the public barrel for the root state, only for feature states
if (options.root) {
return;
}
const indexFilePath = joinPathFragments(
dirname(options.module),
'..',
'index.ts'
);
if (!tree.exists(indexFilePath)) {
return;
}
const indexSourceText = tree.read(indexFilePath, 'utf-8');
let sourceFile = createSourceFile(
indexFilePath,
indexSourceText,
ScriptTarget.Latest,
true
);
// Public API for the feature interfaces, selectors, and facade
const { className, fileName } = names(options.name);
const statePath = `./lib/${options.directory}/${fileName}`;
sourceFile = addGlobal(
tree,
sourceFile,
indexFilePath,
options.barrels
? `import * as ${className}Actions from '${statePath}.actions';`
: `export * from '${statePath}.actions';`
);
sourceFile = addGlobal(
tree,
sourceFile,
indexFilePath,
options.barrels
? `import * as ${className}Feature from '${statePath}.reducer';`
: `export * from '${statePath}.reducer';`
);
sourceFile = addGlobal(
tree,
sourceFile,
indexFilePath,
options.barrels
? `import * as ${className}Selectors from '${statePath}.selectors';`
: `export * from '${statePath}.selectors';`
);
if (options.barrels) {
sourceFile = addGlobal(
tree,
sourceFile,
indexFilePath,
`export { ${className}Actions, ${className}Feature, ${className}Selectors };`
);
}
if (options.syntax === 'creators') {
sourceFile = addGlobal(
tree,
sourceFile,
indexFilePath,
`export * from '${statePath}.models';`
);
}
if (options.facade) {
sourceFile = addGlobal(
tree,
sourceFile,
indexFilePath,
`export * from '${statePath}.facade';`
);
}
}

View File

@ -0,0 +1,198 @@
import type { Tree } from '@nrwl/devkit';
import { names } from '@nrwl/devkit';
import { insertImport } from '@nrwl/workspace/src/utilities/ast-utils';
import type { SourceFile } from 'typescript';
import { createSourceFile, ScriptTarget } from 'typescript';
import {
addImportToModule,
addProviderToModule,
} from '../../../utils/nx-devkit/ast-utils';
import type { NgRxGeneratorOptions } from '../schema';
export function addImportsToModule(
tree: Tree,
options: NgRxGeneratorOptions
): void {
const modulePath = options.module;
const sourceText = tree.read(modulePath, 'utf-8');
let sourceFile = createSourceFile(
modulePath,
sourceText,
ScriptTarget.Latest,
true
);
const addImport = (
source: SourceFile,
symbolName: string,
fileName: string,
isDefault = false
): SourceFile => {
return insertImport(
tree,
source,
modulePath,
symbolName,
fileName,
isDefault
);
};
const dir = `./${names(options.directory).fileName}`;
const pathPrefix = `${dir}/${names(options.name).fileName}`;
const reducerPath = `${pathPrefix}.reducer`;
const effectsPath = `${pathPrefix}.effects`;
const facadePath = `${pathPrefix}.facade`;
const constantName = `${names(options.name).constantName}`;
const effectsName = `${names(options.name).className}Effects`;
const facadeName = `${names(options.name).className}Facade`;
const className = `${names(options.name).className}`;
const reducerImports = `* as from${className}`;
const storeMetaReducers = `metaReducers: !environment.production ? [] : []`;
const storeForRoot = `StoreModule.forRoot({}, {
${storeMetaReducers},
runtimeChecks: {
strictActionImmutability: true,
strictStateImmutability: true
}
})`;
const nxModule = 'NxModule.forRoot()';
const effectsForRoot = `EffectsModule.forRoot([${effectsName}])`;
const effectsForEmptyRoot = `EffectsModule.forRoot([])`;
const storeForFeature = `StoreModule.forFeature(from${className}.${constantName}_FEATURE_KEY, from${className}.reducer)`;
const effectsForFeature = `EffectsModule.forFeature([${effectsName}])`;
const devTools = `!environment.production ? StoreDevtoolsModule.instrument() : []`;
const storeRouterModule = 'StoreRouterConnectingModule.forRoot()';
// this is just a heuristic
const hasRouter = sourceText.indexOf('RouterModule') > -1;
const hasNxModule = sourceText.includes(nxModule);
sourceFile = addImport(sourceFile, 'StoreModule', '@ngrx/store');
sourceFile = addImport(sourceFile, 'EffectsModule', '@ngrx/effects');
if ((options.onlyEmptyRoot || options.minimal) && options.root) {
sourceFile = addImport(
sourceFile,
'StoreDevtoolsModule',
'@ngrx/store-devtools'
);
sourceFile = addImport(
sourceFile,
'environment',
'../environments/environment'
);
sourceFile = addImportToModule(tree, sourceFile, modulePath, storeForRoot);
sourceFile = addImportToModule(
tree,
sourceFile,
modulePath,
effectsForEmptyRoot
);
sourceFile = addImportToModule(tree, sourceFile, modulePath, devTools);
if (hasRouter) {
sourceFile = addImport(
sourceFile,
'StoreRouterConnectingModule',
'@ngrx/router-store'
);
sourceFile = addImportToModule(
tree,
sourceFile,
modulePath,
storeRouterModule
);
}
} else {
const addCommonImports = (): SourceFile => {
sourceFile = addImport(sourceFile, reducerImports, reducerPath, true);
sourceFile = addImport(sourceFile, effectsName, effectsPath);
if (options.facade) {
sourceFile = addImport(sourceFile, facadeName, facadePath);
sourceFile = addProviderToModule(
tree,
sourceFile,
modulePath,
facadeName
);
}
return sourceFile;
};
if (options.root) {
sourceFile = addCommonImports();
if (!hasNxModule) {
sourceFile = addImport(sourceFile, 'NxModule', '@nrwl/angular');
sourceFile = addImportToModule(tree, sourceFile, modulePath, nxModule);
}
sourceFile = addImport(
sourceFile,
'StoreDevtoolsModule',
'@ngrx/store-devtools'
);
sourceFile = addImport(
sourceFile,
'environment',
'../environments/environment'
);
sourceFile = addImportToModule(
tree,
sourceFile,
modulePath,
storeForRoot
);
sourceFile = addImportToModule(
tree,
sourceFile,
modulePath,
effectsForRoot
);
sourceFile = addImportToModule(tree, sourceFile, modulePath, devTools);
if (hasRouter) {
sourceFile = addImport(
sourceFile,
'StoreRouterConnectingModule',
'@ngrx/router-store'
);
sourceFile = addImportToModule(
tree,
sourceFile,
modulePath,
storeRouterModule
);
}
sourceFile = addImportToModule(
tree,
sourceFile,
modulePath,
storeForFeature
);
} else {
sourceFile = addCommonImports();
sourceFile = addImportToModule(
tree,
sourceFile,
modulePath,
storeForFeature
);
sourceFile = addImportToModule(
tree,
sourceFile,
modulePath,
effectsForFeature
);
}
}
}

View File

@ -1,9 +1,10 @@
import { Rule } from '@angular-devkit/schematics';
import { addDepsToPackageJson } from '@nrwl/workspace';
import type { GeneratorCallback, Tree } from '@nrwl/devkit';
import { addDependenciesToPackageJson } from '@nrwl/devkit';
import { ngrxVersion } from '../../../utils/versions';
export function addNgRxToPackageJson(): Rule {
return addDepsToPackageJson(
export function addNgRxToPackageJson(tree: Tree): GeneratorCallback {
return addDependenciesToPackageJson(
tree,
{
'@ngrx/store': ngrxVersion,
'@ngrx/effects': ngrxVersion,

View File

@ -0,0 +1,48 @@
import type { Tree } from '@nrwl/devkit';
import { generateFiles, joinPathFragments, names } from '@nrwl/devkit';
import { dirname } from 'path';
import type { NgRxGeneratorOptions } from '../schema';
/**
* Generate 'feature' scaffolding: actions, reducer, effects, interfaces, selectors, facade
*/
export function generateNgrxFilesFromTemplates(
tree: Tree,
options: NgRxGeneratorOptions
): void {
const name = options.name;
const moduleDir = dirname(options.module);
const templatesDir =
!options.syntax || options.syntax === 'creators'
? './files/creator-syntax'
: './files/classes-syntax';
const projectNames = names(name);
generateFiles(
tree,
joinPathFragments(__dirname, '..', templatesDir),
moduleDir,
{
...options,
tmpl: '',
...projectNames,
}
);
if (!options.facade) {
tree.delete(
joinPathFragments(
moduleDir,
options.directory,
`${projectNames.fileName}.facade.ts`
)
);
tree.delete(
joinPathFragments(
moduleDir,
options.directory,
`${projectNames.fileName}.facade.spec.ts`
)
);
}
}

View File

@ -1,4 +1,5 @@
export { RequestContext } from './request-context';
export { addExportsToBarrel } from './add-exports-barrel';
export { addImportsToModule } from './add-imports-to-module';
export { addNgRxToPackageJson } from './add-ngrx-to-package-json';
export { addExportsToBarrel } from './add-exports-barrel';
export { generateNgrxFilesFromTemplates } from './generate-files';
export { normalizeOptions } from './normalize-options';

View File

@ -0,0 +1,20 @@
import { names } from '@nrwl/devkit';
import type { NgRxGeneratorOptions } from '../schema';
export function normalizeOptions(
options: NgRxGeneratorOptions
): NgRxGeneratorOptions {
const normalizedOptions = {
...options,
directory: names(options.directory).fileName,
};
if (options.minimal) {
normalizedOptions.onlyEmptyRoot = true;
}
if (options.skipImport) {
normalizedOptions.onlyAddFiles = true;
}
return normalizedOptions;
}

View File

@ -0,0 +1,278 @@
import { Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { dirname } from 'path';
import {
AppConfig,
createApp,
createLib,
getAppConfig,
getLibConfig,
} from '../../utils/nx-devkit/testing';
import { ngrxGenerator } from './ngrx';
import { NgRxGeneratorOptions } from './schema';
describe('NgRx generator', () => {
let appConfig: AppConfig;
let statePath: string;
let tree: Tree;
const defaultOptions: NgRxGeneratorOptions = {
directory: '+state',
minimal: true,
module: 'apps/myapp/src/app/app.module.ts',
name: 'users',
useDataPersistence: false,
syntax: 'classes',
};
const expectFileToExist = (file: string) =>
expect(tree.exists(file)).toBeTruthy();
const expectFileToNotExist = (file: string) =>
expect(tree.exists(file)).not.toBeTruthy();
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
createApp(tree, 'myapp');
appConfig = getAppConfig();
statePath = `${dirname(appConfig.appModule)}/+state`;
});
describe('classes syntax', () => {
it('should generate files without a facade', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expectFileToExist(`${statePath}/users.actions.ts`);
expectFileToExist(`${statePath}/users.effects.ts`);
expectFileToExist(`${statePath}/users.effects.spec.ts`);
expectFileToExist(`${statePath}/users.reducer.ts`);
expectFileToExist(`${statePath}/users.reducer.spec.ts`);
expectFileToExist(`${statePath}/users.selectors.ts`);
expectFileToExist(`${statePath}/users.selectors.spec.ts`);
expectFileToNotExist(`${statePath}/users.facade.ts`);
expectFileToNotExist(`${statePath}/users.facade.spec.ts`);
});
it('should generate files with a facade', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
facade: true,
});
expectFileToExist(`${statePath}/users.actions.ts`);
expectFileToExist(`${statePath}/users.effects.ts`);
expectFileToExist(`${statePath}/users.effects.spec.ts`);
expectFileToExist(`${statePath}/users.facade.ts`);
expectFileToExist(`${statePath}/users.facade.spec.ts`);
expectFileToExist(`${statePath}/users.reducer.ts`);
expectFileToExist(`${statePath}/users.reducer.spec.ts`);
expectFileToExist(`${statePath}/users.selectors.ts`);
expectFileToExist(`${statePath}/users.selectors.spec.ts`);
});
it('should generate the ngrx actions', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expect(
tree.read(`${statePath}/users.actions.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate the ngrx effects', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expect(
tree.read(`${statePath}/users.effects.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate the ngrx facade', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
facade: true,
});
expect(
tree.read(`${statePath}/users.facade.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate the ngrx reducer', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expect(
tree.read(`${statePath}/users.reducer.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate the ngrx selectors', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expect(
tree.read(`${statePath}/users.selectors.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should use DataPersistence when useDataPersistence is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
minimal: false,
useDataPersistence: true,
});
expect(
tree.read(`${statePath}/users.effects.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate with custom directory', async () => {
statePath = '/apps/myapp/src/app/my-custom-directory';
await ngrxGenerator(tree, {
...defaultOptions,
directory: 'my-custom-directory',
minimal: false,
facade: true,
});
expectFileToExist(`${statePath}/users.actions.ts`);
expectFileToExist(`${statePath}/users.effects.ts`);
expectFileToExist(`${statePath}/users.effects.spec.ts`);
expectFileToExist(`${statePath}/users.facade.ts`);
expectFileToExist(`${statePath}/users.facade.spec.ts`);
expectFileToExist(`${statePath}/users.reducer.ts`);
expectFileToExist(`${statePath}/users.reducer.spec.ts`);
expectFileToExist(`${statePath}/users.selectors.ts`);
expectFileToExist(`${statePath}/users.selectors.spec.ts`);
});
it('should update the entry point file with the right exports', async () => {
createLib(tree, 'flights');
let libConfig = getLibConfig();
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: libConfig.module,
facade: true,
});
expect(tree.read(libConfig.barrel, 'utf-8')).toMatchSnapshot();
});
it('should update the entry point file correctly when barrels is true', async () => {
createLib(tree, 'flights');
let libConfig = getLibConfig();
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: libConfig.module,
facade: true,
barrels: true,
});
expect(tree.read(libConfig.barrel, 'utf-8')).toMatchSnapshot();
});
it('should update the entry point file with no facade', async () => {
createLib(tree, 'flights');
let libConfig = getLibConfig();
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: libConfig.module,
facade: false,
});
expect(tree.read(libConfig.barrel, 'utf-8')).toMatchSnapshot();
});
describe('unit tests', () => {
it('should generate specs for the ngrx effects', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
});
expect(
tree.read(`${statePath}/super-users.effects.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate specs for the ngrx effects correctly when useDataPersistence is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
useDataPersistence: true,
});
expect(
tree.read(`${statePath}/super-users.effects.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate specs for the ngrx facade', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
facade: true,
});
expect(
tree.read(`${statePath}/super-users.facade.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate specs for the ngrx reducer', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
});
expect(
tree.read(`${statePath}/super-users.reducer.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate specs for the ngrx selectors', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
});
expect(
tree.read(`${statePath}/super-users.selectors.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
});
});
});

View File

@ -0,0 +1,293 @@
import { Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { dirname } from 'path';
import {
AppConfig,
createApp,
createLib,
getAppConfig,
getLibConfig,
} from '../../utils/nx-devkit/testing';
import { ngrxGenerator } from './ngrx';
import { NgRxGeneratorOptions } from './schema';
describe('NgRx generator', () => {
let appConfig: AppConfig;
let statePath: string;
let tree: Tree;
const defaultOptions: NgRxGeneratorOptions = {
directory: '+state',
minimal: true,
module: 'apps/myapp/src/app/app.module.ts',
name: 'users',
useDataPersistence: false,
syntax: 'creators',
};
const expectFileToExist = (file: string) =>
expect(tree.exists(file)).toBeTruthy();
const expectFileToNotExist = (file: string) =>
expect(tree.exists(file)).not.toBeTruthy();
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
createApp(tree, 'myapp');
appConfig = getAppConfig();
statePath = `${dirname(appConfig.appModule)}/+state`;
});
describe('creators syntax', () => {
it('should generate files without a facade', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expectFileToExist(`${statePath}/users.actions.ts`);
expectFileToExist(`${statePath}/users.effects.ts`);
expectFileToExist(`${statePath}/users.effects.spec.ts`);
expectFileToExist(`${statePath}/users.models.ts`);
expectFileToExist(`${statePath}/users.reducer.ts`);
expectFileToExist(`${statePath}/users.reducer.spec.ts`);
expectFileToExist(`${statePath}/users.selectors.ts`);
expectFileToExist(`${statePath}/users.selectors.spec.ts`);
expectFileToNotExist(`${statePath}/users.facade.ts`);
expectFileToNotExist(`${statePath}/users.facade.spec.ts`);
});
it('should generate files with a facade', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
facade: true,
});
expectFileToExist(`${statePath}/users.actions.ts`);
expectFileToExist(`${statePath}/users.effects.ts`);
expectFileToExist(`${statePath}/users.effects.spec.ts`);
expectFileToExist(`${statePath}/users.facade.ts`);
expectFileToExist(`${statePath}/users.facade.spec.ts`);
expectFileToExist(`${statePath}/users.models.ts`);
expectFileToExist(`${statePath}/users.reducer.ts`);
expectFileToExist(`${statePath}/users.reducer.spec.ts`);
expectFileToExist(`${statePath}/users.selectors.ts`);
expectFileToExist(`${statePath}/users.selectors.spec.ts`);
});
it('should generate the ngrx actions', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expect(
tree.read(`${statePath}/users.actions.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate the ngrx effects', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expect(
tree.read(`${statePath}/users.effects.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate the ngrx facade', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
facade: true,
});
expect(
tree.read(`${statePath}/users.facade.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate a models file for the feature', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
minimal: false,
});
expect(
tree.read(`${statePath}/users.models.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate the ngrx reducer', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expect(
tree.read(`${statePath}/users.reducer.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate the ngrx selectors', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
});
expect(
tree.read(`${statePath}/users.selectors.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should use DataPersistence when useDataPersistence is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
module: appConfig.appModule,
minimal: false,
useDataPersistence: true,
});
expect(
tree.read(`${statePath}/users.effects.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate with custom directory', async () => {
statePath = '/apps/myapp/src/app/my-custom-directory';
await ngrxGenerator(tree, {
...defaultOptions,
directory: 'my-custom-directory',
minimal: false,
facade: true,
});
expectFileToExist(`${statePath}/users.actions.ts`);
expectFileToExist(`${statePath}/users.effects.ts`);
expectFileToExist(`${statePath}/users.effects.spec.ts`);
expectFileToExist(`${statePath}/users.facade.ts`);
expectFileToExist(`${statePath}/users.facade.spec.ts`);
expectFileToExist(`${statePath}/users.models.ts`);
expectFileToExist(`${statePath}/users.reducer.ts`);
expectFileToExist(`${statePath}/users.reducer.spec.ts`);
expectFileToExist(`${statePath}/users.selectors.ts`);
expectFileToExist(`${statePath}/users.selectors.spec.ts`);
});
it('should update the entry point file with the right exports', async () => {
createLib(tree, 'flights');
let libConfig = getLibConfig();
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: libConfig.module,
facade: true,
});
expect(tree.read(libConfig.barrel, 'utf-8')).toMatchSnapshot();
});
it('should update the entry point file correctly when barrels is true', async () => {
createLib(tree, 'flights');
let libConfig = getLibConfig();
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: libConfig.module,
facade: true,
barrels: true,
});
expect(tree.read(libConfig.barrel, 'utf-8')).toMatchSnapshot();
});
it('should update the entry point file with no facade', async () => {
createLib(tree, 'flights');
let libConfig = getLibConfig();
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: libConfig.module,
facade: false,
});
expect(tree.read(libConfig.barrel, 'utf-8')).toMatchSnapshot();
});
describe('unit tests', () => {
it('should generate specs for the ngrx effects', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
});
expect(
tree.read(`${statePath}/super-users.effects.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate specs for the ngrx effects correctly when useDataPersistence is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
useDataPersistence: true,
});
expect(
tree.read(`${statePath}/super-users.effects.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate specs for the ngrx facade', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
facade: true,
});
expect(
tree.read(`${statePath}/super-users.facade.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate specs for the ngrx reducer', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
});
expect(
tree.read(`${statePath}/super-users.reducer.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
it('should generate specs for the ngrx selectors', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
name: 'super-users',
module: appConfig.appModule,
minimal: false,
});
expect(
tree.read(`${statePath}/super-users.selectors.spec.ts`, 'utf-8')
).toMatchSnapshot();
});
});
});
});

View File

@ -0,0 +1,229 @@
import type { Tree } from '@nrwl/devkit';
import * as devkit from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { createApp } from '../../utils/nx-devkit/testing';
import { ngrxVersion } from '../../utils/versions';
import { ngrxGenerator } from './ngrx';
import type { NgRxGeneratorOptions } from './schema';
describe('ngrx', () => {
let tree: Tree;
const defaultOptions: NgRxGeneratorOptions = {
directory: '+state',
minimal: true,
module: 'apps/myapp/src/app/app.module.ts',
name: 'users',
useDataPersistence: false,
};
const expectFileToExist = (file: string) =>
expect(tree.exists(file)).toBeTruthy();
beforeEach(() => {
jest.clearAllMocks();
tree = createTreeWithEmptyWorkspace();
createApp(tree, 'myapp');
});
it('should error when the module could not be found', async () => {
const modulePath = 'not-existing.module.ts';
await expect(
ngrxGenerator(tree, {
...defaultOptions,
module: modulePath,
})
).rejects.toThrowError(`Module does not exist: ${modulePath}.`);
});
it('should add an empty root module when minimal is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
root: true,
minimal: true,
});
expect(
tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8')
).toMatchSnapshot();
});
it('should add an empty root module when onlyEmptyRoot is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
root: true,
minimal: false,
onlyEmptyRoot: true,
});
expect(
tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8')
).toMatchSnapshot();
});
it('should add a root module with feature module when minimal and onlyEmptyRoot are false', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
root: true,
minimal: false,
onlyEmptyRoot: false,
});
expect(
tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8')
).toMatchSnapshot();
});
it('should not add RouterStoreModule when the module does not reference the router', async () => {
createApp(tree, 'no-router-app', false);
await ngrxGenerator(tree, {
...defaultOptions,
module: 'apps/no-router-app/src/app/app.module.ts',
root: true,
});
const appModule = tree.read(
'/apps/no-router-app/src/app/app.module.ts',
'utf-8'
);
expect(appModule).not.toContain('StoreRouterConnectingModule.forRoot()');
});
it('should add facade provider when facade is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
root: true,
minimal: false,
facade: true,
});
expect(tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8')).toContain(
'providers: [UsersFacade]'
);
});
it('should not add facade provider when facade is false', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
root: true,
minimal: false,
facade: false,
});
expect(
tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8')
).not.toContain('providers: [UsersFacade]');
});
it('should not add facade provider when minimal is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
root: true,
minimal: true,
facade: true,
});
expect(
tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8')
).not.toContain('providers: [UsersFacade]');
});
it('should not add facade provider when onlyEmptyRoot is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
root: true,
minimal: false,
onlyEmptyRoot: true,
facade: true,
});
expect(
tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8')
).not.toContain('providers: [UsersFacade]');
});
it('should only add files when skipImport is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
minimal: false,
skipImport: true,
});
expectFileToExist('/apps/myapp/src/app/+state/users.actions.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.effects.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.effects.spec.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.reducer.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.selectors.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.selectors.spec.ts');
expect(
tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8')
).toMatchSnapshot();
});
it('should only add files when onlyAddFiles is true', async () => {
await ngrxGenerator(tree, {
...defaultOptions,
minimal: false,
onlyAddFiles: true,
});
expectFileToExist('/apps/myapp/src/app/+state/users.actions.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.effects.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.effects.spec.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.reducer.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.selectors.ts');
expectFileToExist('/apps/myapp/src/app/+state/users.selectors.spec.ts');
expect(
tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8')
).toMatchSnapshot();
});
it('should update package.json', async () => {
await ngrxGenerator(tree, defaultOptions);
const packageJson = devkit.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
);
});
it('should not update package.json when skipPackageJson is true', async () => {
await ngrxGenerator(tree, { ...defaultOptions, skipPackageJson: true });
const packageJson = devkit.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();
});
it('should format files', async () => {
jest.spyOn(devkit, 'formatFiles');
await ngrxGenerator(tree, defaultOptions);
expect(devkit.formatFiles).toHaveBeenCalled();
});
it('should not format files when skipFormat is true', async () => {
jest.spyOn(devkit, 'formatFiles');
await ngrxGenerator(tree, { ...defaultOptions, skipFormat: true });
expect(devkit.formatFiles).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,46 @@
import type { GeneratorCallback, Tree } from '@nrwl/devkit';
import { formatFiles } from '@nrwl/devkit';
import {
addExportsToBarrel,
addImportsToModule,
addNgRxToPackageJson,
generateNgrxFilesFromTemplates,
normalizeOptions,
} from './lib';
import type { NgRxGeneratorOptions } from './schema';
export async function ngrxGenerator(
tree: Tree,
options: NgRxGeneratorOptions
): Promise<GeneratorCallback> {
const normalizedOptions = normalizeOptions(options);
if (!tree.exists(normalizedOptions.module)) {
throw new Error(`Module does not exist: ${normalizedOptions.module}.`);
}
if (
!normalizedOptions.onlyEmptyRoot ||
(!normalizedOptions.root && normalizedOptions.minimal)
) {
generateNgrxFilesFromTemplates(tree, normalizedOptions);
}
if (!normalizedOptions.onlyAddFiles) {
addImportsToModule(tree, normalizedOptions);
addExportsToBarrel(tree, normalizedOptions);
}
let packageInstallationTask: GeneratorCallback = () => {};
if (!normalizedOptions.skipPackageJson) {
packageInstallationTask = addNgRxToPackageJson(tree);
}
if (!normalizedOptions.skipFormat) {
await formatFiles(tree);
}
return packageInstallationTask;
}
export default ngrxGenerator;

View File

@ -0,0 +1,24 @@
export interface NgRxGeneratorOptions {
directory: string;
minimal: boolean;
module: string;
name: string;
useDataPersistence: boolean;
barrels?: boolean;
facade?: boolean;
root?: boolean;
skipFormat?: boolean;
skipImport?: boolean;
skipPackageJson?: boolean;
syntax?: 'classes' | 'creators';
/**
* @deprecated use `skipImport`.
*/
onlyAddFiles?: boolean;
/**
* @deprecated use `minimal`.
*/
onlyEmptyRoot?: boolean;
}

View File

@ -1,17 +1,18 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "SchematicsNxNgrx",
"title": "Add NgRx support to an application or library",
"$id": "NxNgrxGenerator",
"title": "Add NgRx support to an application or library.",
"cli": "nx",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of the NgRx feature state, such as \"products\" or \"users\"). Recommended to use the plural form of the name.",
"description": "Name of the NgRx feature state, such as `products` or `users`. Recommended to use the plural form of the name.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the NgRx feature state? An example would be \"users\"."
"x-prompt": "What name would you like to use for the NgRx feature state? An example would be `users`."
},
"module": {
"type": "string",
@ -43,7 +44,8 @@
"onlyAddFiles": {
"type": "boolean",
"default": false,
"description": "**Deprecated**, use `skipImport`. Only add new NgRx files, without changing the module file (e.g., --onlyAddFiles)."
"description": "Only add new NgRx files, without changing the module file (e.g., --onlyAddFiles).",
"x-deprecated": "Use the `skipImport` option instead."
},
"minimal": {
"type": "boolean",
@ -53,7 +55,8 @@
"onlyEmptyRoot": {
"type": "boolean",
"default": false,
"description": "**Deprecated**, use `minimal`. Do not generate any files. Only generate StoreModule.forRoot and EffectsModule.forRoot (e.g., --onlyEmptyRoot)."
"description": "Do not generate any files. Only generate StoreModule.forRoot and EffectsModule.forRoot (e.g., --onlyEmptyRoot).",
"x-deprecated": "Use the `minimal` option instead."
},
"skipFormat": {
"description": "Skip formatting of generated files.",
@ -82,5 +85,6 @@
"description": "Use barrels to re-export actions, state, and selectors."
}
},
"required": ["module"]
"additionalProperties": false,
"required": ["module", "name"]
}

View File

@ -31,7 +31,7 @@ describe('StorybookConfiguration generator', () => {
'../../../../storybook/collection.json'
),
});
jest.resetModuleRegistry();
jest.resetModules();
jest.doMock('@storybook/angular/package.json', () => ({
version: '6.2.0',
}));

View File

@ -1 +0,0 @@
export { ngrxGenerator } from './ngrx/ngrx';

View File

@ -1,46 +0,0 @@
import { Injectable } from '@angular/core';
import { Effect, Actions <% if (!useDataPersistence) { %>,ofType<% }%> } from '@ngrx/effects';
import { <% if (useDataPersistence) { %>DataPersistence<% } else { %>fetch<% } %> } from '@nrwl/angular';
import { <%= className %>PartialState } from './<%= fileName %>.reducer';
import { Load<%= className %>, <%= className %>Loaded, <%= className %>LoadError, <%= className %>ActionTypes } from './<%= fileName %>.actions';
@Injectable()
export class <%= className %>Effects {
@Effect() load<%= className %>$ =
<% if (useDataPersistence) { %>
this.dataPersistence.fetch(<%= className %>ActionTypes.Load<%= className %>, {
run: (action: Load<%= className %>, state: <%= className %>PartialState) => {
// Your custom REST 'load' logic goes here. For now just return an empty list...
return new <%= className %>Loaded([]);
},
onError: (action: Load<%= className %>, error) => {
console.error('Error', error);
return new <%= className %>LoadError(error);
}
});
<% } else { %>
this.actions$.pipe(
ofType(<%= className %>ActionTypes.Load<%= className %>),
fetch({
run: (action: Load<%= className %>, state: <%= className %>PartialState) => {
// Your custom REST 'load' logic goes here. For now just return an empty list...
return new <%= className %>Loaded([]);
},
onError: (action: Load<%= className %>, error) => {
console.error('Error', error);
return new <%= className %>LoadError(error);
}
})
);
<% } %>
constructor(
private actions$: Actions,
<% if (useDataPersistence) { %>private dataPersistence: DataPersistence<<%= className %>PartialState><% } %>) { }
}

View File

@ -1,747 +0,0 @@
import { UnitTestTree } from '@angular-devkit/schematics/testing';
import { Tree } from '@angular-devkit/schematics';
import { readJsonInTree } from '@nrwl/workspace';
import { getFileContent } from '@nrwl/workspace/testing';
import {
AppConfig,
createApp,
createLib,
getAppConfig,
getLibConfig,
runSchematic,
} from '../../utils/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import * as path from 'path';
describe('ngrx', () => {
let appTree: Tree;
beforeEach(() => {
appTree = Tree.empty();
appTree = createEmptyWorkspace(appTree);
appTree = createApp(appTree, 'myapp');
});
it('should add empty root', async () => {
const tree = await runSchematic(
'ngrx',
{
name: 'state',
module: 'apps/myapp/src/app/app.module.ts',
onlyEmptyRoot: true,
minimal: false,
root: true,
},
appTree
);
const appModule = getFileContent(tree, '/apps/myapp/src/app/app.module.ts');
expect(
tree.exists('apps/myapp/src/app/+state/state.actions.ts')
).toBeFalsy();
expect(appModule).toContain('StoreModule.forRoot(');
expect(appModule).toContain('runtimeChecks: {');
expect(appModule).toContain('strictActionImmutability: true');
expect(appModule).toContain('strictStateImmutability: true');
expect(appModule).toContain('EffectsModule.forRoot');
});
it('should add empty root with minimal option', async () => {
const tree = await runSchematic(
'ngrx',
{
name: 'state',
module: 'apps/myapp/src/app/app.module.ts',
root: true,
onlyEmptyRoot: false,
minimal: true,
},
appTree
);
const appModule = getFileContent(tree, '/apps/myapp/src/app/app.module.ts');
expect(
tree.exists('apps/myapp/src/app/+state/state.actions.ts')
).toBeFalsy();
expect(appModule).toContain('StoreModule.forRoot(');
expect(appModule).toContain('runtimeChecks: {');
expect(appModule).toContain('strictActionImmutability: true');
expect(appModule).toContain('strictStateImmutability: true');
expect(appModule).toContain('EffectsModule.forRoot([])');
});
it('should add root', async () => {
const tree = await runSchematic(
'ngrx',
{
name: 'app',
module: 'apps/myapp/src/app/app.module.ts',
root: true,
minimal: false,
},
appTree
);
[
'/apps/myapp/src/app/+state/app.actions.ts',
'/apps/myapp/src/app/+state/app.effects.ts',
'/apps/myapp/src/app/+state/app.effects.spec.ts',
'/apps/myapp/src/app/+state/app.reducer.ts',
'/apps/myapp/src/app/+state/app.reducer.spec.ts',
'/apps/myapp/src/app/+state/app.selectors.ts',
'/apps/myapp/src/app/+state/app.selectors.spec.ts',
].forEach((fileName) => {
expect(tree.exists(fileName)).toBeTruthy();
});
// Since we did not include the `--facade` option
expect(tree.exists('/apps/myapp/src/app/+state/app.facade.ts')).toBeFalsy();
expect(
tree.exists('/apps/myapp/src/app/+state/app.facade.spec.ts')
).toBeFalsy();
const appModule = getFileContent(tree, '/apps/myapp/src/app/app.module.ts');
expect(appModule).toContain(`import { NxModule } from '@nrwl/angular';`);
expect(appModule).toContain(
`import * as fromApp from './+state/app.reducer';`
);
expect(appModule).toContain('NxModule.forRoot');
expect(appModule).toContain('StoreModule.forRoot');
expect(appModule).toContain(
`StoreModule.forFeature(fromApp.APP_FEATURE_KEY, fromApp.reducer)`
);
expect(appModule).toContain('EffectsModule.forRoot');
expect(appModule).toContain(
'metaReducers: !environment.production ? [] : []'
);
});
it('should add facade to root', async () => {
const tree = await runSchematic(
'ngrx',
{
name: 'app',
module: 'apps/myapp/src/app/app.module.ts',
root: true,
facade: true,
minimal: false,
},
appTree
);
const appModule = getFileContent(tree, '/apps/myapp/src/app/app.module.ts');
expect(appModule).toContain(`import { NxModule } from '@nrwl/angular';`);
expect(appModule).toContain('NxModule.forRoot');
expect(appModule).toContain('StoreModule.forRoot');
expect(appModule).toContain('EffectsModule.forRoot');
expect(appModule).toContain(
'metaReducers: !environment.production ? [] : []'
);
// Do not add Effects file to providers; already registered in EffectsModule
expect(appModule).toContain('providers: [AppFacade]');
[
'/apps/myapp/src/app/+state/app.actions.ts',
'/apps/myapp/src/app/+state/app.effects.ts',
'/apps/myapp/src/app/+state/app.effects.spec.ts',
'/apps/myapp/src/app/+state/app.reducer.ts',
'/apps/myapp/src/app/+state/app.reducer.spec.ts',
'/apps/myapp/src/app/+state/app.facade.ts',
'/apps/myapp/src/app/+state/app.facade.spec.ts',
'/apps/myapp/src/app/+state/app.selectors.ts',
'/apps/myapp/src/app/+state/app.selectors.spec.ts',
].forEach((fileName) => {
expect(tree.exists(fileName)).toBeTruthy();
});
});
it('should not add RouterStoreModule only if the module does not reference the router', async () => {
const newTree = createApp(appTree, 'myapp-norouter', false);
const tree = await runSchematic(
'ngrx',
{
name: 'app',
module: 'apps/myapp-norouter/src/app/app.module.ts',
root: true,
},
newTree
);
const appModule = getFileContent(
tree,
'/apps/myapp-norouter/src/app/app.module.ts'
);
expect(appModule).not.toContain('StoreRouterConnectingModule.forRoot()');
});
it('should add feature', async () => {
const tree = await runSchematic(
'ngrx',
{
name: 'state',
module: 'apps/myapp/src/app/app.module.ts',
minimal: false,
},
appTree
);
const appModule = getFileContent(tree, '/apps/myapp/src/app/app.module.ts');
expect(appModule).toContain('StoreModule.forFeature');
expect(appModule).toContain('EffectsModule.forFeature');
expect(appModule).not.toContain('!environment.production ? [] : []');
expect(
tree.exists(`/apps/myapp/src/app/+state/state.actions.ts`)
).toBeTruthy();
});
it('should add with custom directoryName', async () => {
const tree = await runSchematic(
'ngrx',
{
name: 'state',
module: 'apps/myapp/src/app/app.module.ts',
directory: 'myCustomState',
minimal: false,
},
appTree
);
const appModule = getFileContent(tree, '/apps/myapp/src/app/app.module.ts');
expect(appModule).toContain('StoreModule.forFeature');
expect(appModule).toContain('EffectsModule.forFeature');
expect(appModule).not.toContain('!environment.production ? [] : []');
expect(
tree.exists(`/apps/myapp/src/app/my-custom-state/state.actions.ts`)
).toBeTruthy();
});
it('should only add files', async () => {
const tree = await runSchematic(
'ngrx',
{
name: 'state',
module: 'apps/myapp/src/app/app.module.ts',
onlyAddFiles: true,
facade: true,
minimal: false,
},
appTree
);
const appModule = getFileContent(tree, '/apps/myapp/src/app/app.module.ts');
expect(appModule).not.toContain('StoreModule');
expect(appModule).not.toContain('!environment.production ? [] : []');
[
'/apps/myapp/src/app/+state/state.effects.ts',
'/apps/myapp/src/app/+state/state.facade.ts',
'/apps/myapp/src/app/+state/state.reducer.ts',
'/apps/myapp/src/app/+state/state.selectors.ts',
'/apps/myapp/src/app/+state/state.effects.spec.ts',
'/apps/myapp/src/app/+state/state.facade.spec.ts',
'/apps/myapp/src/app/+state/state.selectors.spec.ts',
].forEach((fileName) => {
expect(tree.exists(fileName)).toBeTruthy();
});
});
it('should only add files with skipImport option', async () => {
const tree = await runSchematic(
'ngrx',
{
name: 'state',
module: 'apps/myapp/src/app/app.module.ts',
onlyAddFiles: false,
skipImport: true,
facade: true,
minimal: false,
},
appTree
);
const appModule = getFileContent(tree, '/apps/myapp/src/app/app.module.ts');
expect(appModule).not.toContain('StoreModule');
expect(appModule).not.toContain('!environment.production ? [] : []');
[
'/apps/myapp/src/app/+state/state.effects.ts',
'/apps/myapp/src/app/+state/state.facade.ts',
'/apps/myapp/src/app/+state/state.reducer.ts',
'/apps/myapp/src/app/+state/state.selectors.ts',
'/apps/myapp/src/app/+state/state.effects.spec.ts',
'/apps/myapp/src/app/+state/state.facade.spec.ts',
'/apps/myapp/src/app/+state/state.selectors.spec.ts',
].forEach((fileName) => {
expect(tree.exists(fileName)).toBeTruthy();
});
});
it('should update package.json', async () => {
const tree = await runSchematic(
'ngrx',
{
name: 'state',
module: 'apps/myapp/src/app/app.module.ts',
},
appTree
);
const packageJson = readJsonInTree(tree, 'package.json');
expect(packageJson.dependencies['@ngrx/store']).toBeDefined();
expect(packageJson.dependencies['@ngrx/router-store']).toBeDefined();
expect(packageJson.dependencies['@ngrx/effects']).toBeDefined();
expect(packageJson.devDependencies['@ngrx/store-devtools']).toBeDefined();
});
it('should error when no module is provided', async () => {
try {
await runSchematic(
'ngrx',
{
name: 'state',
module: '',
},
appTree
);
fail();
} catch (e) {
expect(e.message).toEqual('The required --module option must be passed');
}
});
it('should error the module could not be found', async () => {
try {
await runSchematic(
'ngrx',
{
name: 'state',
module: 'does-not-exist.ts',
},
appTree
);
} catch (e) {
expect(e.message).toEqual('Path does not exist: does-not-exist.ts');
}
});
describe('code generation', () => {
it('should scaffold the ngrx "user" files without a facade', async () => {
const appConfig = getAppConfig();
const hasFile = (file) => expect(tree.exists(file)).toBeTruthy();
const missingFile = (file) => expect(tree.exists(file)).not.toBeTruthy();
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
const tree = await buildNgrxTree(appConfig);
hasFile(`${statePath}/user.actions.ts`);
hasFile(`${statePath}/user.effects.ts`);
hasFile(`${statePath}/user.effects.spec.ts`);
missingFile(`${statePath}/user.facade.ts`);
missingFile(`${statePath}/user.facade.spec.ts`);
hasFile(`${statePath}/user.reducer.ts`);
hasFile(`${statePath}/user.reducer.spec.ts`);
hasFile(`${statePath}/user.selectors.ts`);
});
it('should scaffold the ngrx "user" files WITH a facade', async () => {
const appConfig = getAppConfig();
const hasFile = (file) => expect(tree.exists(file)).toBeTruthy();
const tree = await buildNgrxTree(appConfig, 'user', true);
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
hasFile(`${statePath}/user.actions.ts`);
hasFile(`${statePath}/user.effects.ts`);
hasFile(`${statePath}/user.facade.ts`);
hasFile(`${statePath}/user.reducer.ts`);
hasFile(`${statePath}/user.selectors.ts`);
hasFile(`${statePath}/user.reducer.spec.ts`);
hasFile(`${statePath}/user.effects.spec.ts`);
hasFile(`${statePath}/user.selectors.spec.ts`);
hasFile(`${statePath}/user.facade.spec.ts`);
});
it('should build the ngrx actions', async () => {
const appConfig = getAppConfig();
const tree = await buildNgrxTree(appConfig, 'users');
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
const content = getFileContent(tree, `${statePath}/users.actions.ts`);
expect(content).toContain('UsersActionTypes');
expect(content).toContain(`LoadUsers = '[Users] Load Users'`);
expect(content).toContain(`UsersLoaded = '[Users] Users Loaded'`);
expect(content).toContain(`UsersLoadError = '[Users] Users Load Error'`);
expect(content).toContain('class LoadUsers implements Action');
expect(content).toContain('class UsersLoaded implements Action');
expect(content).toContain(
'type UsersAction = LoadUsers | UsersLoaded | UsersLoadError'
);
expect(content).toContain('export const fromUsersActions');
});
it('should build the ngrx selectors', async () => {
const appConfig = getAppConfig();
const tree = await buildNgrxTree(appConfig, 'users');
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
const content = getFileContent(tree, `${statePath}/users.selectors.ts`);
[
`import { USERS_FEATURE_KEY, UsersState } from './users.reducer'`,
`export const usersQuery`,
].forEach((text) => {
expect(content).toContain(text);
});
});
it('should build the ngrx facade', async () => {
const appConfig = getAppConfig();
const includeFacade = true;
const tree = await buildNgrxTree(appConfig, 'users', includeFacade);
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
const content = getFileContent(tree, `${statePath}/users.facade.ts`);
[
`import { UsersPartialState } from './users.reducer'`,
`import { usersQuery } from './users.selectors'`,
`export class UsersFacade`,
].forEach((text) => {
expect(content).toContain(text);
});
});
it('should build the ngrx reducer', async () => {
const appConfig = getAppConfig();
const tree = await buildNgrxTree(appConfig, 'user');
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
const content = getFileContent(tree, `${statePath}/user.reducer.ts`);
[
`import { UserAction, UserActionTypes } from \'./user.actions\'`,
`export interface User`,
`export interface UserState`,
'export function reducer',
'state: UserState = initialState',
'action: UserAction',
'): UserState',
'case UserActionTypes.UserLoaded',
].forEach((text) => {
expect(content).toContain(text);
});
});
it('should build the ngrx effects', async () => {
const appConfig = getAppConfig();
const tree = await buildNgrxTree(appConfig, 'users');
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
const content = getFileContent(tree, `${statePath}/users.effects.ts`);
[
`import { DataPersistence } from '@nrwl/angular'`,
`
import {
LoadUsers,
UsersLoaded,
UsersLoadError,
UsersActionTypes,
} from './users.actions';`,
`loadUsers$`,
`run: (action: LoadUsers, state: UsersPartialState)`,
`return new UsersLoaded([])`,
`return new UsersLoadError(error)`,
'private actions$: Actions',
'private dataPersistence: DataPersistence<UsersPartialState>',
].forEach((text) => {
expect(content).toContain(text);
});
});
});
describe('unit tests', () => {
it('should produce proper specs for the ngrx reducer', async () => {
const appConfig = getAppConfig();
const tree = await buildNgrxTree(appConfig);
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
const contents = tree.readContent(`${statePath}/user.reducer.spec.ts`);
expect(contents).toContain(`describe('User Reducer', () => {`);
expect(contents).toContain(
'const result = reducer(initialState, action);'
);
});
it('should update the barrel API with exports for ngrx facade, selector, and reducer', async () => {
appTree = createLib(appTree, 'flights');
let libConfig = getLibConfig();
let tree = await runSchematic(
'ngrx',
{
name: 'super-users',
module: libConfig.module,
facade: true,
},
appTree
);
const barrel = tree.readContent(libConfig.barrel);
expect(barrel).toContain(
`export * from './lib/+state/super-users.facade';`
);
});
it('should not update the barrel API with a facade', async () => {
appTree = createLib(appTree, 'flights');
let libConfig = getLibConfig();
let tree = await runSchematic(
'ngrx',
{
name: 'super-users',
module: libConfig.module,
facade: false,
},
appTree
);
const barrel = tree.readContent(libConfig.barrel);
expect(barrel).not.toContain(
`export * from './lib/+state/super-users.facade';`
);
});
it('should produce proper tests for the ngrx reducer for a name with a dash', async () => {
const appConfig = getAppConfig();
const tree = await runSchematic(
'ngrx',
{
name: 'super-users',
module: appConfig.appModule,
minimal: false,
},
appTree
);
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
const contents = tree.readContent(
`${statePath}/super-users.reducer.spec.ts`
);
expect(contents).toContain(`describe('SuperUsers Reducer', () => {`);
expect(contents).toContain(
`const result = reducer(initialState, action);`
);
});
});
describe('creators syntax', () => {
let appConfig = getAppConfig();
let tree: UnitTestTree;
let statePath: string;
beforeEach(async () => {
appConfig = getAppConfig();
tree = await runSchematic(
'ngrx',
{
name: 'users',
module: appConfig.appModule,
syntax: 'creators',
minimal: false,
facade: true,
useDataPersistence: false,
},
appTree
);
statePath = `${path.dirname(appConfig.appModule)}/+state`;
});
it('should generate a set of actions for the feature', async () => {
const content = tree.readContent(`${statePath}/users.actions.ts`);
[
'[Users Page] Init',
'[Users/API] Load Users Success',
'props<{ users: UsersEntity[] }>()',
'[Users/API] Load Users Failure',
'props<{ error: any }>()',
].forEach((text) => {
expect(content).toContain(text);
});
});
it('should generate a reducer for the feature', async () => {
const content = tree.readContent(`${statePath}/users.reducer.ts`);
[
`export const USERS_FEATURE_KEY = 'users';`,
`const usersReducer = createReducer`,
'export function reducer(state: State | undefined, action: Action) {',
].forEach((text) => {
expect(content).toContain(text);
});
});
it('should generate effects for the feature', async () => {
const content = tree.readContent(`${statePath}/users.effects.ts`);
[
`import { createEffect, Actions, ofType } from '@ngrx/effects';`,
'fetch({',
].forEach((text) => {
expect(content).toContain(text);
});
});
it('should generate selectors for the feature', async () => {
const content = tree.readContent(`${statePath}/users.selectors.ts`);
[
`import { USERS_FEATURE_KEY, State, usersAdapter } from './users.reducer';`,
'const { selectAll, selectEntities } = usersAdapter.getSelectors();',
].forEach((text) => {
expect(content).toContain(text);
});
});
it('should generate a facade for the feature if enabled', async () => {
const content = tree.readContent(`${statePath}/users.facade.ts`);
[
`loaded$ = this.store.pipe(select(UsersSelectors.getUsersLoaded));`,
`allUsers$ = this.store.pipe(select(UsersSelectors.getAllUsers));`,
`selectedUsers$ = this.store.pipe(select(UsersSelectors.getSelected));`,
].forEach((text) => {
expect(content).toContain(text);
});
});
it('should generate a models file for the feature', async () => {
const content = tree.readContent(`${statePath}/users.models.ts`);
[
'export interface UsersEntity',
'id: string | number; // Primary ID',
].forEach((text) => {
expect(content).toContain(text);
});
});
it('should use DataPersistence operators when useDataPersistence is set to false', async () => {
appTree = Tree.empty();
appTree = createEmptyWorkspace(appTree);
appTree = createApp(appTree, 'myapp');
const tree = await runSchematic(
'ngrx',
{
name: 'users',
module: appConfig.appModule,
syntax: 'creators',
facade: true,
minimal: false,
},
appTree
);
const content = tree.readContent(`${statePath}/users.effects.ts`);
[`{ fetch }`, `, ofType`, `ofType(UsersActions.init),`].forEach(
(text) => {
expect(content).toContain(text);
}
);
expect(content).not.toContain('dataPersistence.fetch');
});
it('should re-export actions, state, and selectors using barrels if enabled', async () => {
appTree = Tree.empty();
appTree = createEmptyWorkspace(appTree);
appTree = createApp(appTree, 'myapp');
appTree.create('/apps/myapp/src/index.ts', '');
const tree = await runSchematic(
'ngrx',
{
name: 'users',
module: appConfig.appModule,
syntax: 'creators',
barrels: true,
},
appTree
);
const content = tree.readContent('/apps/myapp/src/index.ts');
[
`import * as UsersActions from './lib/+state/users.actions';`,
`import * as UsersFeature from './lib/+state/users.reducer';`,
`import * as UsersSelectors from './lib/+state/users.selectors';`,
`export { UsersActions, UsersFeature, UsersSelectors };`,
`export * from './lib/+state/users.models';`,
].forEach((text) => {
expect(content).toContain(text);
});
});
});
describe('classes syntax', () => {
it('should use fetch operator when useDataPersistence is set to false', async () => {
const appConfig = getAppConfig();
const tree = await runSchematic(
'ngrx',
{
name: 'users',
module: appConfig.appModule,
syntax: 'classes',
minimal: false,
facade: true,
useDataPersistence: false,
},
appTree
);
const statePath = `${path.dirname(appConfig.appModule)}/+state`;
const content = tree.readContent(`${statePath}/users.effects.ts`);
[`{ fetch }`, `, ofType`, `ofType(UsersActionTypes.LoadUsers),`].forEach(
(text) => {
expect(content).toContain(text);
}
);
expect(content).not.toContain('dataPersistence.fetch');
});
});
async function buildNgrxTree(
appConfig: AppConfig,
featureName: string = 'user',
withFacade = false,
useDataPersistence = true
): Promise<UnitTestTree> {
return await runSchematic(
'ngrx',
{
name: featureName,
module: appConfig.appModule,
facade: withFacade,
syntax: 'classes',
minimal: false,
useDataPersistence,
},
appTree
);
}
});

View File

@ -1,119 +0,0 @@
import {
apply,
chain,
url,
mergeWith,
template,
move,
noop,
filter,
Rule,
Tree,
SchematicContext,
} from '@angular-devkit/schematics';
import { Schema } from './schema';
import * as path from 'path';
import {
addImportsToModule,
addNgRxToPackageJson,
addExportsToBarrel,
RequestContext,
} from './rules';
import { formatFiles } from '@nrwl/workspace';
import { names } from '@nrwl/devkit';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
/**
* Rule to generate the Nx 'ngrx' Collection
* Note: see https://nx.dev/latest/angular/guides/misc-ngrx for guide to generated files
*/
export default function generateNgrxCollection(_options: Schema): Rule {
return (host: Tree, context: SchematicContext) => {
const options = normalizeOptions(_options);
if (!options.module) {
throw new Error(`The required --module option must be passed`);
} else if (!host.exists(options.module)) {
throw new Error(`Path does not exist: ${options.module}`);
}
const requestContext: RequestContext = {
featureName: options.name,
moduleDir: path.dirname(options.module),
options,
host,
};
if (options.minimal) {
options.onlyEmptyRoot = true;
}
if (options.skipImport) {
options.onlyAddFiles = true;
}
const fileGeneration =
!options.onlyEmptyRoot || (!options.root && options.minimal)
? [generateNgrxFilesFromTemplates(options)]
: [];
const moduleModification = !options.onlyAddFiles
? [
addImportsToModule(requestContext),
addExportsToBarrel(requestContext.options),
]
: [];
const packageJsonModification = !options.skipPackageJson
? [addNgRxToPackageJson()]
: [];
return chain([
...fileGeneration,
...moduleModification,
...packageJsonModification,
formatFiles(options),
])(host, context);
};
}
// ********************************************************
// Internal Function
// ********************************************************
/**
* Generate 'feature' scaffolding: actions, reducer, effects, interfaces, selectors, facade
*/
function generateNgrxFilesFromTemplates(options: Schema) {
const name = options.name;
const moduleDir = path.dirname(options.module);
const excludeFacade = (path) => path.match(/^((?!facade).)*$/);
const templateSource = apply(
url(options.syntax === 'creators' ? './creator-files' : './files'),
[
!options.facade ? filter(excludeFacade) : noop(),
template({ ...options, tmpl: '', ...names(name) }),
move(moduleDir),
]
);
return mergeWith(templateSource);
}
/**
* Extract the parent 'directory' for the specified
*/
function normalizeOptions(options: Schema): Schema {
return {
...options,
directory: names(options.directory).fileName,
};
}
export const ngrxGenerator = wrapAngularDevkitSchematic(
'@nrwl/angular',
'ngrx'
);

View File

@ -1,93 +0,0 @@
import * as ts from 'typescript';
import * as path from 'path';
import { Rule, Tree } from '@angular-devkit/schematics';
import { insert, addGlobal } from '@nrwl/workspace';
import { Schema } from '../schema';
import { names } from '@nrwl/devkit';
/**
* Add ngrx feature exports to the public barrel in the feature library
*/
export function addExportsToBarrel(options: Schema): Rule {
return (host: Tree) => {
if (!host.exists(options.module)) {
throw new Error(
`Specified module path (${options.module}) does not exist`
);
}
// Only update the public barrel for feature libraries
if (options.root != true) {
const moduleDir = path.dirname(options.module);
const indexFilePath = path.join(moduleDir, '../index.ts');
const hasFacade = options.facade == true;
const addModels = options.syntax === 'creators';
const className = `${names(options.name).className}`;
const exportBarrels = options.barrels === true;
const buffer = host.read(indexFilePath);
if (!!buffer) {
// AST to 'index.ts' barrel for the public API
const indexSource = buffer!.toString('utf-8');
const indexSourceFile = ts.createSourceFile(
indexFilePath,
indexSource,
ts.ScriptTarget.Latest,
true
);
// Public API for the feature interfaces, selectors, and facade
const { fileName } = names(options.name);
const statePath = `./lib/${options.directory}/${fileName}`;
insert(host, indexFilePath, [
...addGlobal(
indexSourceFile,
indexFilePath,
exportBarrels
? `import * as ${className}Actions from '${statePath}.actions';`
: `export * from '${statePath}.actions';`
),
...addGlobal(
indexSourceFile,
indexFilePath,
exportBarrels
? `import * as ${className}Feature from '${statePath}.reducer';`
: `export * from '${statePath}.reducer';`
),
...addGlobal(
indexSourceFile,
indexFilePath,
exportBarrels
? `import * as ${className}Selectors from '${statePath}.selectors';`
: `export * from '${statePath}.selectors';`
),
...(exportBarrels
? addGlobal(
indexSourceFile,
indexFilePath,
`export { ${className}Actions, ${className}Feature, ${className}Selectors };`
)
: []),
...(addModels
? addGlobal(
indexSourceFile,
indexFilePath,
`export * from '${statePath}.models';`
)
: []),
...(hasFacade
? addGlobal(
indexSourceFile,
indexFilePath,
`export * from '${statePath}.facade';`
)
: []),
]);
}
}
return host;
};
}

View File

@ -1,133 +0,0 @@
import { Rule, Tree } from '@angular-devkit/schematics';
import * as ts from 'typescript';
import { insert } from '@nrwl/workspace';
import { RequestContext } from './request-context';
import {
addImportToModule,
addProviderToModule,
} from '../../../utils/ast-utils';
import { Change, insertImport } from '@nrwl/workspace/src/utils/ast-utils';
import { names } from '@nrwl/devkit';
export function addImportsToModule(context: RequestContext): Rule {
return (host: Tree) => {
const modulePath = context.options.module;
const sourceText = host.read(modulePath)!.toString('utf-8');
const source = ts.createSourceFile(
modulePath,
sourceText,
ts.ScriptTarget.Latest,
true
);
const addImport = (
symbolName: string,
fileName: string,
isDefault = false
): Change => {
return insertImport(source, modulePath, symbolName, fileName, isDefault);
};
const dir = `./${names(context.options.directory).fileName}`;
const pathPrefix = `${dir}/${names(context.featureName).fileName}`;
const reducerPath = `${pathPrefix}.reducer`;
const effectsPath = `${pathPrefix}.effects`;
const facadePath = `${pathPrefix}.facade`;
const constantName = `${names(context.featureName).constantName}`;
const effectsName = `${names(context.featureName).className}Effects`;
const facadeName = `${names(context.featureName).className}Facade`;
const className = `${names(context.featureName).className}`;
const reducerImports = `* as from${className}`;
const storeMetaReducers = `metaReducers: !environment.production ? [] : []`;
const storeForRoot = `StoreModule.forRoot({},
{
${storeMetaReducers},
runtimeChecks: {
strictActionImmutability: true,
strictStateImmutability: true
}
}
)`;
const nxModule = 'NxModule.forRoot()';
const effectsForRoot = `EffectsModule.forRoot([${effectsName}])`;
const effectsForEmptyRoot = `EffectsModule.forRoot([])`;
const storeForFeature = `StoreModule.forFeature(from${className}.${constantName}_FEATURE_KEY, from${className}.reducer)`;
const effectsForFeature = `EffectsModule.forFeature([${effectsName}])`;
const devTools = `!environment.production ? StoreDevtoolsModule.instrument() : []`;
const storeRouterModule = 'StoreRouterConnectingModule.forRoot()';
// InsertImport [symbol,source] value pairs
const nxModuleImport = ['NxModule', '@nrwl/angular'];
const storeModule = ['StoreModule', '@ngrx/store'];
const effectsModule = ['EffectsModule', '@ngrx/effects'];
const storeDevTools = ['StoreDevtoolsModule', '@ngrx/store-devtools'];
const environment = ['environment', '../environments/environment'];
const storeRouter = ['StoreRouterConnectingModule', '@ngrx/router-store'];
// this is just a heuristic
const hasRouter = sourceText.indexOf('RouterModule') > -1;
const hasNxModule = sourceText.includes('NxModule.forRoot()');
if (
(context.options.onlyEmptyRoot || context.options.minimal) &&
context.options.root
) {
insert(host, modulePath, [
addImport.apply(this, storeModule),
addImport.apply(this, effectsModule),
addImport.apply(this, storeDevTools),
addImport.apply(this, environment),
...(hasRouter ? [addImport.apply(this, storeRouter)] : []),
...addImportToModule(source, modulePath, storeForRoot),
...addImportToModule(source, modulePath, effectsForEmptyRoot),
...addImportToModule(source, modulePath, devTools),
...(hasRouter
? addImportToModule(source, modulePath, storeRouterModule)
: []),
]);
} else {
let common = [
addImport.apply(this, storeModule),
addImport.apply(this, effectsModule),
addImport(reducerImports, reducerPath, true),
addImport(effectsName, effectsPath),
];
if (context.options.facade) {
common = [
...common,
addImport(facadeName, facadePath),
...addProviderToModule(source, modulePath, `${facadeName}`),
];
}
if (context.options.root) {
insert(host, modulePath, [
...common,
...(!hasNxModule ? [addImport.apply(this, nxModuleImport)] : []),
addImport.apply(this, storeDevTools),
addImport.apply(this, environment),
...(hasRouter ? [addImport.apply(this, storeRouter)] : []),
...(!hasNxModule
? addImportToModule(source, modulePath, nxModule)
: []),
...addImportToModule(source, modulePath, storeForRoot),
...addImportToModule(source, modulePath, effectsForRoot),
...addImportToModule(source, modulePath, devTools),
...(hasRouter
? addImportToModule(source, modulePath, storeRouterModule)
: []),
...addImportToModule(source, modulePath, storeForFeature),
]);
} else {
insert(host, modulePath, [
...common,
...addImportToModule(source, modulePath, storeForFeature),
...addImportToModule(source, modulePath, effectsForFeature),
]);
}
}
return host;
};
}

View File

@ -1,23 +0,0 @@
import * as path from 'path';
import { Tree } from '@angular-devkit/schematics';
import { Schema } from '../schema';
import { stringUtils } from '@nrwl/workspace';
/**
* Schematic request context
*/
export interface RequestContext {
featureName: string;
moduleDir: string;
options?: Schema;
host?: Tree;
}
export function buildNameToNgrxFile(context: RequestContext, suffice: string) {
return path.join(
context.moduleDir,
context.options.directory,
`${stringUtils.dasherize(context.featureName)}.${suffice}`
);
}

View File

@ -1,23 +0,0 @@
export interface Schema {
name: string;
module: string;
directory: string;
root: boolean;
facade: boolean;
minimal: boolean;
skipImport: boolean;
/**
* @deprecated use `minimal`
*/
onlyEmptyRoot: boolean;
/**
* @deprecated use `skipImport`
*/
onlyAddFiles: boolean;
skipFormat: boolean;
skipPackageJson: boolean;
syntax?: string;
useDataPersistence: boolean;
barrels: boolean;
}

View File

@ -8,6 +8,7 @@ import {
pathFormat,
} from '@angular-devkit/schematics/src/formats';
import {
formatDeprecated,
generateJsonFile,
generateMarkdownFile,
sortAlphabeticallyFunction,
@ -136,9 +137,9 @@ function generateTemplate(
: ``;
template += dedent`
### ${option.name} ${option.required ? '(*__required__*)' : ''} ${
option.hidden ? '(__hidden__)' : ''
}
### ${option.deprecated ? `~~${option.name}~~` : option.name} ${
option.required ? '(*__required__*)' : ''
} ${option.hidden ? '(__hidden__)' : ''}
${
!!option.aliases.length
@ -151,10 +152,12 @@ function generateTemplate(
: `Default: \`${option.default}\`\n`
}
Type: \`${option.type}\`
`;
template += dedent`
${enumStr}
${option.description}
${formatDeprecated(option.description, option.deprecated)}
`;
});
}