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

This commit is contained in:
Leosvel Pérez Espinosa 2023-01-13 19:50:57 +01:00 committed by GitHub
parent c94ac41f56
commit 295547b3a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 373 additions and 24 deletions

View File

@ -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": {

View File

@ -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"
}

View File

@ -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());
}
}
"
`;

View File

@ -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) {}
}

View File

@ -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 %>());
}
}

View File

@ -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,
{

View File

@ -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(

View File

@ -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';

View File

@ -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.'
);
}
}
}

View File

@ -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".`
);
});
});
});

View File

@ -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<GeneratorCallback> {
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) {

View File

@ -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": {

View File

@ -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<keyof typeof versions, 'versions'>,
angularMajorVersion: number
): string {
return angularMajorVersion < major(coerce(angularVersion))
? versionsMap[`angularV${angularMajorVersion}`]?.[pkgVersionName] ??
versions[pkgVersionName]
: versions[pkgVersionName];
}