diff --git a/docs/angular/api-angular/generators/ngrx.md b/docs/angular/api-angular/generators/ngrx.md index 2b80efe29e..f4a49fdd4c 100644 --- a/docs/angular/api-angular/generators/ngrx.md +++ b/docs/angular/api-angular/generators/ngrx.md @@ -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 diff --git a/docs/node/api-angular/generators/ngrx.md b/docs/node/api-angular/generators/ngrx.md index 9d4a0f23dc..7995a15629 100644 --- a/docs/node/api-angular/generators/ngrx.md +++ b/docs/node/api-angular/generators/ngrx.md @@ -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 diff --git a/docs/react/api-angular/generators/ngrx.md b/docs/react/api-angular/generators/ngrx.md index 9d4a0f23dc..7995a15629 100644 --- a/docs/react/api-angular/generators/ngrx.md +++ b/docs/react/api-angular/generators/ngrx.md @@ -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 diff --git a/packages/angular/collection.json b/packages/angular/collection.json index a3d2ca8370..37eeb35ce5 100644 --- a/packages/angular/collection.json +++ b/packages/angular/collection.json @@ -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", diff --git a/packages/angular/generators.ts b/packages/angular/generators.ts index b46d365690..ebcdf30c96 100644 --- a/packages/angular/generators.ts +++ b/packages/angular/generators.ts @@ -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'; diff --git a/packages/angular/src/generators/ngrx/__snapshots__/ngrx.classes.spec.ts.snap b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.classes.spec.ts.snap new file mode 100644 index 0000000000..9dc6669b64 --- /dev/null +++ b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.classes.spec.ts.snap @@ -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) {} + + 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(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 + ) {} +} +" +`; + +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; + 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; + 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; + 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); + }); + }); +}); +" +`; diff --git a/packages/angular/src/generators/ngrx/__snapshots__/ngrx.creators.spec.ts.snap b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.creators.spec.ts.snap new file mode 100644 index 0000000000..a68264ad12 --- /dev/null +++ b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.creators.spec.ts.snap @@ -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 { + 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 = createEntityAdapter(); + +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(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, state: UsersFeature.UsersPartialState) => { + // Your custom service 'load' logic goes here. For now just return a success action... + return UsersActions.loadUsersSuccess({ users: [] }); + }, + onError: (action: ReturnType, error) => { + console.error('Error', error); + return UsersActions.loadUsersFailure({ error }); + } + })); + + constructor( + private readonly actions$: Actions, + private readonly dataPersistence: DataPersistence + ) {} +} +" +`; + +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; + 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; + 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; + 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); + }); + }); +}); +" +`; diff --git a/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap new file mode 100644 index 0000000000..6cdec3bcc4 --- /dev/null +++ b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap @@ -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 {} + " +`; diff --git a/packages/angular/src/generators/ngrx/compat.ts b/packages/angular/src/generators/ngrx/compat.ts new file mode 100644 index 0000000000..7e7ba07137 --- /dev/null +++ b/packages/angular/src/generators/ngrx/compat.ts @@ -0,0 +1,4 @@ +import { convertNxGenerator } from '@nrwl/devkit'; +import { ngrxGenerator } from './ngrx'; + +export default convertNxGenerator(ngrxGenerator); diff --git a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.actions.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.actions.ts__tmpl__ similarity index 51% rename from packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.actions.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.actions.ts__tmpl__ index dcdc7da26f..3eb240aed1 100644 --- a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.actions.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.actions.ts__tmpl__ @@ -2,23 +2,25 @@ import {Action} from '@ngrx/store'; import {Entity} from './<%= fileName %>.reducer'; export enum <%= className %>ActionTypes { - Load<%= className %> = '[<%= className %>] Load <%= className %>', - <%= className %>Loaded = '[<%= className %>] <%= className %> Loaded', - <%= className %>LoadError = '[<%= className %>] <%= className %> Load Error' + Load<%= className %> = '[<%= className %>] Load <%= className %>', + <%= className %>Loaded = '[<%= className %>] <%= className %> Loaded', + <%= className %>LoadError = '[<%= className %>] <%= className %> Load Error' } export class Load<%= className %> implements Action { - readonly type = <%= className %>ActionTypes.Load<%= className %>; + readonly type = <%= className %>ActionTypes.Load<%= className %>; } export class <%= className %>LoadError implements Action { - readonly type = <%= className %>ActionTypes.<%= className %>LoadError; - constructor(public payload: any) { } + 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[]) { } + readonly type = <%= className %>ActionTypes.<%= className %>Loaded; + + constructor(public payload: Entity[]) {} } export type <%= className %>Action = Load<%= className %> | <%= className %>Loaded | <%= className %>LoadError; diff --git a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.effects.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.effects.spec.ts__tmpl__ similarity index 83% rename from packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.effects.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.effects.spec.ts__tmpl__ index ca61b517b2..b11fe8135b 100644 --- a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.effects.spec.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.effects.spec.ts__tmpl__ @@ -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) ], }); @@ -35,9 +32,10 @@ describe('<%= className %>Effects', () => { describe('load<%= className %>$', () => { it('should work', () => { - actions = hot('-a-|', {a: new Load<%= className %>()}); + actions = hot('-a-|', { a: new Load<%= className %>() }); + expect(effects.load<%= className %>$).toBeObservable( - hot('-a-|', {a: new <%= className %>Loaded([])}) + hot('-a-|', { a: new <%= className %>Loaded([]) }) ); }); }); diff --git a/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.effects.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.effects.ts__tmpl__ new file mode 100644 index 0000000000..61b78b2c35 --- /dev/null +++ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.effects.ts__tmpl__ @@ -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><% } %> + ) {} +} diff --git a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.facade.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.facade.spec.ts__tmpl__ similarity index 98% rename from packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.facade.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.facade.spec.ts__tmpl__ index da56073099..6ce1cc5044 100644 --- a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.facade.spec.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.facade.spec.ts__tmpl__ @@ -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', () => { diff --git a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.facade.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.facade.ts__tmpl__ similarity index 90% rename from packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.facade.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.facade.ts__tmpl__ index b8a87461a4..e1332ef1e9 100644 --- a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.facade.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.facade.ts__tmpl__ @@ -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 %>()); diff --git a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.reducer.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.reducer.spec.ts__tmpl__ similarity index 62% rename from packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.reducer.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.reducer.spec.ts__tmpl__ index b485123f8d..9ebd14bf2f 100644 --- a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.reducer.spec.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.reducer.spec.ts__tmpl__ @@ -1,5 +1,3 @@ -import { Action } from '@ngrx/store'; - import { <%= className %>Loaded } from './<%= fileName %>.actions'; import { <%= className %>State, Entity, initialState, reducer } from './<%= fileName %>.reducer'; @@ -10,12 +8,12 @@ describe('<%= className %> Reducer', () => { name: name || `name-${id}` }); - 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); - const result : <%= className %>State = reducer(initialState, action); - const selId : string = get<%= className %>Id(result.list[1]); + describe('valid <%= className %> actions', () => { + 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]); expect(result.loaded).toBe(true); expect(result.list.length).toBe(2); @@ -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); diff --git a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.reducer.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.reducer.ts__tmpl__ similarity index 100% rename from packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.reducer.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.reducer.ts__tmpl__ diff --git a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.selectors.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.selectors.spec.ts__tmpl__ similarity index 83% rename from packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.selectors.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.selectors.spec.ts__tmpl__ index 90ebb43fc8..35988db151 100644 --- a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.selectors.spec.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.selectors.spec.ts__tmpl__ @@ -8,11 +8,11 @@ describe('<%= className %> Selectors', () => { let storeState: <%= className %>PartialState; beforeEach(() => { - const create<%= className %> = (id: string, name = ''): Entity => ({ - id, - name: name || `name-${id}` - }); - storeState = { + const create<%= className %> = (id: string, name = ''): Entity => ({ + id, + name: name || `name-${id}` + }); + storeState = { <%= propertyName %>: { list : [ create<%= className %>('PRODUCT-AAA'), @@ -22,8 +22,8 @@ describe('<%= className %> Selectors', () => { selectedId: 'PRODUCT-BBB', error: ERROR_MSG, loaded: true - } - }; + } + }; }); describe('<%= className %> Selectors', () => { @@ -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); diff --git a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.selectors.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.selectors.ts__tmpl__ similarity index 89% rename from packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.selectors.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.selectors.ts__tmpl__ index 1e6b0b599b..39369fd37d 100644 --- a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.selectors.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/classes-syntax/__directory__/__fileName__.selectors.ts__tmpl__ @@ -12,8 +12,8 @@ const getAll<%= className %> = createSelector( get<%= className %>State, getLoad }); const getSelectedId = createSelector( get<%= className %>State, (state:<%= className %>State) => state.selectedId ); const getSelected<%= className %> = createSelector( getAll<%= className %>, getSelectedId, (<%= propertyName %>, id) => { - const result = <%= propertyName %>.find(it => it['id'] === id); - return result ? Object.assign({}, result) : undefined; + const result = <%= propertyName %>.find(it => it['id'] === id); + return result ? Object.assign({}, result) : undefined; }); export const <%= propertyName %>Query = { diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.actions.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.actions.ts__tmpl__ similarity index 100% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.actions.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.actions.ts__tmpl__ diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.effects.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.effects.spec.ts__tmpl__ similarity index 75% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.effects.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.effects.spec.ts__tmpl__ index 8485be9388..637e544d7c 100644 --- a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.effects.spec.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.effects.spec.ts__tmpl__ @@ -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; + let actions: Observable; 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() ], @@ -33,7 +31,7 @@ describe('<%= className %>Effects', () => { describe('init$', () => { it('should work', () => { - actions = hot('-a-|', {a: <%= className %>Actions.init()}); + actions = hot('-a-|', { a: <%= className %>Actions.init() }); const expected = hot('-a-|', { a: <%= className %>Actions.load<%= className %>Success({ <%= propertyName %>: [] }) }); diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.effects.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.effects.ts__tmpl__ similarity index 53% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.effects.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.effects.ts__tmpl__ index 0a089499eb..af4fcd04ce 100644 --- a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.effects.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.effects.ts__tmpl__ @@ -2,40 +2,36 @@ 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 { -<% if (useDataPersistence) { %> init$ = createEffect(() => this.dataPersistence.fetch(<%= className %>Actions.init, { + <% if (useDataPersistence) { %>init$ = createEffect(() => this.dataPersistence.fetch(<%= className %>Actions.init, { run: (action: ReturnTypeActions.init>, state: <%= className %>Feature.<%= className %>PartialState) => { // Your custom service 'load' logic goes here. For now just return a success action... return <%= className %>Actions.load<%= className %>Success({ <%= propertyName %>: [] }); }, - onError: (action: ReturnTypeActions.init>, error) => { console.error('Error', error); return <%= className %>Actions.load<%= className %>Failure({ error }); } - })); -<% } 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 %>: [] }); - }, + }));<% } 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 }); + } + }) + ));<% } %> - 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>, <% } %> - ) { } + constructor( + private readonly actions$: Actions<% if (useDataPersistence) { %>, + private readonly dataPersistence: DataPersistence<<%= className %>Feature.<%= className %>PartialState><% } %> + ) {} } diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.facade.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.facade.spec.ts__tmpl__ similarity index 98% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.facade.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.facade.spec.ts__tmpl__ index 84b8aa7389..4ff8df8fa0 100644 --- a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.facade.spec.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.facade.spec.ts__tmpl__ @@ -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', () => { diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.facade.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.facade.ts__tmpl__ similarity index 95% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.facade.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.facade.ts__tmpl__ index dd52d0b3de..5ac3918bd8 100644 --- a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.facade.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.facade.ts__tmpl__ @@ -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 diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.models.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.models.ts__tmpl__ similarity index 74% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.models.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.models.ts__tmpl__ index fcdfc4ad1e..225df94d2c 100644 --- a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.models.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.models.ts__tmpl__ @@ -2,6 +2,6 @@ * Interface for the '<%= className %>' data */ export interface <%= className %>Entity { - id: string | number; // Primary ID + id: string | number; // Primary ID name: string; }; \ No newline at end of file diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.reducer.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.reducer.spec.ts__tmpl__ similarity index 77% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.reducer.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.reducer.spec.ts__tmpl__ index 6f32eced11..45ba538dca 100644 --- a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.reducer.spec.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.reducer.spec.ts__tmpl__ @@ -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,10 +11,10 @@ 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' ) + create<%= className %>Entity('PRODUCT-AAA'), + create<%= className %>Entity('PRODUCT-zzz') ]; const action = <%= className %>Actions.load<%= className %>Success({ <%= propertyName %> }); @@ -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); diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.reducer.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.reducer.ts__tmpl__ similarity index 100% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.reducer.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.reducer.ts__tmpl__ index 1d38a346d8..ac721b11c0 100644 --- a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.reducer.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.reducer.ts__tmpl__ @@ -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'; diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.selectors.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.selectors.spec.ts__tmpl__ similarity index 91% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.selectors.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.selectors.spec.ts__tmpl__ index dc90037824..6bea8babe7 100644 --- a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.selectors.spec.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.selectors.spec.ts__tmpl__ @@ -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); diff --git a/packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.selectors.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.selectors.ts__tmpl__ similarity index 100% rename from packages/angular/src/schematics/ngrx/creator-files/__directory__/__fileName__.selectors.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/creator-syntax/__directory__/__fileName__.selectors.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/lib/add-exports-barrel.ts b/packages/angular/src/generators/ngrx/lib/add-exports-barrel.ts new file mode 100644 index 0000000000..1d48fc5fef --- /dev/null +++ b/packages/angular/src/generators/ngrx/lib/add-exports-barrel.ts @@ -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';` + ); + } +} diff --git a/packages/angular/src/generators/ngrx/lib/add-imports-to-module.ts b/packages/angular/src/generators/ngrx/lib/add-imports-to-module.ts new file mode 100644 index 0000000000..c75b0941ff --- /dev/null +++ b/packages/angular/src/generators/ngrx/lib/add-imports-to-module.ts @@ -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 + ); + } + } +} diff --git a/packages/angular/src/schematics/ngrx/rules/add-ngrx-to-package-json.ts b/packages/angular/src/generators/ngrx/lib/add-ngrx-to-package-json.ts similarity index 59% rename from packages/angular/src/schematics/ngrx/rules/add-ngrx-to-package-json.ts rename to packages/angular/src/generators/ngrx/lib/add-ngrx-to-package-json.ts index 8fbb444781..1deab53904 100644 --- a/packages/angular/src/schematics/ngrx/rules/add-ngrx-to-package-json.ts +++ b/packages/angular/src/generators/ngrx/lib/add-ngrx-to-package-json.ts @@ -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, diff --git a/packages/angular/src/generators/ngrx/lib/generate-files.ts b/packages/angular/src/generators/ngrx/lib/generate-files.ts new file mode 100644 index 0000000000..fd6adfe0c5 --- /dev/null +++ b/packages/angular/src/generators/ngrx/lib/generate-files.ts @@ -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` + ) + ); + } +} diff --git a/packages/angular/src/schematics/ngrx/rules/index.ts b/packages/angular/src/generators/ngrx/lib/index.ts similarity index 60% rename from packages/angular/src/schematics/ngrx/rules/index.ts rename to packages/angular/src/generators/ngrx/lib/index.ts index 231f515d25..53f0de87c3 100644 --- a/packages/angular/src/schematics/ngrx/rules/index.ts +++ b/packages/angular/src/generators/ngrx/lib/index.ts @@ -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'; diff --git a/packages/angular/src/generators/ngrx/lib/normalize-options.ts b/packages/angular/src/generators/ngrx/lib/normalize-options.ts new file mode 100644 index 0000000000..95801bdfd4 --- /dev/null +++ b/packages/angular/src/generators/ngrx/lib/normalize-options.ts @@ -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; +} diff --git a/packages/angular/src/generators/ngrx/ngrx.classes.spec.ts b/packages/angular/src/generators/ngrx/ngrx.classes.spec.ts new file mode 100644 index 0000000000..14d2b05dc7 --- /dev/null +++ b/packages/angular/src/generators/ngrx/ngrx.classes.spec.ts @@ -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(); + }); + }); + }); +}); diff --git a/packages/angular/src/generators/ngrx/ngrx.creators.spec.ts b/packages/angular/src/generators/ngrx/ngrx.creators.spec.ts new file mode 100644 index 0000000000..538cc51864 --- /dev/null +++ b/packages/angular/src/generators/ngrx/ngrx.creators.spec.ts @@ -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(); + }); + }); + }); +}); diff --git a/packages/angular/src/generators/ngrx/ngrx.spec.ts b/packages/angular/src/generators/ngrx/ngrx.spec.ts new file mode 100644 index 0000000000..3e34682e87 --- /dev/null +++ b/packages/angular/src/generators/ngrx/ngrx.spec.ts @@ -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(); + }); +}); diff --git a/packages/angular/src/generators/ngrx/ngrx.ts b/packages/angular/src/generators/ngrx/ngrx.ts new file mode 100644 index 0000000000..c733b85aed --- /dev/null +++ b/packages/angular/src/generators/ngrx/ngrx.ts @@ -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 { + 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; diff --git a/packages/angular/src/generators/ngrx/schema.d.ts b/packages/angular/src/generators/ngrx/schema.d.ts new file mode 100644 index 0000000000..b5943c99e3 --- /dev/null +++ b/packages/angular/src/generators/ngrx/schema.d.ts @@ -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; +} diff --git a/packages/angular/src/schematics/ngrx/schema.json b/packages/angular/src/generators/ngrx/schema.json similarity index 78% rename from packages/angular/src/schematics/ngrx/schema.json rename to packages/angular/src/generators/ngrx/schema.json index d3f35d147a..fe60a23368 100644 --- a/packages/angular/src/schematics/ngrx/schema.json +++ b/packages/angular/src/generators/ngrx/schema.json @@ -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"] } diff --git a/packages/angular/src/generators/storybook-configuration/storybook-configuration.spec.ts b/packages/angular/src/generators/storybook-configuration/storybook-configuration.spec.ts index ff596124ac..f524b89afb 100644 --- a/packages/angular/src/generators/storybook-configuration/storybook-configuration.spec.ts +++ b/packages/angular/src/generators/storybook-configuration/storybook-configuration.spec.ts @@ -31,7 +31,7 @@ describe('StorybookConfiguration generator', () => { '../../../../storybook/collection.json' ), }); - jest.resetModuleRegistry(); + jest.resetModules(); jest.doMock('@storybook/angular/package.json', () => ({ version: '6.2.0', })); diff --git a/packages/angular/src/schematics/generators.ts b/packages/angular/src/schematics/generators.ts deleted file mode 100644 index 7f7851b0fd..0000000000 --- a/packages/angular/src/schematics/generators.ts +++ /dev/null @@ -1 +0,0 @@ -export { ngrxGenerator } from './ngrx/ngrx'; diff --git a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.effects.ts__tmpl__ b/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.effects.ts__tmpl__ deleted file mode 100644 index 6926e378d9..0000000000 --- a/packages/angular/src/schematics/ngrx/files/__directory__/__fileName__.effects.ts__tmpl__ +++ /dev/null @@ -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><% } %>) { } -} diff --git a/packages/angular/src/schematics/ngrx/ngrx.spec.ts b/packages/angular/src/schematics/ngrx/ngrx.spec.ts deleted file mode 100644 index a5b6db1ff2..0000000000 --- a/packages/angular/src/schematics/ngrx/ngrx.spec.ts +++ /dev/null @@ -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', - ].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 { - return await runSchematic( - 'ngrx', - { - name: featureName, - module: appConfig.appModule, - facade: withFacade, - syntax: 'classes', - minimal: false, - useDataPersistence, - }, - appTree - ); - } -}); diff --git a/packages/angular/src/schematics/ngrx/ngrx.ts b/packages/angular/src/schematics/ngrx/ngrx.ts deleted file mode 100644 index 583bda608a..0000000000 --- a/packages/angular/src/schematics/ngrx/ngrx.ts +++ /dev/null @@ -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' -); diff --git a/packages/angular/src/schematics/ngrx/rules/add-exports-barrel.ts b/packages/angular/src/schematics/ngrx/rules/add-exports-barrel.ts deleted file mode 100644 index 24cf081333..0000000000 --- a/packages/angular/src/schematics/ngrx/rules/add-exports-barrel.ts +++ /dev/null @@ -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; - }; -} diff --git a/packages/angular/src/schematics/ngrx/rules/add-imports-to-module.ts b/packages/angular/src/schematics/ngrx/rules/add-imports-to-module.ts deleted file mode 100644 index 36a6057dba..0000000000 --- a/packages/angular/src/schematics/ngrx/rules/add-imports-to-module.ts +++ /dev/null @@ -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; - }; -} diff --git a/packages/angular/src/schematics/ngrx/rules/request-context.ts b/packages/angular/src/schematics/ngrx/rules/request-context.ts deleted file mode 100644 index dce79c0ac6..0000000000 --- a/packages/angular/src/schematics/ngrx/rules/request-context.ts +++ /dev/null @@ -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}` - ); -} diff --git a/packages/angular/src/schematics/ngrx/schema.d.ts b/packages/angular/src/schematics/ngrx/schema.d.ts deleted file mode 100644 index ad7103864b..0000000000 --- a/packages/angular/src/schematics/ngrx/schema.d.ts +++ /dev/null @@ -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; -} diff --git a/scripts/documentation/generate-generators-data.ts b/scripts/documentation/generate-generators-data.ts index 1278ef0b96..6cb46927fd 100644 --- a/scripts/documentation/generate-generators-data.ts +++ b/scripts/documentation/generate-generators-data.ts @@ -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,11 +152,13 @@ function generateTemplate( : `Default: \`${option.default}\`\n` } Type: \`${option.type}\` - - ${enumStr} - - ${option.description} `; + + template += dedent` + ${enumStr} + + ${formatDeprecated(option.description, option.deprecated)} + `; }); }