feat(angular): add backwards compat support for the ngrx generator (#14348)
This commit is contained in:
parent
c94ac41f56
commit
295547b3a9
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
@ -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) {}
|
||||
}
|
||||
@ -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 %>());
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
{
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
packages/angular/src/generators/ngrx/lib/validate-options.ts
Normal file
41
packages/angular/src/generators/ngrx/lib/validate-options.ts
Normal 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.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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".`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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": {
|
||||
|
||||
13
packages/angular/src/utils/version-utils.ts
Normal file
13
packages/angular/src/utils/version-utils.ts
Normal 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];
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user