From 0012395e94620c338b884b302a88b1c07d8e3823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Fri, 13 Jan 2023 18:16:20 +0100 Subject: [PATCH] feat(angular): add backwards compat support for the ngrx generator --- .../packages/angular/generators/ngrx.json | 4 +- packages/angular/package.json | 10 +- .../ngrx/__snapshots__/ngrx.spec.ts.snap | 114 ++++++++++++++++++ .../__fileName__.actions.ts__tmpl__ | 0 .../__fileName__.effects.spec.ts__tmpl__ | 0 .../__fileName__.effects.ts__tmpl__ | 0 .../__fileName__.facade.spec.ts__tmpl__ | 0 .../__fileName__.facade.ts__tmpl__ | 0 .../__fileName__.models.ts__tmpl__ | 0 .../__fileName__.reducer.spec.ts__tmpl__ | 0 .../__fileName__.reducer.ts__tmpl__ | 0 .../__fileName__.selectors.spec.ts__tmpl__ | 0 .../__fileName__.selectors.ts__tmpl__ | 0 .../__fileName__.effects.ts__tmpl__ | 22 ++++ .../__fileName__.facade.ts__tmpl__ | 27 +++++ .../ngrx/lib/add-ngrx-to-package-json.ts | 16 ++- .../src/generators/ngrx/lib/generate-files.ts | 18 ++- .../angular/src/generators/ngrx/lib/index.ts | 1 + .../generators/ngrx/lib/validate-options.ts | 41 +++++++ .../angular/src/generators/ngrx/ngrx.spec.ts | 113 ++++++++++++++++- packages/angular/src/generators/ngrx/ngrx.ts | 14 +-- .../angular/src/generators/ngrx/schema.json | 4 +- packages/angular/src/utils/version-utils.ts | 13 ++ 23 files changed, 373 insertions(+), 24 deletions(-) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.actions.ts__tmpl__ (100%) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.effects.spec.ts__tmpl__ (100%) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.effects.ts__tmpl__ (100%) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.facade.spec.ts__tmpl__ (100%) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.facade.ts__tmpl__ (100%) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.models.ts__tmpl__ (100%) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.reducer.spec.ts__tmpl__ (100%) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.reducer.ts__tmpl__ (100%) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.selectors.spec.ts__tmpl__ (100%) rename packages/angular/src/generators/ngrx/files/{ => latest}/__directory__/__fileName__.selectors.ts__tmpl__ (100%) create mode 100644 packages/angular/src/generators/ngrx/files/no-inject/__directory__/__fileName__.effects.ts__tmpl__ create mode 100644 packages/angular/src/generators/ngrx/files/no-inject/__directory__/__fileName__.facade.ts__tmpl__ create mode 100644 packages/angular/src/generators/ngrx/lib/validate-options.ts create mode 100644 packages/angular/src/utils/version-utils.ts diff --git a/docs/generated/packages/angular/generators/ngrx.json b/docs/generated/packages/angular/generators/ngrx.json index 5a36d7a848ae8..20a06ebe15153 100644 --- a/docs/generated/packages/angular/generators/ngrx.json +++ b/docs/generated/packages/angular/generators/ngrx.json @@ -32,12 +32,12 @@ }, "parent": { "type": "string", - "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.", + "description": "The path to the `NgModule` or the `Routes` definition file (for Standalone API usage) where the feature state will be registered. _Note: The Standalone API usage is only supported in Angular versions >= 14.1.0_.", "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.", + "description": "The route that the Standalone NgRx Providers should be added to. _Note: This is only supported in Angular versions >= 14.1.0_.", "default": "''" }, "directory": { diff --git a/packages/angular/package.json b/packages/angular/package.json index ffa2cb409789b..074bc834ac2bf 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -40,7 +40,6 @@ }, "dependencies": { "@angular-devkit/schematics": "~15.1.0", - "@nguniversal/builders": "~15.0.0", "@nrwl/cypress": "file:../cypress", "@nrwl/devkit": "file:../devkit", "@nrwl/jest": "file:../jest", @@ -61,6 +60,15 @@ "webpack": "^5.75.0", "webpack-merge": "5.7.3" }, + "peerDependencies": { + "@nguniversal/builders": "~15.0.0", + "rxjs": "^6.5.3 || ^7.5.0" + }, + "peerDependenciesMeta": { + "@nguniversal/builders": { + "optional": true + } + }, "publishConfig": { "access": "public" } 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 b01db02ea9c62..ff7019a0a7d8f 100644 --- a/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap +++ b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap @@ -604,3 +604,117 @@ 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)]}];" `; + +exports[`ngrx angular v14 support should generate the ngrx effects using "inject" for versions >= 14.1.0 1`] = ` +"import { Injectable, inject } from '@angular/core'; +import { createEffect, Actions, ofType } from '@ngrx/effects'; + +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), + switchMap(() => of(UsersActions.loadUsersSuccess({ users: [] }))), + catchError((error) => { + console.error('Error', error); + return of(UsersActions.loadUsersFailure({ error })); + } + ) + )); +} +" +`; + +exports[`ngrx angular v14 support should generate the ngrx effects with no usage of "inject" 1`] = ` +"import { Injectable } from '@angular/core'; +import { createEffect, Actions, ofType } from '@ngrx/effects'; + +import * as UsersActions from './users.actions'; +import * as UsersFeature from './users.reducer'; + +import {switchMap, catchError, of} from 'rxjs'; + +@Injectable() +export class UsersEffects { + init$ = createEffect(() => this.actions$.pipe( + ofType(UsersActions.initUsers), + switchMap(() => of(UsersActions.loadUsersSuccess({ users: [] }))), + catchError((error) => { + console.error('Error', error); + return of(UsersActions.loadUsersFailure({ error })); + } + ) + )); + + constructor(private readonly actions$: Actions) {} +} +" +`; + +exports[`ngrx angular v14 support should generate the ngrx facade using "inject" for versions >= 14.1.0 1`] = ` +"import { Injectable, inject } from '@angular/core'; +import { select, Store, Action } from '@ngrx/store'; + +import * as UsersActions from './users.actions'; +import * as UsersFeature from './users.reducer'; +import * as UsersSelectors from './users.selectors'; + +@Injectable() +export class UsersFacade { + private readonly store = inject(Store); + + /** + * Combine pieces of state using createSelector, + * and expose them as observables through the facade. + */ + loaded$ = this.store.pipe(select(UsersSelectors.selectUsersLoaded)); + allUsers$ = this.store.pipe(select(UsersSelectors.selectAllUsers)); + selectedUsers$ = this.store.pipe(select(UsersSelectors.selectEntity)); + + /** + * Use the initialization action to perform one + * or more tasks in your Effects. + */ + init() { + this.store.dispatch(UsersActions.initUsers()); + } +} +" +`; + +exports[`ngrx angular v14 support should generate the ngrx facade with no usage of "inject" 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.selectUsersLoaded)); + allUsers$ = this.store.pipe(select(UsersSelectors.selectAllUsers)); + selectedUsers$ = this.store.pipe(select(UsersSelectors.selectEntity)); + + constructor(private readonly store: Store) {} + + /** + * Use the initialization action to perform one + * or more tasks in your Effects. + */ + init() { + this.store.dispatch(UsersActions.initUsers()); + } +} +" +`; diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.actions.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.actions.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.actions.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.actions.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.effects.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.effects.spec.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.effects.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.effects.spec.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.effects.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.effects.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.effects.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.effects.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.facade.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.facade.spec.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.facade.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.facade.spec.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.facade.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.facade.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.facade.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.facade.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.models.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.models.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.models.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.models.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.reducer.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.reducer.spec.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.reducer.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.reducer.spec.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.reducer.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.reducer.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.reducer.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.reducer.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.selectors.spec.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.selectors.spec.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.selectors.spec.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.selectors.spec.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/__directory__/__fileName__.selectors.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.selectors.ts__tmpl__ similarity index 100% rename from packages/angular/src/generators/ngrx/files/__directory__/__fileName__.selectors.ts__tmpl__ rename to packages/angular/src/generators/ngrx/files/latest/__directory__/__fileName__.selectors.ts__tmpl__ diff --git a/packages/angular/src/generators/ngrx/files/no-inject/__directory__/__fileName__.effects.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/no-inject/__directory__/__fileName__.effects.ts__tmpl__ new file mode 100644 index 0000000000000..508c619520756 --- /dev/null +++ b/packages/angular/src/generators/ngrx/files/no-inject/__directory__/__fileName__.effects.ts__tmpl__ @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { createEffect, Actions, ofType } from '@ngrx/effects'; + +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 { + init$ = createEffect(() => this.actions$.pipe( + ofType(<%= className %>Actions.init<%= className %>), + switchMap(() => of(<%= className %>Actions.load<%= className %>Success({ <%= propertyName %>: [] }))), + catchError((error) => { + console.error('Error', error); + return of(<%= className %>Actions.load<%= className %>Failure({ error })); + } + ) + )); + + constructor(private readonly actions$: Actions) {} +} diff --git a/packages/angular/src/generators/ngrx/files/no-inject/__directory__/__fileName__.facade.ts__tmpl__ b/packages/angular/src/generators/ngrx/files/no-inject/__directory__/__fileName__.facade.ts__tmpl__ new file mode 100644 index 0000000000000..b5bd3607baa4f --- /dev/null +++ b/packages/angular/src/generators/ngrx/files/no-inject/__directory__/__fileName__.facade.ts__tmpl__ @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { select, Store, Action } from '@ngrx/store'; + +import * as <%= className %>Actions from './<%= fileName %>.actions'; +import * as <%= className %>Feature from './<%= fileName %>.reducer'; +import * as <%= className %>Selectors from './<%= fileName %>.selectors'; + +@Injectable() +export class <%= className %>Facade { + /** + * Combine pieces of state using createSelector, + * and expose them as observables through the facade. + */ + loaded$ = this.store.pipe(select(<%= className %>Selectors.select<%= className %>Loaded)); + all<%= className %>$ = this.store.pipe(select(<%= className %>Selectors.selectAll<%= className %>)); + selected<%= className %>$ = this.store.pipe(select(<%= className %>Selectors.selectEntity)); + + constructor(private readonly store: Store) {} + + /** + * Use the initialization action to perform one + * or more tasks in your Effects. + */ + init() { + this.store.dispatch(<%= className %>Actions.init<%= className %>()); + } +} diff --git a/packages/angular/src/generators/ngrx/lib/add-ngrx-to-package-json.ts b/packages/angular/src/generators/ngrx/lib/add-ngrx-to-package-json.ts index 94cd321da154e..51b910e6a5e95 100644 --- a/packages/angular/src/generators/ngrx/lib/add-ngrx-to-package-json.ts +++ b/packages/angular/src/generators/ngrx/lib/add-ngrx-to-package-json.ts @@ -1,11 +1,10 @@ import type { GeneratorCallback, Tree } from '@nrwl/devkit'; import { addDependenciesToPackageJson, readJson } from '@nrwl/devkit'; -import { gte } from 'semver'; -import { - ngrxVersion, - rxjsVersion as defaultRxjsVersion, -} from '../../../utils/versions'; import { checkAndCleanWithSemver } from '@nrwl/workspace/src/utilities/version-utils'; +import { gte } from 'semver'; +import { getPkgVersionForAngularMajorVersion } from '../../../utils/version-utils'; +import { rxjsVersion as defaultRxjsVersion } from '../../../utils/versions'; +import { getInstalledAngularMajorVersion } from '../../utils/angular-version-utils'; export function addNgRxToPackageJson(tree: Tree): GeneratorCallback { let rxjsVersion: string; @@ -18,6 +17,13 @@ export function addNgRxToPackageJson(tree: Tree): GeneratorCallback { rxjsVersion = checkAndCleanWithSemver('rxjs', defaultRxjsVersion); } const jasmineMarblesVersion = gte(rxjsVersion, '7.0.0') ? '~0.9.1' : '~0.8.3'; + + const angularMajorVersion = getInstalledAngularMajorVersion(tree); + const ngrxVersion = getPkgVersionForAngularMajorVersion( + 'ngrxVersion', + angularMajorVersion + ); + return addDependenciesToPackageJson( tree, { diff --git a/packages/angular/src/generators/ngrx/lib/generate-files.ts b/packages/angular/src/generators/ngrx/lib/generate-files.ts index 634c4395607f1..f5a6448f830d0 100644 --- a/packages/angular/src/generators/ngrx/lib/generate-files.ts +++ b/packages/angular/src/generators/ngrx/lib/generate-files.ts @@ -1,5 +1,7 @@ import type { Tree } from '@nrwl/devkit'; import { generateFiles, joinPathFragments, names } from '@nrwl/devkit'; +import { lt } from 'semver'; +import { getInstalledAngularVersion } from '../../utils/angular-version-utils'; import { NormalizedNgRxGeneratorOptions } from './normalize-options'; /** @@ -14,7 +16,7 @@ export function generateNgrxFilesFromTemplates( generateFiles( tree, - joinPathFragments(__dirname, '..', 'files'), + joinPathFragments(__dirname, '..', 'files', 'latest'), options.parentDirectory, { ...options, @@ -23,6 +25,20 @@ export function generateNgrxFilesFromTemplates( } ); + const angularVersion = getInstalledAngularVersion(tree); + if (lt(angularVersion, '14.1.0')) { + generateFiles( + tree, + joinPathFragments(__dirname, '..', 'files', 'no-inject'), + options.parentDirectory, + { + ...options, + ...projectNames, + tmpl: '', + } + ); + } + if (!options.facade) { tree.delete( joinPathFragments( diff --git a/packages/angular/src/generators/ngrx/lib/index.ts b/packages/angular/src/generators/ngrx/lib/index.ts index 53f0de87c3dd6..cb30ebe54f2f4 100644 --- a/packages/angular/src/generators/ngrx/lib/index.ts +++ b/packages/angular/src/generators/ngrx/lib/index.ts @@ -3,3 +3,4 @@ export { addImportsToModule } from './add-imports-to-module'; export { addNgRxToPackageJson } from './add-ngrx-to-package-json'; export { generateNgrxFilesFromTemplates } from './generate-files'; export { normalizeOptions } from './normalize-options'; +export { validateOptions } from './validate-options'; diff --git a/packages/angular/src/generators/ngrx/lib/validate-options.ts b/packages/angular/src/generators/ngrx/lib/validate-options.ts new file mode 100644 index 0000000000000..08ad22abc981a --- /dev/null +++ b/packages/angular/src/generators/ngrx/lib/validate-options.ts @@ -0,0 +1,41 @@ +import type { Tree } from '@nrwl/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { lt } from 'semver'; +import { getInstalledAngularVersion } from '../../utils/angular-version-utils'; +import type { NgRxGeneratorOptions } from '../schema'; + +export function validateOptions( + tree: Tree, + options: NgRxGeneratorOptions +): void { + if (!options.module && !options.parent) { + throw new Error('Please provide a value for "--parent"!'); + } + if (options.module && !tree.exists(options.module)) { + throw new Error(`Module does not exist: ${options.module}.`); + } + if (options.parent && !tree.exists(options.parent)) { + throw new Error(`Parent does not exist: ${options.parent}.`); + } + + const angularVersion = getInstalledAngularVersion(tree); + const parentPath = options.parent ?? options.module; + if (parentPath && lt(angularVersion, '14.1.0')) { + const parentContent = tree.read(parentPath, 'utf-8'); + const ast = tsquery.ast(parentContent); + + const NG_MODULE_DECORATOR_SELECTOR = + 'ClassDeclaration > Decorator > CallExpression:has(Identifier[name=NgModule])'; + const nodes = tsquery(ast, NG_MODULE_DECORATOR_SELECTOR, { + visitAllChildren: true, + }); + if (nodes.length === 0) { + throw new Error( + `The provided parent path "${parentPath}" does not contain an "NgModule". ` + + 'Please make sure to provide a path to an "NgModule" where the state will be registered. ' + + 'If you are trying to use a "Routes" definition file (for Standalone API usage), ' + + 'please note this is not supported in Angular versions lower than 14.1.0.' + ); + } + } +} diff --git a/packages/angular/src/generators/ngrx/ngrx.spec.ts b/packages/angular/src/generators/ngrx/ngrx.spec.ts index 501c91c0fa150..63413b290b39b 100644 --- a/packages/angular/src/generators/ngrx/ngrx.spec.ts +++ b/packages/angular/src/generators/ngrx/ngrx.spec.ts @@ -9,7 +9,7 @@ import { getAppConfig, getLibConfig, } from '../../utils/nx-devkit/testing'; -import { ngrxVersion } from '../../utils/versions'; +import { ngrxVersion, versions } from '../../utils/versions'; import { ngrxGenerator } from './ngrx'; import applicationGenerator from '../application/application'; import type { NgRxGeneratorOptions } from './schema'; @@ -588,4 +588,115 @@ describe('ngrx', () => { ).toMatchSnapshot(); }); }); + + describe('angular v14 support', () => { + beforeEach(async () => { + jest.clearAllMocks(); + tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + await applicationGenerator(tree, { name: 'myapp' }); + devkit.updateJson(tree, 'package.json', (json) => ({ + ...json, + dependencies: { + ...json.dependencies, + '@angular/core': '14.0.0', + }, + })); + }); + + it('should install the ngrx 14 packages', async () => { + await ngrxGenerator(tree, defaultOptions); + + const packageJson = devkit.readJson(tree, 'package.json'); + expect(packageJson.dependencies['@ngrx/store']).toEqual( + versions.angularV14.ngrxVersion + ); + expect(packageJson.dependencies['@ngrx/effects']).toEqual( + versions.angularV14.ngrxVersion + ); + expect(packageJson.dependencies['@ngrx/entity']).toEqual( + versions.angularV14.ngrxVersion + ); + expect(packageJson.dependencies['@ngrx/router-store']).toEqual( + versions.angularV14.ngrxVersion + ); + expect(packageJson.dependencies['@ngrx/component-store']).toEqual( + versions.angularV14.ngrxVersion + ); + expect(packageJson.devDependencies['@ngrx/schematics']).toEqual( + versions.angularV14.ngrxVersion + ); + expect(packageJson.devDependencies['@ngrx/store-devtools']).toEqual( + versions.angularV14.ngrxVersion + ); + expect(packageJson.devDependencies['jasmine-marbles']).toBeDefined(); + }); + + it('should generate the ngrx effects with no usage of "inject"', async () => { + await ngrxGenerator(tree, defaultOptions); + + expect( + tree.read('apps/myapp/src/app/+state/users.effects.ts', 'utf-8') + ).toMatchSnapshot(); + }); + + it('should generate the ngrx effects using "inject" for versions >= 14.1.0', async () => { + devkit.updateJson(tree, 'package.json', (json) => ({ + ...json, + dependencies: { + ...json.dependencies, + '@angular/core': '14.1.0', + }, + })); + + await ngrxGenerator(tree, defaultOptions); + + expect( + tree.read('apps/myapp/src/app/+state/users.effects.ts', 'utf-8') + ).toMatchSnapshot(); + }); + + it('should generate the ngrx facade with no usage of "inject"', async () => { + await ngrxGenerator(tree, { ...defaultOptions, facade: true }); + + expect( + tree.read('apps/myapp/src/app/+state/users.facade.ts', 'utf-8') + ).toMatchSnapshot(); + }); + + it('should generate the ngrx facade using "inject" for versions >= 14.1.0', async () => { + devkit.updateJson(tree, 'package.json', (json) => ({ + ...json, + dependencies: { + ...json.dependencies, + '@angular/core': '14.1.0', + }, + })); + + await ngrxGenerator(tree, { ...defaultOptions, facade: true }); + + expect( + tree.read('apps/myapp/src/app/+state/users.facade.ts', 'utf-8') + ).toMatchSnapshot(); + }); + + it('should throw when the provided parent does not have an NgModule', async () => { + const parentPath = 'apps/myapp/src/app/app.routes.ts'; + tree.write( + parentPath, + `import { Routes } from '@angular/router'; + import { NxWelcomeComponent } from './nx-welcome.component'; + export const appRoutes: Routes = [{ path: '', component: NxWelcomeComponent }];` + ); + + // ACT & ASSERT + await expect( + ngrxGenerator(tree, { + ...defaultStandaloneOptions, + parent: parentPath, + }) + ).rejects.toThrowError( + `The provided parent path "${parentPath}" does not contain an "NgModule".` + ); + }); + }); }); diff --git a/packages/angular/src/generators/ngrx/ngrx.ts b/packages/angular/src/generators/ngrx/ngrx.ts index ea9826843def9..19b0d2ac3be34 100644 --- a/packages/angular/src/generators/ngrx/ngrx.ts +++ b/packages/angular/src/generators/ngrx/ngrx.ts @@ -6,6 +6,7 @@ import { addNgRxToPackageJson, generateNgrxFilesFromTemplates, normalizeOptions, + validateOptions, } from './lib'; import type { NgRxGeneratorOptions } from './schema'; @@ -13,18 +14,7 @@ export async function ngrxGenerator( tree: Tree, schema: NgRxGeneratorOptions ): Promise { - if (!schema.module && !schema.parent) { - throw new Error('Please provide a value for `--parent`!'); - } - - if (schema.module && !tree.exists(schema.module)) { - throw new Error(`Module does not exist: ${schema.module}.`); - } - - if (schema.parent && !tree.exists(schema.parent)) { - throw new Error(`Parent does not exist: ${schema.parent}.`); - } - + validateOptions(tree, schema); const options = normalizeOptions(schema); if (!options.minimal || !options.root) { diff --git a/packages/angular/src/generators/ngrx/schema.json b/packages/angular/src/generators/ngrx/schema.json index 3f373ce2ec87f..b2024080486e5 100644 --- a/packages/angular/src/generators/ngrx/schema.json +++ b/packages/angular/src/generators/ngrx/schema.json @@ -32,12 +32,12 @@ }, "parent": { "type": "string", - "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.", + "description": "The path to the `NgModule` or the `Routes` definition file (for Standalone API usage) where the feature state will be registered. _Note: The Standalone API usage is only supported in Angular versions >= 14.1.0_.", "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.", + "description": "The route that the Standalone NgRx Providers should be added to. _Note: This is only supported in Angular versions >= 14.1.0_.", "default": "''" }, "directory": { diff --git a/packages/angular/src/utils/version-utils.ts b/packages/angular/src/utils/version-utils.ts new file mode 100644 index 0000000000000..73a632e1c8fd1 --- /dev/null +++ b/packages/angular/src/utils/version-utils.ts @@ -0,0 +1,13 @@ +import { coerce, major } from 'semver'; +import { angularVersion, versions as versionsMap } from './versions'; +import * as versions from './versions'; + +export function getPkgVersionForAngularMajorVersion( + pkgVersionName: Exclude, + angularMajorVersion: number +): string { + return angularMajorVersion < major(coerce(angularVersion)) + ? versionsMap[`angularV${angularMajorVersion}`]?.[pkgVersionName] ?? + versions[pkgVersionName] + : versions[pkgVersionName]; +}