diff --git a/docs/generated/packages/angular/generators/ngrx.json b/docs/generated/packages/angular/generators/ngrx.json index f017e58321..5a36d7a848 100644 --- a/docs/generated/packages/angular/generators/ngrx.json +++ b/docs/generated/packages/angular/generators/ngrx.json @@ -28,13 +28,17 @@ "module": { "type": "string", "description": "The path to the `NgModule` where the feature state will be registered. The host directory will create/use the new state directory.", - "x-prompt": "What is the path to the module where this NgRx state should be registered?", "x-deprecated": "This option will be removed in a future version of Nx. Please switch to using --parent instead." }, "parent": { "type": "string", - "description": "The path to the `NgModule` where the feature state will be registered. The host directory will create/use the new state directory.", - "x-prompt": "What is the path to the module where this NgRx state should be registered?" + "description": "The path to the `NgModule` or the `Routes` definition file (for Standalone API usage) where the feature state will be registered. The host directory will create/use the new state directory.", + "x-prompt": "What is the path to the module or Routes definition where this NgRx state should be registered?" + }, + "route": { + "type": "string", + "description": "The route that the Standalone NgRx Providers should be added to.", + "default": "''" }, "directory": { "type": "string", diff --git a/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap index 4b64ceddc7..b01db02ea9 100644 --- a/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap +++ b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ngrx generated unit tests should generate specs for the ngrx effects 1`] = ` +exports[`ngrx NgModule Syntax generated 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'; @@ -41,7 +41,7 @@ describe('SuperUsersEffects', () => { " `; -exports[`ngrx generated unit tests should generate specs for the ngrx facade 1`] = ` +exports[`ngrx NgModule Syntax generated 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'; @@ -144,7 +144,7 @@ describe('SuperUsersFacade', () => { " `; -exports[`ngrx generated unit tests should generate specs for the ngrx reducer 1`] = ` +exports[`ngrx NgModule Syntax generated unit tests should generate specs for the ngrx reducer 1`] = ` "import { Action } from '@ngrx/store'; import * as SuperUsersActions from './super-users.actions'; @@ -185,7 +185,7 @@ describe('SuperUsers Reducer', () => { " `; -exports[`ngrx generated unit tests should generate specs for the ngrx selectors 1`] = ` +exports[`ngrx NgModule Syntax generated unit tests should generate specs for the ngrx selectors 1`] = ` "import { SuperUsersEntity } from './super-users.models'; import { superUsersAdapter, SuperUsersPartialState, initialSuperUsersState } from './super-users.reducer'; import * as SuperUsersSelectors from './super-users.selectors'; @@ -247,7 +247,7 @@ describe('SuperUsers Selectors', () => { " `; -exports[`ngrx should add a root module with feature module when minimal is set to false 1`] = ` +exports[`ngrx NgModule Syntax should add a root module with feature module when minimal is set to false 1`] = ` " import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; @@ -273,7 +273,7 @@ import { StoreRouterConnectingModule } from '@ngrx/router-store'; " `; -exports[`ngrx should add a root module with feature module when minimal is set to false using --module 1`] = ` +exports[`ngrx NgModule Syntax should add a root module with feature module when minimal is set to false using --module 1`] = ` " import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; @@ -299,7 +299,7 @@ import { StoreRouterConnectingModule } from '@ngrx/router-store'; " `; -exports[`ngrx should add an empty root module when minimal and root are set to true 1`] = ` +exports[`ngrx NgModule Syntax should add an empty root module when minimal and root are set to true 1`] = ` " import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; @@ -323,7 +323,7 @@ import { StoreRouterConnectingModule } from '@ngrx/router-store'; " `; -exports[`ngrx should generate a models file for the feature 1`] = ` +exports[`ngrx NgModule Syntax should generate a models file for the feature 1`] = ` "/** * Interface for the 'Users' data */ @@ -333,7 +333,7 @@ export interface UsersEntity { };" `; -exports[`ngrx should generate the ngrx actions 1`] = ` +exports[`ngrx NgModule Syntax should generate the ngrx actions 1`] = ` "import { createAction, props } from '@ngrx/store'; import { UsersEntity } from './users.models'; @@ -353,36 +353,33 @@ export const loadUsersFailure = createAction( " `; -exports[`ngrx should generate the ngrx effects 1`] = ` +exports[`ngrx NgModule Syntax should generate the ngrx effects 1`] = ` "import { Injectable, inject } 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'; +import {switchMap, catchError, of} from 'rxjs'; + @Injectable() export class UsersEffects { private actions$ = inject(Actions); init$ = createEffect(() => this.actions$.pipe( ofType(UsersActions.initUsers), - fetch({ - run: (action) => { - // Your custom service 'load' logic goes here. For now just return a success action... - return UsersActions.loadUsersSuccess({ users: [] }); - }, - onError: (action, error) => { + switchMap(() => of(UsersActions.loadUsersSuccess({ users: [] }))), + catchError((error) => { console.error('Error', error); - return UsersActions.loadUsersFailure({ error }); + return of(UsersActions.loadUsersFailure({ error })); } - }) + ) )); } " `; -exports[`ngrx should generate the ngrx facade 1`] = ` +exports[`ngrx NgModule Syntax should generate the ngrx facade 1`] = ` "import { Injectable, inject } from '@angular/core'; import { select, Store, Action } from '@ngrx/store'; @@ -413,7 +410,7 @@ export class UsersFacade { " `; -exports[`ngrx should generate the ngrx reducer 1`] = ` +exports[`ngrx NgModule Syntax should generate the ngrx reducer 1`] = ` "import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; import { createReducer, on, Action } from '@ngrx/store'; @@ -458,7 +455,7 @@ export function usersReducer(state: UsersState | undefined, action: Action) { " `; -exports[`ngrx should generate the ngrx selectors 1`] = ` +exports[`ngrx NgModule Syntax should generate the ngrx selectors 1`] = ` "import { createFeatureSelector, createSelector } from '@ngrx/store'; import { USERS_FEATURE_KEY, UsersState, usersAdapter } from './users.reducer'; @@ -500,7 +497,7 @@ export const selectEntity = createSelector( " `; -exports[`ngrx should not generate imports when skipImport is true 1`] = ` +exports[`ngrx NgModule Syntax should not generate imports when skipImport is true 1`] = ` " import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; @@ -515,7 +512,7 @@ exports[`ngrx should not generate imports when skipImport is true 1`] = ` " `; -exports[`ngrx should update the entry point file correctly when barrels is true 1`] = ` +exports[`ngrx NgModule 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'; @@ -532,7 +529,7 @@ export { SuperUsersActions, SuperUsersFeature, SuperUsersSelectors }; " `; -exports[`ngrx should update the entry point file with no facade 1`] = ` +exports[`ngrx NgModule 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'; @@ -542,7 +539,7 @@ export * from './lib/+state/super-users.actions'; " `; -exports[`ngrx should update the entry point file with the right exports 1`] = ` +exports[`ngrx NgModule 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'; @@ -552,3 +549,58 @@ export * from './lib/+state/super-users.actions'; export * from './lib/flights.module'; " `; + +exports[`ngrx Standalone APIs should add a root module with feature module when minimal is set to false 1`] = ` +"import { bootstrapApplication } from '@angular/platform-browser'; +import { provideRouter, withEnabledBlockingInitialNavigation } from '@angular/router'; +import { AppComponent } from './app/app.component'; +import { appRoutes } from './app/app.routes'; +import { provideStore, provideState } from '@ngrx/store'; +import { provideEffects } from '@ngrx/effects'; +import * as fromUsers from './+state/users.reducer'; +import { UsersEffects } from './+state/users.effects'; + +bootstrapApplication(AppComponent, { + providers: [provideEffects(UsersEffects),provideState(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer),provideEffects(),provideStore(),provideRouter(appRoutes, withEnabledBlockingInitialNavigation())], +}).catch((err) => console.error(err));" +`; + +exports[`ngrx Standalone APIs should add an empty provideStore when minimal and root are set to true 1`] = ` +"import { bootstrapApplication } from '@angular/platform-browser'; +import { provideRouter, withEnabledBlockingInitialNavigation } from '@angular/router'; +import { AppComponent } from './app/app.component'; +import { appRoutes } from './app/app.routes'; +import { provideStore, provideState } from '@ngrx/store'; +import { provideEffects } from '@ngrx/effects'; + +bootstrapApplication(AppComponent, { + providers: [provideEffects(),provideStore(),provideRouter(appRoutes, withEnabledBlockingInitialNavigation())], +}).catch((err) => console.error(err));" +`; + +exports[`ngrx Standalone APIs should add facade provider when facade is true 1`] = ` +"import { bootstrapApplication } from '@angular/platform-browser'; +import { provideRouter, withEnabledBlockingInitialNavigation } from '@angular/router'; +import { AppComponent } from './app/app.component'; +import { appRoutes } from './app/app.routes'; +import { provideStore, provideState } from '@ngrx/store'; +import { provideEffects } from '@ngrx/effects'; +import * as fromUsers from './+state/users.reducer'; +import { UsersEffects } from './+state/users.effects'; +import { UsersFacade } from './+state/users.facade'; + +bootstrapApplication(AppComponent, { + providers: [provideEffects(UsersEffects),provideState(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer),provideEffects(),provideStore(),UsersFacade,provideRouter(appRoutes, withEnabledBlockingInitialNavigation())], +}).catch((err) => console.error(err));" +`; + +exports[`ngrx Standalone APIs should add facade provider when facade is true and --root is false 1`] = ` +"import { Routes } from '@angular/router'; + import { NxWelcomeComponent } from './nx-welcome.component'; +import { provideStore, provideState } from '@ngrx/store'; +import { provideEffects } from '@ngrx/effects'; +import * as fromUsers from './+state/users.reducer'; +import { UsersEffects } from './+state/users.effects'; +import { UsersFacade } from './+state/users.facade'; + export const appRoutes: Routes = [{ path: '', component: NxWelcomeComponent , providers: [UsersFacade, provideState(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer), provideEffects(UsersEffects)]}];" +`; diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.effects.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.effects.ts__tmpl__ index f97fd5682a..472e2f1e72 100644 --- a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.effects.ts__tmpl__ +++ b/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.effects.ts__tmpl__ @@ -1,25 +1,22 @@ import { Injectable, inject } from '@angular/core'; import { createEffect, Actions, ofType } from '@ngrx/effects'; -import { fetch } from '@nrwl/angular'; import * as <%= className %>Actions from './<%= fileName %>.actions'; import * as <%= className %>Feature from './<%= fileName %>.reducer'; +import {switchMap, catchError, of} from 'rxjs'; + @Injectable() export class <%= className %>Effects { private actions$ = inject(Actions); init$ = createEffect(() => this.actions$.pipe( ofType(<%= className %>Actions.init<%= className %>), - 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) => { + switchMap(() => of(<%= className %>Actions.load<%= className %>Success({ <%= propertyName %>: [] }))), + catchError((error) => { console.error('Error', error); - return <%= className %>Actions.load<%= className %>Failure({ error }); + return of(<%= className %>Actions.load<%= className %>Failure({ error })); } - }) + ) )); } 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 index 0a803e0edd..cbac71a0b5 100644 --- a/packages/angular/src/generators/ngrx/lib/add-imports-to-module.ts +++ b/packages/angular/src/generators/ngrx/lib/add-imports-to-module.ts @@ -5,9 +5,139 @@ import type { SourceFile } from 'typescript'; import { createSourceFile, ScriptTarget } from 'typescript'; import { addImportToModule, + addProviderToBootstrapApplication, addProviderToModule, } from '../../../utils/nx-devkit/ast-utils'; import type { NormalizedNgRxGeneratorOptions } from './normalize-options'; +import { addProviderToRoute } from '../../../utils/nx-devkit/route-utils'; + +function addRootStoreImport( + tree: Tree, + isParentStandalone: boolean, + route: string, + sourceFile: SourceFile, + parentPath: string, + provideRootStore: string, + storeForRoot: string +) { + if (isParentStandalone) { + if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { + addProviderToBootstrapApplication(tree, parentPath, provideRootStore); + } else { + addProviderToRoute(tree, parentPath, route, provideRootStore); + } + } else { + sourceFile = addImportToModule(tree, sourceFile, parentPath, storeForRoot); + } + return sourceFile; +} + +function addRootEffectsImport( + tree: Tree, + isParentStandalone: boolean, + route: string, + sourceFile: SourceFile, + parentPath: string, + provideRootEffects: string, + effectsForEmptyRoot: string +) { + if (isParentStandalone) { + if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { + addProviderToBootstrapApplication(tree, parentPath, provideRootEffects); + } else { + addProviderToRoute(tree, parentPath, route, provideRootEffects); + } + } else { + sourceFile = addImportToModule( + tree, + sourceFile, + parentPath, + effectsForEmptyRoot + ); + } + + return sourceFile; +} + +function addRouterStoreImport( + tree: Tree, + sourceFile: SourceFile, + addImport: ( + source: SourceFile, + symbolName: string, + fileName: string, + isDefault?: boolean + ) => SourceFile, + parentPath: string, + storeRouterModule: string +) { + sourceFile = addImport( + sourceFile, + 'StoreRouterConnectingModule', + '@ngrx/router-store' + ); + return addImportToModule(tree, sourceFile, parentPath, storeRouterModule); +} + +function addStoreForFeatureImport( + tree: Tree, + isParentStandalone, + route: string, + sourceFile: SourceFile, + parentPath: string, + provideStoreForFeature: string, + storeForFeature: string +) { + if (isParentStandalone) { + if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { + addProviderToBootstrapApplication( + tree, + parentPath, + provideStoreForFeature + ); + } else { + addProviderToRoute(tree, parentPath, route, provideStoreForFeature); + } + } else { + sourceFile = addImportToModule( + tree, + sourceFile, + parentPath, + storeForFeature + ); + } + return sourceFile; +} + +function addEffectsForFeatureImport( + tree: Tree, + isParentStandalone, + route: string, + sourceFile: SourceFile, + parentPath: string, + provideEffectsForFeature: string, + effectsForFeature: string +) { + if (isParentStandalone) { + if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { + addProviderToBootstrapApplication( + tree, + parentPath, + provideEffectsForFeature + ); + } else { + addProviderToRoute(tree, parentPath, route, provideEffectsForFeature); + } + } else { + sourceFile = addImportToModule( + tree, + sourceFile, + parentPath, + effectsForFeature + ); + } + return sourceFile; +} export function addImportsToModule( tree: Tree, @@ -21,6 +151,9 @@ export function addImportsToModule( ScriptTarget.Latest, true ); + + const isParentStandalone = !sourceText.includes('@NgModule'); + const addImport = ( source: SourceFile, symbolName: string, @@ -65,30 +198,48 @@ export function addImportsToModule( const effectsForFeature = `EffectsModule.forFeature([${effectsName}])`; const storeRouterModule = 'StoreRouterConnectingModule.forRoot()'; + const provideRootStore = `provideStore()`; + const provideRootEffects = `provideEffects()`; + const provideEffectsForFeature = `provideEffects(${effectsName})`; + const provideStoreForFeature = `provideState(from${className}.${constantName}_FEATURE_KEY, from${className}.${propertyName}Reducer)`; + + if (isParentStandalone) { + sourceFile = addImport(sourceFile, 'provideStore', '@ngrx/store'); + sourceFile = addImport(sourceFile, 'provideState', '@ngrx/store'); + sourceFile = addImport(sourceFile, 'provideEffects', '@ngrx/effects'); + } else { + sourceFile = addImport(sourceFile, 'StoreModule', '@ngrx/store'); + sourceFile = addImport(sourceFile, 'EffectsModule', '@ngrx/effects'); + } + // this is just a heuristic const hasRouter = sourceText.indexOf('RouterModule') > -1; - sourceFile = addImport(sourceFile, 'StoreModule', '@ngrx/store'); - sourceFile = addImport(sourceFile, 'EffectsModule', '@ngrx/effects'); - if (options.minimal && options.root) { - sourceFile = addImportToModule(tree, sourceFile, parentPath, storeForRoot); - sourceFile = addImportToModule( + sourceFile = addRootStoreImport( tree, + isParentStandalone, + options.route, sourceFile, parentPath, + provideRootStore, + storeForRoot + ); + sourceFile = addRootEffectsImport( + tree, + isParentStandalone, + options.route, + sourceFile, + parentPath, + provideRootEffects, effectsForEmptyRoot ); - if (hasRouter) { - sourceFile = addImport( - sourceFile, - 'StoreRouterConnectingModule', - '@ngrx/router-store' - ); - sourceFile = addImportToModule( + if (hasRouter && !isParentStandalone) { + sourceFile = addRouterStoreImport( tree, sourceFile, + addImport, parentPath, storeRouterModule ); @@ -100,12 +251,20 @@ export function addImportsToModule( if (options.facade) { sourceFile = addImport(sourceFile, facadeName, facadePath); - sourceFile = addProviderToModule( - tree, - sourceFile, - parentPath, - facadeName - ); + if (isParentStandalone) { + if (tree.read(parentPath, 'utf-8').includes('bootstrapApplication')) { + addProviderToBootstrapApplication(tree, parentPath, facadeName); + } else { + addProviderToRoute(tree, parentPath, options.route, facadeName); + } + } else { + sourceFile = addProviderToModule( + tree, + sourceFile, + parentPath, + facadeName + ); + } } return sourceFile; @@ -114,52 +273,76 @@ export function addImportsToModule( if (options.root) { sourceFile = addCommonImports(); - sourceFile = addImportToModule( + sourceFile = addRootStoreImport( tree, + isParentStandalone, + options.route, sourceFile, parentPath, + provideRootStore, storeForRoot ); - sourceFile = addImportToModule( + + sourceFile = addRootEffectsImport( tree, + isParentStandalone, + options.route, sourceFile, parentPath, + provideRootEffects, effectsForRoot ); - if (hasRouter) { - sourceFile = addImport( - sourceFile, - 'StoreRouterConnectingModule', - '@ngrx/router-store' - ); - sourceFile = addImportToModule( + if (hasRouter && !isParentStandalone) { + sourceFile = addRouterStoreImport( tree, sourceFile, + addImport, parentPath, storeRouterModule ); } - sourceFile = addImportToModule( + sourceFile = addStoreForFeatureImport( tree, + isParentStandalone, + options.route, sourceFile, parentPath, + provideStoreForFeature, storeForFeature ); + + if (isParentStandalone) { + addEffectsForFeatureImport( + tree, + isParentStandalone, + options.route, + sourceFile, + parentPath, + provideEffectsForFeature, + effectsForFeature + ); + } } else { sourceFile = addCommonImports(); - sourceFile = addImportToModule( + sourceFile = addStoreForFeatureImport( tree, + isParentStandalone, + options.route, sourceFile, parentPath, + provideStoreForFeature, storeForFeature ); - sourceFile = addImportToModule( + sourceFile = addEffectsForFeatureImport( tree, + isParentStandalone, + options.route, sourceFile, parentPath, + provideEffectsForFeature, effectsForFeature ); } diff --git a/packages/angular/src/generators/ngrx/lib/normalize-options.ts b/packages/angular/src/generators/ngrx/lib/normalize-options.ts index 7559011d69..3b3f0a5a66 100644 --- a/packages/angular/src/generators/ngrx/lib/normalize-options.ts +++ b/packages/angular/src/generators/ngrx/lib/normalize-options.ts @@ -16,6 +16,7 @@ export function normalizeOptions( : options.parent ? dirname(options.parent) : undefined, + route: options.route ?? "''", directory: names(options.directory).fileName, }; } diff --git a/packages/angular/src/generators/ngrx/ngrx.spec.ts b/packages/angular/src/generators/ngrx/ngrx.spec.ts index 5b57f213bf..501c91c0fa 100644 --- a/packages/angular/src/generators/ngrx/ngrx.spec.ts +++ b/packages/angular/src/generators/ngrx/ngrx.spec.ts @@ -11,6 +11,7 @@ import { } from '../../utils/nx-devkit/testing'; import { ngrxVersion } from '../../utils/versions'; import { ngrxGenerator } from './ngrx'; +import applicationGenerator from '../application/application'; import type { NgRxGeneratorOptions } from './schema'; describe('ngrx', () => { @@ -25,6 +26,13 @@ describe('ngrx', () => { name: 'users', }; + const defaultStandaloneOptions: NgRxGeneratorOptions = { + directory: '+state', + minimal: true, + parent: 'apps/my-app/src/main.ts', + name: 'users', + }; + const defaultModuleOptions: NgRxGeneratorOptions = { directory: '+state', minimal: true, @@ -37,442 +45,546 @@ describe('ngrx', () => { const expectFileToNotExist = (file: string) => expect(tree.exists(file)).not.toBeTruthy(); - beforeEach(() => { - jest.clearAllMocks(); - tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); - createApp(tree, 'myapp'); - appConfig = getAppConfig(); - statePath = `${dirname(appConfig.appModule)}/+state`; - }); - - 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 error when the module could not be found using --module', 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 and root are set to true', async () => { - await ngrxGenerator(tree, { - ...defaultOptions, - root: true, - minimal: true, + describe('NgModule Syntax', () => { + beforeEach(() => { + jest.clearAllMocks(); + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + createApp(tree, 'myapp'); + appConfig = getAppConfig(); + statePath = `${dirname(appConfig.appModule)}/+state`; }); - expect( - tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8') - ).toMatchSnapshot(); - }); + it('should error when the module could not be found', async () => { + const modulePath = 'not-existing.module.ts'; - it('should not generate files when minimal and root are set to true', async () => { - await ngrxGenerator(tree, { - ...defaultOptions, - root: true, - minimal: true, + await expect( + ngrxGenerator(tree, { + ...defaultOptions, + module: modulePath, + }) + ).rejects.toThrowError(`Module does not exist: ${modulePath}.`); }); - expect(tree.exists('/apps/myapp/src/app/+state/users.actions.ts')).toBe( - false - ); - expect(tree.exists('/apps/myapp/src/app/+state/users.effects.ts')).toBe( - false - ); - expect( - tree.exists('/apps/myapp/src/app/+state/users.effects.spec.ts') - ).toBe(false); - expect(tree.exists('/apps/myapp/src/app/+state/users.reducer.ts')).toBe( - false - ); - expect(tree.exists('/apps/myapp/src/app/+state/users.selectors.ts')).toBe( - false - ); - expect( - tree.exists('/apps/myapp/src/app/+state/users.selectors.spec.ts') - ).toBe(false); - }); + it('should error when the module could not be found using --module', async () => { + const modulePath = 'not-existing.module.ts'; - it('should add a root module with feature module when minimal is set to false', async () => { - await ngrxGenerator(tree, { - ...defaultOptions, - root: true, - minimal: false, + await expect( + ngrxGenerator(tree, { + ...defaultOptions, + module: modulePath, + }) + ).rejects.toThrowError(`Module does not exist: ${modulePath}.`); }); - expect( - tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8') - ).toMatchSnapshot(); - }); - - it('should add a root module with feature module when minimal is set to false using --module', async () => { - await ngrxGenerator(tree, { - ...defaultModuleOptions, - root: true, - minimal: 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 generate imports 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 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 - ); - expect(packageJson.devDependencies['jasmine-marbles']).toBeDefined(); - }); - - 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 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 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(); - }); - - 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(); - }); - - describe('generated unit tests', () => { - it('should generate specs for the ngrx effects', async () => { + it('should add an empty root module when minimal and root are set to true', async () => { await ngrxGenerator(tree, { ...defaultOptions, - name: 'super-users', - module: appConfig.appModule, + root: true, + minimal: true, + }); + + expect( + tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8') + ).toMatchSnapshot(); + }); + + it('should not generate files when minimal and root are set to true', async () => { + await ngrxGenerator(tree, { + ...defaultOptions, + root: true, + minimal: true, + }); + + expect(tree.exists('/apps/myapp/src/app/+state/users.actions.ts')).toBe( + false + ); + expect(tree.exists('/apps/myapp/src/app/+state/users.effects.ts')).toBe( + false + ); + expect( + tree.exists('/apps/myapp/src/app/+state/users.effects.spec.ts') + ).toBe(false); + expect(tree.exists('/apps/myapp/src/app/+state/users.reducer.ts')).toBe( + false + ); + expect(tree.exists('/apps/myapp/src/app/+state/users.selectors.ts')).toBe( + false + ); + expect( + tree.exists('/apps/myapp/src/app/+state/users.selectors.spec.ts') + ).toBe(false); + }); + + it('should add a root module with feature module when minimal is set to false', async () => { + await ngrxGenerator(tree, { + ...defaultOptions, + root: true, minimal: false, }); expect( - tree.read(`${statePath}/super-users.effects.spec.ts`, 'utf-8') + tree.read('/apps/myapp/src/app/app.module.ts', 'utf-8') ).toMatchSnapshot(); }); - it('should generate specs for the ngrx facade', async () => { + it('should add a root module with feature module when minimal is set to false using --module', async () => { + await ngrxGenerator(tree, { + ...defaultModuleOptions, + root: true, + minimal: 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, - name: 'super-users', - module: appConfig.appModule, + 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(`${statePath}/super-users.facade.spec.ts`, 'utf-8') + 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 generate imports 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 generate specs for the ngrx reducer', async () => { + 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 + ); + expect(packageJson.devDependencies['jasmine-marbles']).toBeDefined(); + }); + + 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 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, - name: 'super-users', module: appConfig.appModule, minimal: false, }); expect( - tree.read(`${statePath}/super-users.reducer.spec.ts`, 'utf-8') + tree.read(`${statePath}/users.models.ts`, 'utf-8') ).toMatchSnapshot(); }); - it('should generate specs for the ngrx selectors', async () => { + it('should generate the ngrx reducer', async () => { await ngrxGenerator(tree, { ...defaultOptions, - name: 'super-users', module: appConfig.appModule, - minimal: false, }); expect( - tree.read(`${statePath}/super-users.selectors.spec.ts`, 'utf-8') + 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 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(); + }); + + 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(); + }); + + describe('generated 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 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(); + }); + }); + }); + + describe('Standalone APIs', () => { + beforeEach(async () => { + jest.clearAllMocks(); + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + await applicationGenerator(tree, { + name: 'my-app', + standalone: true, + routing: true, + }); + tree.write( + 'apps/my-app/src/app/app.component.html', + '' + ); + tree.write( + 'apps/my-app/src/app/app.routes.ts', + `import { Routes } from '@angular/router'; + import { NxWelcomeComponent } from './nx-welcome.component'; + export const appRoutes: Routes = [{ path: '', component: NxWelcomeComponent }];` + ); + }); + + it('should throw when the parent cannot be found', async () => { + // ARRANGE + const parentPath = 'apps/my-app/src/app/non-existent.routes.ts'; + + // ACT & ASSERT + await expect( + ngrxGenerator(tree, { + ...defaultStandaloneOptions, + parent: parentPath, + }) + ).rejects.toThrowError(`Parent does not exist: ${parentPath}.`); + }); + + it('should add an empty provideStore when minimal and root are set to true', async () => { + await ngrxGenerator(tree, { + ...defaultStandaloneOptions, + root: true, + minimal: true, + }); + + expect(tree.read('/apps/my-app/src/main.ts', 'utf-8')).toMatchSnapshot(); + expect(tree.exists('/apps/my-app/src/app/+state/users.actions.ts')).toBe( + false + ); + expect(tree.exists('/apps/my-app/src/app/+state/users.effects.ts')).toBe( + false + ); + expect( + tree.exists('/apps/my-app/src/app/+state/users.effects.spec.ts') + ).toBe(false); + expect(tree.exists('/apps/my-app/src/app/+state/users.reducer.ts')).toBe( + false + ); + expect( + tree.exists('/apps/my-app/src/app/+state/users.selectors.ts') + ).toBe(false); + expect( + tree.exists('/apps/my-app/src/app/+state/users.selectors.spec.ts') + ).toBe(false); + }); + + it('should add a root module with feature module when minimal is set to false', async () => { + await ngrxGenerator(tree, { + ...defaultStandaloneOptions, + root: true, + minimal: false, + }); + + expect(tree.read('/apps/my-app/src/main.ts', 'utf-8')).toMatchSnapshot(); + }); + + it('should add facade provider when facade is true', async () => { + await ngrxGenerator(tree, { + ...defaultStandaloneOptions, + root: true, + minimal: false, + facade: true, + }); + + expect(tree.read('/apps/my-app/src/main.ts', 'utf-8')).toMatchSnapshot(); + }); + + it('should add facade provider when facade is true and --root is false', async () => { + await ngrxGenerator(tree, { + ...defaultStandaloneOptions, + root: false, + minimal: false, + facade: true, + parent: 'apps/my-app/src/app/app.routes.ts', + }); + + expect( + tree.read('/apps/my-app/src/app/app.routes.ts', 'utf-8') ).toMatchSnapshot(); }); }); diff --git a/packages/angular/src/generators/ngrx/schema.d.ts b/packages/angular/src/generators/ngrx/schema.d.ts index 04bbcf2109..6093bbaab4 100644 --- a/packages/angular/src/generators/ngrx/schema.d.ts +++ b/packages/angular/src/generators/ngrx/schema.d.ts @@ -3,6 +3,7 @@ export interface NgRxGeneratorOptions { minimal: boolean; module?: string; parent?: string; + route?: string; name: string; barrels?: boolean; facade?: boolean; diff --git a/packages/angular/src/generators/ngrx/schema.json b/packages/angular/src/generators/ngrx/schema.json index e4de368f38..3f373ce2ec 100644 --- a/packages/angular/src/generators/ngrx/schema.json +++ b/packages/angular/src/generators/ngrx/schema.json @@ -28,13 +28,17 @@ "module": { "type": "string", "description": "The path to the `NgModule` where the feature state will be registered. The host directory will create/use the new state directory.", - "x-prompt": "What is the path to the module where this NgRx state should be registered?", "x-deprecated": "This option will be removed in a future version of Nx. Please switch to using --parent instead." }, "parent": { "type": "string", - "description": "The path to the `NgModule` where the feature state will be registered. The host directory will create/use the new state directory.", - "x-prompt": "What is the path to the module where this NgRx state should be registered?" + "description": "The path to the `NgModule` or the `Routes` definition file (for Standalone API usage) where the feature state will be registered. The host directory will create/use the new state directory.", + "x-prompt": "What is the path to the module or Routes definition where this NgRx state should be registered?" + }, + "route": { + "type": "string", + "description": "The route that the Standalone NgRx Providers should be added to.", + "default": "''" }, "directory": { "type": "string", diff --git a/packages/angular/src/utils/nx-devkit/ast-utils.ts b/packages/angular/src/utils/nx-devkit/ast-utils.ts index 3e1c9c1a86..d57d373c13 100644 --- a/packages/angular/src/utils/nx-devkit/ast-utils.ts +++ b/packages/angular/src/utils/nx-devkit/ast-utils.ts @@ -653,6 +653,22 @@ export function addProviderToModule( ); } +export function addProviderToComponent( + host: Tree, + source: ts.SourceFile, + componentPath: string, + symbolName: string +): ts.SourceFile { + return _addSymbolToDecoratorMetadata( + host, + source, + componentPath, + 'providers', + symbolName, + 'Component' + ); +} + export function addDeclarationToModule( host: Tree, source: ts.SourceFile,