Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(angular): add backwards compat support for the ngrx generator #14348

Merged
merged 1 commit into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/generated/packages/angular/generators/ngrx.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
10 changes: 9 additions & 1 deletion packages/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
Expand Down
114 changes: 114 additions & 0 deletions packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
"
`;
Original file line number Diff line number Diff line change
@@ -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) {}
}
Original file line number Diff line number Diff line change
@@ -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 %>());
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
{
Expand Down
18 changes: 17 additions & 1 deletion packages/angular/src/generators/ngrx/lib/generate-files.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand All @@ -14,7 +16,7 @@ export function generateNgrxFilesFromTemplates(

generateFiles(
tree,
joinPathFragments(__dirname, '..', 'files'),
joinPathFragments(__dirname, '..', 'files', 'latest'),
options.parentDirectory,
{
...options,
Expand All @@ -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(
Expand Down
1 change: 1 addition & 0 deletions packages/angular/src/generators/ngrx/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
41 changes: 41 additions & 0 deletions packages/angular/src/generators/ngrx/lib/validate-options.ts
Original file line number Diff line number Diff line change
@@ -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.'
);
}
}
}
Loading