diff --git a/docs/generated/packages/angular/generators/ngrx.json b/docs/generated/packages/angular/generators/ngrx.json index 3c7d7d713ce22..f017e5832121d 100644 --- a/docs/generated/packages/angular/generators/ngrx.json +++ b/docs/generated/packages/angular/generators/ngrx.json @@ -26,6 +26,12 @@ "x-prompt": "What name would you like to use for the NgRx feature state? An example would be `users`." }, "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?" @@ -74,7 +80,7 @@ } }, "additionalProperties": false, - "required": ["module", "name"], + "required": ["name"], "presets": [] }, "description": "Adds NgRx support to an application or library.", diff --git a/e2e/angular-extensions/src/ngrx.test.ts b/e2e/angular-extensions/src/ngrx.test.ts index 2a8039ba150b1..aa5116e72853a 100644 --- a/e2e/angular-extensions/src/ngrx.test.ts +++ b/e2e/angular-extensions/src/ngrx.test.ts @@ -27,7 +27,7 @@ describe('Angular Package', () => { // Generate root ngrx state management runCLI( - `generate @nrwl/angular:ngrx users --module=apps/${myapp}/src/app/app.module.ts --root --minimal=false` + `generate @nrwl/angular:ngrx users --parent=apps/${myapp}/src/app/app.module.ts --root --minimal=false` ); const packageJson = readJson('package.json'); expect(packageJson.dependencies['@ngrx/store']).toBeDefined(); @@ -39,7 +39,7 @@ describe('Angular Package', () => { // Generate feature library and ngrx state within that library runCLI(`g @nrwl/angular:lib ${mylib} --prefix=fl`); runCLI( - `generate @nrwl/angular:ngrx flights --module=libs/${mylib}/src/lib/${mylib}.module.ts --facade` + `generate @nrwl/angular:ngrx flights --parent=libs/${mylib}/src/lib/${mylib}.module.ts --facade` ); expect(runCLI(`build ${myapp}`)).toMatch(/main\.[a-z0-9]+\.js/); @@ -56,7 +56,40 @@ describe('Angular Package', () => { // Generate root ngrx state management runCLI( - `generate @nrwl/angular:ngrx users --module=apps/${myapp}/src/app/app.module.ts --root` + `generate @nrwl/angular:ngrx users --parent=apps/${myapp}/src/app/app.module.ts --root` + ); + const packageJson = readJson('package.json'); + expect(packageJson.dependencies['@ngrx/entity']).toBeDefined(); + expect(packageJson.dependencies['@ngrx/store']).toBeDefined(); + expect(packageJson.dependencies['@ngrx/effects']).toBeDefined(); + expect(packageJson.dependencies['@ngrx/router-store']).toBeDefined(); + expect(packageJson.devDependencies['@ngrx/schematics']).toBeDefined(); + expect(packageJson.devDependencies['@ngrx/store-devtools']).toBeDefined(); + + const mylib = uniq('mylib'); + // Generate feature library and ngrx state within that library + runCLI(`g @nrwl/angular:lib ${mylib} --prefix=fl`); + + const flags = `--facade --barrels`; + runCLI( + `generate @nrwl/angular:ngrx flights --parent=libs/${mylib}/src/lib/${mylib}.module.ts ${flags}` + ); + + expect(runCLI(`build ${myapp}`)).toMatch(/main\.[a-z0-9]+\.js/); + expectTestsPass(await runCLIAsync(`test ${myapp} --no-watch`)); + // TODO: remove this condition + if (getSelectedPackageManager() !== 'pnpm') { + expectTestsPass(await runCLIAsync(`test ${mylib} --no-watch`)); + } + }, 1000000); + + it('should work with creators using --module', async () => { + const myapp = uniq('myapp'); + runCLI(`generate @nrwl/angular:app ${myapp} --routing --no-interactive`); + + // Generate root ngrx state management + runCLI( + `generate @nrwl/angular:ngrx users --parent=apps/${myapp}/src/app/app.module.ts --root` ); const packageJson = readJson('package.json'); expect(packageJson.dependencies['@ngrx/entity']).toBeDefined(); 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 6a072ee414fc9..4b64ceddc7bc1 100644 --- a/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap +++ b/packages/angular/src/generators/ngrx/__snapshots__/ngrx.spec.ts.snap @@ -273,6 +273,32 @@ 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`] = ` +" + import { NgModule } from '@angular/core'; + import { BrowserModule } from '@angular/platform-browser'; + import { RouterModule } from '@angular/router'; + import { AppComponent } from './app.component'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import * as fromUsers from './+state/users.reducer'; +import { UsersEffects } from './+state/users.effects'; +import { StoreRouterConnectingModule } from '@ngrx/router-store'; + @NgModule({ + imports: [BrowserModule, RouterModule.forRoot([]), StoreModule.forRoot({}, { + metaReducers: [], + runtimeChecks: { + strictActionImmutability: true, + strictStateImmutability: true + } + }), EffectsModule.forRoot([UsersEffects]), StoreRouterConnectingModule.forRoot(), StoreModule.forFeature(fromUsers.USERS_FEATURE_KEY, fromUsers.usersReducer)], + declarations: [AppComponent], + bootstrap: [AppComponent] + }) + export class AppModule {} + " +`; + exports[`ngrx should add an empty root module when minimal and root are set to true 1`] = ` " import { NgModule } from '@angular/core'; diff --git a/packages/angular/src/generators/ngrx/lib/add-exports-barrel.ts b/packages/angular/src/generators/ngrx/lib/add-exports-barrel.ts index ee7e54e726cf5..043fc47a331be 100644 --- a/packages/angular/src/generators/ngrx/lib/add-exports-barrel.ts +++ b/packages/angular/src/generators/ngrx/lib/add-exports-barrel.ts @@ -1,16 +1,15 @@ import type { Tree } from '@nrwl/devkit'; import { joinPathFragments, names } from '@nrwl/devkit'; import { addGlobal } from '@nrwl/workspace/src/utilities/ast-utils'; -import { dirname } from 'path'; import { createSourceFile, ScriptTarget } from 'typescript'; -import type { NgRxGeneratorOptions } from '../schema'; +import type { NormalizedNgRxGeneratorOptions } from './normalize-options'; /** * Add ngrx feature exports to the public barrel in the feature library */ export function addExportsToBarrel( tree: Tree, - options: NgRxGeneratorOptions + options: NormalizedNgRxGeneratorOptions ): void { // Don't update the public barrel for the root state, only for feature states if (options.root) { @@ -18,7 +17,7 @@ export function addExportsToBarrel( } const indexFilePath = joinPathFragments( - dirname(options.module), + options.parentDirectory, '..', 'index.ts' ); 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 0eb47890ad8e1..0a803e0edddef 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 @@ -7,16 +7,16 @@ import { addImportToModule, addProviderToModule, } from '../../../utils/nx-devkit/ast-utils'; -import type { NgRxGeneratorOptions } from '../schema'; +import type { NormalizedNgRxGeneratorOptions } from './normalize-options'; export function addImportsToModule( tree: Tree, - options: NgRxGeneratorOptions + options: NormalizedNgRxGeneratorOptions ): void { - const modulePath = options.module; - const sourceText = tree.read(modulePath, 'utf-8'); + const parentPath = options.module ?? options.parent; + const sourceText = tree.read(parentPath, 'utf-8'); let sourceFile = createSourceFile( - modulePath, + parentPath, sourceText, ScriptTarget.Latest, true @@ -30,7 +30,7 @@ export function addImportsToModule( return insertImport( tree, source, - modulePath, + parentPath, symbolName, fileName, isDefault @@ -72,11 +72,11 @@ export function addImportsToModule( sourceFile = addImport(sourceFile, 'EffectsModule', '@ngrx/effects'); if (options.minimal && options.root) { - sourceFile = addImportToModule(tree, sourceFile, modulePath, storeForRoot); + sourceFile = addImportToModule(tree, sourceFile, parentPath, storeForRoot); sourceFile = addImportToModule( tree, sourceFile, - modulePath, + parentPath, effectsForEmptyRoot ); @@ -89,7 +89,7 @@ export function addImportsToModule( sourceFile = addImportToModule( tree, sourceFile, - modulePath, + parentPath, storeRouterModule ); } @@ -103,7 +103,7 @@ export function addImportsToModule( sourceFile = addProviderToModule( tree, sourceFile, - modulePath, + parentPath, facadeName ); } @@ -117,13 +117,13 @@ export function addImportsToModule( sourceFile = addImportToModule( tree, sourceFile, - modulePath, + parentPath, storeForRoot ); sourceFile = addImportToModule( tree, sourceFile, - modulePath, + parentPath, effectsForRoot ); @@ -136,7 +136,7 @@ export function addImportsToModule( sourceFile = addImportToModule( tree, sourceFile, - modulePath, + parentPath, storeRouterModule ); } @@ -144,7 +144,7 @@ export function addImportsToModule( sourceFile = addImportToModule( tree, sourceFile, - modulePath, + parentPath, storeForFeature ); } else { @@ -153,13 +153,13 @@ export function addImportsToModule( sourceFile = addImportToModule( tree, sourceFile, - modulePath, + parentPath, storeForFeature ); sourceFile = addImportToModule( tree, sourceFile, - modulePath, + parentPath, effectsForFeature ); } diff --git a/packages/angular/src/generators/ngrx/lib/generate-files.ts b/packages/angular/src/generators/ngrx/lib/generate-files.ts index cc92ea7af3337..634c4395607f1 100644 --- a/packages/angular/src/generators/ngrx/lib/generate-files.ts +++ b/packages/angular/src/generators/ngrx/lib/generate-files.ts @@ -1,36 +1,39 @@ import type { Tree } from '@nrwl/devkit'; import { generateFiles, joinPathFragments, names } from '@nrwl/devkit'; -import { dirname } from 'path'; -import type { NgRxGeneratorOptions } from '../schema'; +import { NormalizedNgRxGeneratorOptions } from './normalize-options'; /** * Generate 'feature' scaffolding: actions, reducer, effects, interfaces, selectors, facade */ export function generateNgrxFilesFromTemplates( tree: Tree, - options: NgRxGeneratorOptions + options: NormalizedNgRxGeneratorOptions ): void { const name = options.name; - const moduleDir = dirname(options.module); const projectNames = names(name); - generateFiles(tree, joinPathFragments(__dirname, '..', 'files'), moduleDir, { - ...options, - ...projectNames, - tmpl: '', - }); + generateFiles( + tree, + joinPathFragments(__dirname, '..', 'files'), + options.parentDirectory, + { + ...options, + ...projectNames, + tmpl: '', + } + ); if (!options.facade) { tree.delete( joinPathFragments( - moduleDir, + options.parentDirectory, options.directory, `${projectNames.fileName}.facade.ts` ) ); tree.delete( joinPathFragments( - moduleDir, + options.parentDirectory, options.directory, `${projectNames.fileName}.facade.spec.ts` ) diff --git a/packages/angular/src/generators/ngrx/lib/normalize-options.ts b/packages/angular/src/generators/ngrx/lib/normalize-options.ts index 818e2ae933f93..7559011d69642 100644 --- a/packages/angular/src/generators/ngrx/lib/normalize-options.ts +++ b/packages/angular/src/generators/ngrx/lib/normalize-options.ts @@ -1,11 +1,21 @@ import { names } from '@nrwl/devkit'; import type { NgRxGeneratorOptions } from '../schema'; +import { dirname } from 'path'; + +export type NormalizedNgRxGeneratorOptions = NgRxGeneratorOptions & { + parentDirectory: string; +}; export function normalizeOptions( options: NgRxGeneratorOptions -): NgRxGeneratorOptions { +): NormalizedNgRxGeneratorOptions { return { ...options, + parentDirectory: options.module + ? dirname(options.module) + : options.parent + ? dirname(options.parent) + : undefined, 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 24e12108163ca..5b57f213bf3db 100644 --- a/packages/angular/src/generators/ngrx/ngrx.spec.ts +++ b/packages/angular/src/generators/ngrx/ngrx.spec.ts @@ -19,6 +19,13 @@ describe('ngrx', () => { let tree: Tree; const defaultOptions: NgRxGeneratorOptions = { + directory: '+state', + minimal: true, + parent: 'apps/myapp/src/app/app.module.ts', + name: 'users', + }; + + const defaultModuleOptions: NgRxGeneratorOptions = { directory: '+state', minimal: true, module: 'apps/myapp/src/app/app.module.ts', @@ -49,6 +56,17 @@ describe('ngrx', () => { ).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, @@ -100,6 +118,18 @@ describe('ngrx', () => { ).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); diff --git a/packages/angular/src/generators/ngrx/ngrx.ts b/packages/angular/src/generators/ngrx/ngrx.ts index 580df8badc4f1..ea9826843def9 100644 --- a/packages/angular/src/generators/ngrx/ngrx.ts +++ b/packages/angular/src/generators/ngrx/ngrx.ts @@ -11,29 +11,37 @@ import type { NgRxGeneratorOptions } from './schema'; export async function ngrxGenerator( tree: Tree, - options: NgRxGeneratorOptions + schema: NgRxGeneratorOptions ): Promise { - const normalizedOptions = normalizeOptions(options); + 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 (!tree.exists(normalizedOptions.module)) { - throw new Error(`Module does not exist: ${normalizedOptions.module}.`); + if (schema.parent && !tree.exists(schema.parent)) { + throw new Error(`Parent does not exist: ${schema.parent}.`); } - if (!normalizedOptions.minimal || !normalizedOptions.root) { - generateNgrxFilesFromTemplates(tree, normalizedOptions); + const options = normalizeOptions(schema); + + if (!options.minimal || !options.root) { + generateNgrxFilesFromTemplates(tree, options); } - if (!normalizedOptions.skipImport) { - addImportsToModule(tree, normalizedOptions); - addExportsToBarrel(tree, normalizedOptions); + if (!options.skipImport) { + addImportsToModule(tree, options); + addExportsToBarrel(tree, options); } let packageInstallationTask: GeneratorCallback = () => {}; - if (!normalizedOptions.skipPackageJson) { + if (!options.skipPackageJson) { packageInstallationTask = addNgRxToPackageJson(tree); } - if (!normalizedOptions.skipFormat) { + if (!options.skipFormat) { await formatFiles(tree); } diff --git a/packages/angular/src/generators/ngrx/schema.d.ts b/packages/angular/src/generators/ngrx/schema.d.ts index d6695482d5296..04bbcf2109458 100644 --- a/packages/angular/src/generators/ngrx/schema.d.ts +++ b/packages/angular/src/generators/ngrx/schema.d.ts @@ -1,7 +1,8 @@ export interface NgRxGeneratorOptions { directory: string; minimal: boolean; - module: string; + module?: string; + parent?: 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 f9b9ca1ef96bc..e4de368f38bc4 100644 --- a/packages/angular/src/generators/ngrx/schema.json +++ b/packages/angular/src/generators/ngrx/schema.json @@ -26,6 +26,12 @@ "x-prompt": "What name would you like to use for the NgRx feature state? An example would be `users`." }, "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?" @@ -74,5 +80,5 @@ } }, "additionalProperties": false, - "required": ["module", "name"] + "required": ["name"] }