From a75608ede00573e6c8678558d28b26187d172aa3 Mon Sep 17 00:00:00 2001 From: profanis Date: Sun, 3 Dec 2023 00:05:08 +0200 Subject: [PATCH 1/3] fix(schematics): replace the deprecated api in the unit tests and add a missing decorator (#2088) --- .../store/auth/auth.state.spec.ts__template__ | 24 ++++++++++----- .../store/auth/auth.state.ts__template__ | 2 ++ .../dictionary.state.spec.ts__template__ | 30 ++++++++++++++----- .../dictionary.state.ts__template__ | 2 ++ .../user/user.state.spec.ts__template__ | 22 +++++++++----- .../states/user/user.state.ts__template__ | 2 ++ .../files/__name__.state.spec.ts__template__ | 16 +++++----- .../state/files/__name__.state.ts__template__ | 2 ++ .../files/__name__.state.spec.ts__template__ | 11 +++---- .../store/files/__name__.state.ts__template__ | 2 ++ .../store/schematics/src/utils/versions.json | 2 +- 11 files changed, 78 insertions(+), 37 deletions(-) diff --git a/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.spec.ts__template__ b/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.spec.ts__template__ index f22f777f3..aca577a5b 100644 --- a/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.spec.ts__template__ +++ b/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.spec.ts__template__ @@ -1,21 +1,21 @@ import { NgxsModule, Store } from '@ngxs/store'; -import { async, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { AuthenticationStateModel, AuthState } from './auth.state'; import { SetAuthData } from './auth.actions'; describe('[TEST]: AuthStore', () => { let store: Store; - beforeEach(async(() => { + beforeEach(() => { TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([AuthState])] - }) - .compileComponents() - .then(); - store = TestBed.get(Store); - })); + imports: [NgxsModule.forRoot([AuthState])], + }); + + store = TestBed.inject(Store); + }); it('Should be correct dispatch and value is empty', () => { + // Arrange const Authentication: AuthenticationStateModel = { id: '', firstName: '', @@ -24,12 +24,17 @@ describe('[TEST]: AuthStore', () => { email: '', roles: [] }; + + // Act store.dispatch(new SetAuthData(Authentication)); const actual = store.selectSnapshot(AuthState.getAuthData); + + // Assert expect(actual).toEqual(Authentication); }); it('Should be correct dispatch and next value is correct completed', () => { + // Arrange const authentication: AuthenticationStateModel = { id: '12', firstName: 'Adam', @@ -39,8 +44,11 @@ describe('[TEST]: AuthStore', () => { roles: ['ADMIN'] }; + // Act store.dispatch(new SetAuthData(authentication)); const actual = store.selectSnapshot(AuthState.getAuthData); + + // Assert expect(actual).toEqual(authentication); }); }); diff --git a/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.ts__template__ b/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.ts__template__ index 2bb2c028d..0af651207 100644 --- a/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.ts__template__ +++ b/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.ts__template__ @@ -1,3 +1,4 @@ +import { Injectable } from '@angular/core'; import { Action, Selector, State, StateContext } from '@ngxs/store'; import { SetAuthData } from './auth.actions'; @@ -21,6 +22,7 @@ export interface AuthenticationStateModel { roles: [] } }) +@Injectable() export class AuthState { @Selector() public static getAuthData(state: AuthenticationStateModel): AuthenticationStateModel { diff --git a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.spec.ts__template__ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.spec.ts__template__ index 0dd789824..438c999a5 100644 --- a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.spec.ts__template__ +++ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.spec.ts__template__ @@ -1,5 +1,5 @@ import { NgxsModule, Store } from '@ngxs/store'; -import { async, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { DictionaryState, DictionaryStateModel } from './dictionary.state'; import { DictionaryReset, SetDictionaryData } from './dictionary.actions'; @@ -27,16 +27,16 @@ const data = [ describe('[TEST]: Dictionary state', () => { let store: Store; - beforeEach(async(() => { + beforeEach(() => { TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([DictionaryState])] - }) - .compileComponents() - .then(); - store = TestBed.get(Store); - })); + imports: [NgxsModule.forRoot([DictionaryState])], + }); + + store = TestBed.inject(Store); + }); it('Should be correct dispatch and dictionary is empty', () => { + // Arrange const dictionary: DictionaryStateModel = { content: [], page: 0, @@ -44,12 +44,17 @@ describe('[TEST]: Dictionary state', () => { totalPages: 0, totalElements: 0 }; + + // Act store.dispatch(new SetDictionaryData(dictionary)); const actual = store.selectSnapshot(DictionaryState.getDictionaryState); + + // Assert expect(actual).toEqual(dictionary); }); it('Should be state is filled DictionaryStateModel', () => { + // Arrange const dictionary: DictionaryStateModel = { content: data, page: 0, @@ -57,12 +62,17 @@ describe('[TEST]: Dictionary state', () => { totalPages: 2, totalElements: 1 }; + + // Act store.dispatch(new SetDictionaryData(dictionary)); const actual = store.selectSnapshot(DictionaryState.getDictionaryState); + + // Assert expect(actual).toEqual(dictionary); }); it('should be reset state', function () { + // Arrange const dictionary: DictionaryStateModel = { content: [], page: 0, @@ -70,8 +80,12 @@ describe('[TEST]: Dictionary state', () => { totalPages: 0, totalElements: 0 }; + + // Act store.dispatch(new DictionaryReset()); const actual = store.selectSnapshot(DictionaryState.getDictionaryState); + + // Assert expect(actual).toEqual(dictionary); }); }); diff --git a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.ts__template__ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.ts__template__ index 9c67d2cd4..861a6c250 100644 --- a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.ts__template__ +++ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.ts__template__ @@ -1,3 +1,4 @@ +import { Injectable } from '@angular/core'; import { Action, Selector, State, StateContext } from '@ngxs/store'; import { DictionaryReset, SetDictionaryData } from './dictionary.actions'; @@ -19,6 +20,7 @@ export interface DictionaryStateModel { totalElements: 0 } }) +@Injectable() export class DictionaryState { @Selector() public static getDictionaryState(state: DictionaryStateModel): DictionaryStateModel { diff --git a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.spec.ts__template__ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.spec.ts__template__ index 424918d77..ee3b5048e 100644 --- a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.spec.ts__template__ +++ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.spec.ts__template__ @@ -1,21 +1,21 @@ import { NgxsModule, Store } from '@ngxs/store'; -import { async, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { UserStateModel, UserState } from './user.state'; import { SetUser } from './user.actions'; describe('[TEST]: User state', () => { let store: Store; - beforeEach(async(() => { + beforeEach(() => { TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([UserState])] - }) - .compileComponents() - .then(); - store = TestBed.get(Store); - })); + imports: [NgxsModule.forRoot([UserState])], + }); + + store = TestBed.inject(Store); + }); it('Should be state is UserStateModel', () => { + // Arrange const user: UserStateModel = { userId: '', departmentCode: '', @@ -27,13 +27,17 @@ describe('[TEST]: User state', () => { positionId: '', positionName: '' }; + + // Act store.dispatch(new SetUser(user)); const actual = store.selectSnapshot(({ user }) => user); + // Assert expect(actual).toEqual(user); }); it('Should be state is filled UserStateModel', () => { + // Arrange const user: UserStateModel = { userId: '12', departmentCode: '2392', @@ -46,9 +50,11 @@ describe('[TEST]: User state', () => { positionName: 'admin' }; + // Act store.dispatch(new SetUser(user)); const actual = store.selectSnapshot(({ user }) => user); + // Assert expect(actual).toEqual(user); }); }); diff --git a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.ts__template__ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.ts__template__ index fa28508d4..ad230f354 100644 --- a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.ts__template__ +++ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.ts__template__ @@ -1,3 +1,4 @@ +import { Injectable } from '@angular/core'; import { Action, Selector, State, StateContext } from '@ngxs/store'; import { SetUser } from './user.actions'; @@ -27,6 +28,7 @@ export interface UserStateModel { departmentName: '' } }) +@Injectable() export class UserState { @Selector() public static getUser(state: UserStateModel): UserStateModel { diff --git a/packages/store/schematics/src/state/files/__name__.state.spec.ts__template__ b/packages/store/schematics/src/state/files/__name__.state.spec.ts__template__ index 2ced8c919..e3f4180ec 100644 --- a/packages/store/schematics/src/state/files/__name__.state.spec.ts__template__ +++ b/packages/store/schematics/src/state/files/__name__.state.spec.ts__template__ @@ -1,15 +1,17 @@ -import { TestBed, async } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { NgxsModule, Store } from '@ngxs/store'; import { <%= classify(name) %>State, <%= classify(name) %>StateModel } from './<%= dasherize(name) %>.state'; describe('<%= classify(name) %> state', () => { let store: Store; - beforeEach(async(() => { - TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([<%= classify(name) %>State])] - }).compileComponents(); - store = TestBed.get(Store); - })); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NgxsModule.forRoot([<%= classify(name) %>State])] + }); + + store = TestBed.inject(Store); + }); it('should create an empty state', () => { const actual = store.selectSnapshot(<%= classify(name) %>State.getState); diff --git a/packages/store/schematics/src/state/files/__name__.state.ts__template__ b/packages/store/schematics/src/state/files/__name__.state.ts__template__ index 2f0c31315..79f4b4fea 100644 --- a/packages/store/schematics/src/state/files/__name__.state.ts__template__ +++ b/packages/store/schematics/src/state/files/__name__.state.ts__template__ @@ -1,3 +1,4 @@ +import { Injectable } from '@angular/core'; import { State, Selector } from '@ngxs/store'; export interface <%= classify(name) %>StateModel { @@ -10,6 +11,7 @@ export interface <%= classify(name) %>StateModel { items: [] } }) +@Injectable() export class <%= classify(name) %>State { @Selector() diff --git a/packages/store/schematics/src/store/files/__name__.state.spec.ts__template__ b/packages/store/schematics/src/store/files/__name__.state.spec.ts__template__ index 467b72742..4e9ae01ad 100644 --- a/packages/store/schematics/src/store/files/__name__.state.spec.ts__template__ +++ b/packages/store/schematics/src/store/files/__name__.state.spec.ts__template__ @@ -1,16 +1,17 @@ -import { TestBed, async } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { NgxsModule, Store } from '@ngxs/store'; import { <%= classify(name) %>State, <%= classify(name) %>StateModel } from './<%= dasherize(name) %>.state'; import { <%= classify(name) %>Action } from './<%= dasherize(name) %>.actions'; describe('<%= classify(name) %> store', () => { let store: Store; - beforeEach(async(() => { + beforeEach(() => { TestBed.configureTestingModule({ imports: [NgxsModule.forRoot([<%= classify(name) %>State])] - }).compileComponents(); - store = TestBed.get(Store); - })); + }); + + store = TestBed.inject(Store); + }); it('should create an action and add an item', () => { const expected: <%= classify(name) %>StateModel = { diff --git a/packages/store/schematics/src/store/files/__name__.state.ts__template__ b/packages/store/schematics/src/store/files/__name__.state.ts__template__ index e212c4f99..f4bfbb2bc 100644 --- a/packages/store/schematics/src/store/files/__name__.state.ts__template__ +++ b/packages/store/schematics/src/store/files/__name__.state.ts__template__ @@ -1,3 +1,4 @@ +import { Injectable } from '@angular/core'; import { State, Action, Selector, StateContext } from '@ngxs/store'; import { <%= classify(name) %>Action } from './<%= dasherize(name) %>.actions'; @@ -11,6 +12,7 @@ export interface <%= classify(name) %>StateModel { items: [] } }) +@Injectable() export class <%= classify(name) %>State { @Selector() diff --git a/packages/store/schematics/src/utils/versions.json b/packages/store/schematics/src/utils/versions.json index c0b690178..5060e793f 100644 --- a/packages/store/schematics/src/utils/versions.json +++ b/packages/store/schematics/src/utils/versions.json @@ -1,3 +1,3 @@ { - "@ngxs/store": "3.8.1" + "@ngxs/store": "3.8.2" } From ce98589803421fdc848704ded7521ebfc86b1225 Mon Sep 17 00:00:00 2001 From: profanis Date: Fri, 15 Dec 2023 17:22:18 +0200 Subject: [PATCH 2/3] feat: state, store and starter-kit schematics support a project option (#2089) * feat: state, store and starter-kit schematics support a project option * fix: create a schematics utils files to reduce redundancy, and test standalone apps * fix: solve codeclimate issues * fix: move schematics testing file under schematics dir --- docs/concepts/state.md | 15 +- docs/concepts/store.md | 15 +- docs/introduction/starter-kit.md | 11 +- packages/store/internals/testing/src/index.ts | 4 +- .../store/schematics/src/_testing/index.ts | 1 + .../schematics/src/_testing/schematics.ts | 77 ++++++++ .../src/ng-add/ng-add.factory.spec.ts | 184 +++++++++++------- .../store/auth/auth.state.spec.ts__template__ | 5 +- .../dictionary.state.spec.ts__template__ | 5 +- .../user/user.state.spec.ts__template__ | 5 +- .../starter-kit/starter-kit.factory.spec.ts | 151 ++++++++++---- .../src/starter-kit/starter-kit.factory.ts | 22 ++- .../src/starter-kit/starter-kit.schema.d.ts | 4 + .../files/__name__.state.spec.ts__template__ | 5 +- .../store/schematics/src/state/schema.json | 4 + .../src/state/state.factory.spec.ts | 118 ++++++++--- .../schematics/src/state/state.factory.ts | 31 +-- .../schematics/src/state/state.schema.d.ts | 4 + .../files/__name__.state.spec.ts__template__ | 5 +- .../store/schematics/src/store/schema.json | 4 + .../src/store/store.factory.spec.ts | 106 +++++++--- .../schematics/src/store/store.factory.ts | 31 +-- .../schematics/src/store/store.schema.d.ts | 4 + packages/store/tsconfig.schematics.json | 3 +- 24 files changed, 594 insertions(+), 220 deletions(-) create mode 100644 packages/store/schematics/src/_testing/index.ts create mode 100644 packages/store/schematics/src/_testing/schematics.ts diff --git a/docs/concepts/state.md b/docs/concepts/state.md index 0f4e533d9..06d9f518e 100644 --- a/docs/concepts/state.md +++ b/docs/concepts/state.md @@ -16,12 +16,15 @@ You have the option to enter the options yourself ng generate @ngxs/store:state --name NAME_OF_YOUR_STATE ``` -| Option | Description | Required | Default Value | -| :----- | :------------------------------------------------------------- | :------: | :------------------- | -| --name | The name of the state | Yes | | -| --path | The path to create the state | No | App's root directory | -| --spec | Boolean flag to indicate if a unit test file should be created | No | `true` | -| --flat | Boolean flag to indicate if a dir is created | No | `false` | +| Option | Description | Required | Default Value | +| :-------- | :------------------------------------------------------------- | :------: | :-------------------------- | +| --name | The name of the state | Yes | | +| --path | The path to create the state | No | App's root directory | +| --spec | Boolean flag to indicate if a unit test file should be created | No | `true` | +| --flat | Boolean flag to indicate if a dir is created | No | `false` | +| --project | Name of the project as it is defined in your angular.json | No | Workspace's default project | + +> When working with multiple projects within a workspace, you can explicitly specify the `project` where you want to install the **state**. The schematic will automatically detect whether the provided project is a standalone or not, and it will generate the necessary files accordingly. 🪄 **This command will**: diff --git a/docs/concepts/store.md b/docs/concepts/store.md index 5a3df0c42..ebc0e3701 100644 --- a/docs/concepts/store.md +++ b/docs/concepts/store.md @@ -18,12 +18,15 @@ You have the option to enter the options yourself ng generate @ngxs/store:store --name NAME_OF_YOUR_STORE ``` -| Option | Description | Required | Default Value | -| :----- | :------------------------------------------------------------- | :------: | :------------------- | -| --name | The name of the store | Yes | | -| --path | The path to create the store | No | App's root directory | -| --spec | Boolean flag to indicate if a unit test file should be created | No | `true` | -| --flat | Boolean flag to indicate if a dir is created | No | `false` | +| Option | Description | Required | Default Value | +| :-------- | :------------------------------------------------------------- | :------: | :-------------------------- | +| --name | The name of the store | Yes | | +| --path | The path to create the store | No | App's root directory | +| --spec | Boolean flag to indicate if a unit test file should be created | No | `true` | +| --flat | Boolean flag to indicate if a dir is created | No | `false` | +| --project | Name of the project as it is defined in your angular.json | No | Workspace's default project | + +> When working with multiple projects within a workspace, you can explicitly specify the `project` where you want to install the **store**. The schematic will automatically detect whether the provided project is a standalone or not, and it will generate the necessary files accordingly. 🪄 **This command will**: diff --git a/docs/introduction/starter-kit.md b/docs/introduction/starter-kit.md index 60a5f0b77..5b710055e 100644 --- a/docs/introduction/starter-kit.md +++ b/docs/introduction/starter-kit.md @@ -16,10 +16,13 @@ You have the option to enter the options yourself ng generate @ngxs/store:starter-kit --path YOUR_PATH ``` -| Option | Description | Required | Default Value | -| :----- | :------------------------------------------------------------- | :------: | :------------ | -| --path | The path to create the starter kit | Yes | | -| --spec | Boolean flag to indicate if a unit test file should be created | No | `true` | +| Option | Description | Required | Default Value | +| :-------- | :------------------------------------------------------------- | :------: | :-------------------------- | +| --path | The path to create the starter kit | Yes | | +| --spec | Boolean flag to indicate if a unit test file should be created | No | `true` | +| --project | Name of the project as it is defined in your angular.json | No | Workspace's default project | + +> When working with multiple projects within a workspace, you can explicitly specify the `project` where you want to install the **starter kit**. The schematic will automatically detect whether the provided project is a standalone or not, and it will generate the necessary files accordingly. 🪄 **This command will**: diff --git a/packages/store/internals/testing/src/index.ts b/packages/store/internals/testing/src/index.ts index 0dee0a917..ad8316dcd 100644 --- a/packages/store/internals/testing/src/index.ts +++ b/packages/store/internals/testing/src/index.ts @@ -1,4 +1,4 @@ -export { NgxsTestBed } from './ngxs.setup'; -export { NgxsTesting } from './symbol'; export { freshPlatform } from './fresh-platform'; +export { NgxsTestBed } from './ngxs.setup'; export { skipConsoleLogging } from './skip-console-logging'; +export { NgxsTesting } from './symbol'; diff --git a/packages/store/schematics/src/_testing/index.ts b/packages/store/schematics/src/_testing/index.ts new file mode 100644 index 000000000..094690335 --- /dev/null +++ b/packages/store/schematics/src/_testing/index.ts @@ -0,0 +1 @@ +export * from './schematics'; diff --git a/packages/store/schematics/src/_testing/schematics.ts b/packages/store/schematics/src/_testing/schematics.ts new file mode 100644 index 000000000..b2a932dcf --- /dev/null +++ b/packages/store/schematics/src/_testing/schematics.ts @@ -0,0 +1,77 @@ +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { workspaceRoot } from '@nrwl/devkit'; +import { Schema as ApplicationOptions } from '@schematics/angular/application/schema'; +import { Schema as WorkspaceOptions } from '@schematics/angular/workspace/schema'; +import * as path from 'path'; + +const angularSchematicRunner = new SchematicTestRunner( + '@schematics/angular', + path.join(workspaceRoot, 'node_modules/@schematics/angular/collection.json') +); + +const defaultWorkspaceOptions: WorkspaceOptions = { + name: 'workspace', + newProjectRoot: 'projects', + version: '1.0.0' +}; + +const defaultAppOptions: ApplicationOptions = { + name: 'foo', + inlineStyle: false, + inlineTemplate: false, + routing: true, + skipTests: false, + skipPackageJson: false +}; + +export async function createWorkspace( + isStandalone = false, + schematicRunner = angularSchematicRunner, + workspaceOptions = defaultWorkspaceOptions, + appOptions = defaultAppOptions +) { + let appTree: UnitTestTree = await schematicRunner.runSchematic( + 'workspace', + workspaceOptions + ); + appTree = await schematicRunner.runSchematic('application', appOptions, appTree); + + isStandalone && updateToStandalone(appTree, workspaceOptions, appOptions); + + return appTree; +} + +// Note: This is a workaround to convert the application as standalone. Should be removed when migrating to NG17 +function updateToStandalone( + appTree: UnitTestTree, + workspaceOptions: WorkspaceOptions, + appOptions: ApplicationOptions +) { + const mainTsContent = ` + import { + bootstrapApplication, + } from '@angular/platform-browser'; + import { AppComponent } from './app/app.component'; + + bootstrapApplication(AppComponent).catch((err) => + console.error(err), + ); + `; + + const appComponentContent = ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + standalone: true, + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'], + }) + export class AppComponent {} + `; + + const projectPath = `/${workspaceOptions.newProjectRoot}/${appOptions.name}`; + appTree.overwrite(`${projectPath}/src/main.ts`, mainTsContent); + appTree.overwrite(`${projectPath}/src/app/app.component.ts`, appComponentContent); + appTree.delete(`${projectPath}/src/app/app.module.ts`); +} diff --git a/packages/store/schematics/src/ng-add/ng-add.factory.spec.ts b/packages/store/schematics/src/ng-add/ng-add.factory.spec.ts index fe4be66f8..66ce65e18 100644 --- a/packages/store/schematics/src/ng-add/ng-add.factory.spec.ts +++ b/packages/store/schematics/src/ng-add/ng-add.factory.spec.ts @@ -1,101 +1,151 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import { workspaceRoot } from '@nrwl/devkit'; import { join } from 'path'; -import { Schema as ApplicationOptions } from '@schematics/angular/application/schema'; -import { Schema as WorkspaceOptions } from '@schematics/angular/workspace/schema'; - +import { createWorkspace } from '../_testing'; import { LIBRARIES } from '../utils/common/lib.config'; import { NgxsPackageSchema } from './ng-add.schema'; describe('Ngxs ng-add Schematic', () => { - const angularSchematicRunner = new SchematicTestRunner( - '@schematics/angular', - join(workspaceRoot, 'node_modules/@schematics/angular/collection.json') - ); - const ngxsSchematicRunner = new SchematicTestRunner( '@ngxs/store/schematics', join(workspaceRoot, 'packages/store/schematics/collection.json') ); - const defaultOptions: NgxsPackageSchema = { - skipInstall: false, - plugins: [] - }; - - const workspaceOptions: WorkspaceOptions = { - name: 'workspace', - newProjectRoot: 'projects', - version: '1.0.0' - }; - - const appOptions: ApplicationOptions = { - name: 'foo', - inlineStyle: false, - inlineTemplate: false, - routing: true, - skipTests: false, - skipPackageJson: false - }; - - let appTree: UnitTestTree; - beforeEach(async () => { - appTree = await angularSchematicRunner.runSchematic('workspace', workspaceOptions); - appTree = await angularSchematicRunner.runSchematic('application', appOptions, appTree); - }); + describe('run ngxs-init alias', () => { + const defaultOptions: NgxsPackageSchema = { + skipInstall: false, + plugins: [] + }; - describe('importing the Ngxs module', () => { - test.each` - project - ${undefined} - ${'foo'} - `('should import the module when project is $project ', async ({ project }) => { - // Arrange - const options: NgxsPackageSchema = { ...defaultOptions, project }; - // Act - const tree = await ngxsSchematicRunner.runSchematic('ngxs-init', options, appTree); + const testSetup = async (options?: { + isStandalone?: boolean; + ngxsPackageSchema?: NgxsPackageSchema; + runSchematic?: boolean; + }) => { + const runSchematic = options?.runSchematic || true; + const appTree = await createWorkspace(options?.isStandalone); + let tree: UnitTestTree | undefined = undefined; + if (runSchematic) { + const schemaOptions: NgxsPackageSchema = options?.ngxsPackageSchema || defaultOptions; + tree = await ngxsSchematicRunner.runSchematic('ngxs-init', schemaOptions, appTree); + } - // Assert - const content = tree.readContent('/projects/foo/src/app/app.module.ts'); - expect(content).toMatch(/import { NgxsModule } from '@ngxs\/store'/); - expect(content).toMatch(/imports: \[[^\]]*NgxsModule.forRoot\(\[\],[^\]]*\]/m); - expect(content).toContain( - 'NgxsModule.forRoot([], { developmentMode: /** !environment.production */ false, selectorOptions: { suppressErrors: false, injectContainerState: false } })' - ); + return { appTree, tree }; + }; + describe('importing the Ngxs module in a non standalone app', () => { + test.each` + project + ${undefined} + ${'foo'} + `('should import the module when project is $project ', async ({ project }) => { + // Arrange & Act + const { tree } = await testSetup({ + isStandalone: false, + ngxsPackageSchema: { ...defaultOptions, project } + }); + + // Assert + const content = tree!.readContent('/projects/foo/src/app/app.module.ts'); + expect(content).toMatch(/import { NgxsModule } from '@ngxs\/store'/); + expect(content).toMatch(/imports: \[[^\]]*NgxsModule.forRoot\(\[\],[^\]]*\]/m); + expect(content).toContain( + 'NgxsModule.forRoot([], { developmentMode: /** !environment.production */ false, selectorOptions: { suppressErrors: false, injectContainerState: false } })' + ); + }); + it('should throw if invalid project is specified', async () => { + // Arrange + const { appTree } = await testSetup({ + runSchematic: false + }); + + const schemaOptions: NgxsPackageSchema = { ...defaultOptions, project: 'hello' }; + + // Assert + await expect( + ngxsSchematicRunner.runSchematic('ng-add', schemaOptions, appTree) + ).rejects.toThrow(`Project "${schemaOptions.project}" does not exist.`); + }); }); - it('should throw if invalid project is specified', async () => { - // Arrange - const options: NgxsPackageSchema = { ...defaultOptions, project: 'hello' }; - await expect( - ngxsSchematicRunner.runSchematic('ng-add', options, appTree) - ).rejects.toThrow(`Project "${options.project}" does not exist.`); + + describe('should have the provideStore provider in a standalone app', () => { + test.each` + project + ${undefined} + ${'foo'} + `('should import the module when project is $project ', async ({ project }) => { + // Arrange + const { tree } = await testSetup({ + isStandalone: true, + ngxsPackageSchema: { ...defaultOptions, project } + }); + + // Act + const content = tree!.readContent('/projects/foo/src/main.ts'); + + // Assert + expect(content).toMatch(/provideStore\(/); + expect(tree!.files).not.toContain('/projects/foo/src/app/app.module.ts'); + }); }); }); describe('ng-add package in package.json', () => { + const testSetup = async (options?: { + isStandalone?: boolean; + ngxsPackageSchema?: NgxsPackageSchema; + runSchematic?: boolean; + }) => { + const runSchematic = options?.runSchematic || true; + const appTree = await createWorkspace(options?.isStandalone); + let tree: UnitTestTree | undefined = undefined; + + if (runSchematic) { + const schemaOptions: NgxsPackageSchema = options?.ngxsPackageSchema || {}; + tree = await ngxsSchematicRunner.runSchematic('ng-add', schemaOptions, appTree); + } + + return { appTree, tree }; + }; + it('should add ngxs store with provided plugins in package.json', async () => { + // Arrange const plugins = [LIBRARIES.DEVTOOLS, LIBRARIES.LOGGER]; - const options: NgxsPackageSchema = { plugins }; - appTree = await ngxsSchematicRunner.runSchematic('ng-add', options, appTree); + let { tree } = await testSetup({ + ngxsPackageSchema: { plugins } + }); - const packageJsonText = appTree.readContent('/package.json'); + // Act + const packageJsonText = tree!.readContent('/package.json'); const packageJson = JSON.parse(packageJsonText); - expect(plugins.every(p => !!packageJson.dependencies[p])).toBeTruthy(); + + // Assert + expect(plugins?.every(p => !!packageJson.dependencies[p])).toBeTruthy(); }); it('should add ngxs store with all plugins in package.json', async () => { - const packages = Object.values(LIBRARIES).filter(v => v !== LIBRARIES.STORE); - const options: NgxsPackageSchema = { plugins: packages }; - appTree = await ngxsSchematicRunner.runSchematic('ng-add', options, appTree); + // Arrange + const plugins = Object.values(LIBRARIES).filter(v => v !== LIBRARIES.STORE); + let { tree } = await testSetup({ + ngxsPackageSchema: { plugins } + }); - const packageJsonText = appTree.readContent('/package.json'); + // Act + const packageJsonText = tree!.readContent('/package.json'); const packageJson = JSON.parse(packageJsonText); - expect(packages.every(p => !!packageJson.dependencies[p])).toBeTruthy(); + + // Assert + expect(plugins.every(p => !!packageJson.dependencies[p])).toBeTruthy(); }); it('should not attempt to add non-existent package', async () => { - const packages = ['who-am-i']; - const options: NgxsPackageSchema = { plugins: packages }; + // Arrange & Act + const { appTree } = await testSetup({ + runSchematic: false + }); + const plugins = ['who-am-i']; + const options: NgxsPackageSchema = { plugins }; + + // Assert await expect( ngxsSchematicRunner.runSchematic('ng-add', options, appTree) ).rejects.toThrow(); diff --git a/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.spec.ts__template__ b/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.spec.ts__template__ index aca577a5b..4742ecfcb 100644 --- a/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.spec.ts__template__ +++ b/packages/store/schematics/src/starter-kit/files/store/auth/auth.state.spec.ts__template__ @@ -1,4 +1,4 @@ -import { NgxsModule, Store } from '@ngxs/store'; +import { <% if (isStandalone) { %> provideStore, <% } else { %> NgxsModule, <% } %> Store } from '@ngxs/store'; import { TestBed } from '@angular/core/testing'; import { AuthenticationStateModel, AuthState } from './auth.state'; import { SetAuthData } from './auth.actions'; @@ -8,7 +8,8 @@ describe('[TEST]: AuthStore', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([AuthState])], + <% if (isStandalone) { %> providers: [provideStore([AuthState])] + <% } else { %> imports: [NgxsModule.forRoot([AuthState])] <% } %> }); store = TestBed.inject(Store); diff --git a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.spec.ts__template__ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.spec.ts__template__ index 438c999a5..b743ecd1f 100644 --- a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.spec.ts__template__ +++ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/dictionary/dictionary.state.spec.ts__template__ @@ -1,4 +1,4 @@ -import { NgxsModule, Store } from '@ngxs/store'; +import { <% if (isStandalone) { %> provideStore, <% } else { %> NgxsModule, <% } %> Store } from '@ngxs/store'; import { TestBed } from '@angular/core/testing'; import { DictionaryState, DictionaryStateModel } from './dictionary.state'; import { DictionaryReset, SetDictionaryData } from './dictionary.actions'; @@ -29,7 +29,8 @@ describe('[TEST]: Dictionary state', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([DictionaryState])], + <% if (isStandalone) { %> providers: [provideStore([DictionaryState])] + <% } else { %> imports: [NgxsModule.forRoot([DictionaryState])] <% } %> }); store = TestBed.inject(Store); diff --git a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.spec.ts__template__ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.spec.ts__template__ index ee3b5048e..7479acacd 100644 --- a/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.spec.ts__template__ +++ b/packages/store/schematics/src/starter-kit/files/store/dashboard/states/user/user.state.spec.ts__template__ @@ -1,4 +1,4 @@ -import { NgxsModule, Store } from '@ngxs/store'; +import { <% if (isStandalone) { %> provideStore, <% } else { %> NgxsModule, <% } %> Store } from '@ngxs/store'; import { TestBed } from '@angular/core/testing'; import { UserStateModel, UserState } from './user.state'; import { SetUser } from './user.actions'; @@ -8,7 +8,8 @@ describe('[TEST]: User state', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([UserState])], + <% if (isStandalone) { %> providers: [provideStore([UserState])] + <% } else { %> imports: [NgxsModule.forRoot([UserState])] <% } %> }); store = TestBed.inject(Store); diff --git a/packages/store/schematics/src/starter-kit/starter-kit.factory.spec.ts b/packages/store/schematics/src/starter-kit/starter-kit.factory.spec.ts index 7b609a50c..bc9dd6efc 100644 --- a/packages/store/schematics/src/starter-kit/starter-kit.factory.spec.ts +++ b/packages/store/schematics/src/starter-kit/starter-kit.factory.spec.ts @@ -1,6 +1,7 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; -import * as path from 'path'; import { workspaceRoot } from '@nrwl/devkit'; +import * as path from 'path'; +import { createWorkspace } from '../_testing'; import { StarterKitSchema } from './starter-kit.schema'; describe('Generate ngxs starter kit', () => { @@ -8,50 +9,120 @@ describe('Generate ngxs starter kit', () => { '.', path.join(workspaceRoot, 'packages/store/schematics/collection.json') ); + + const defaultOptions: StarterKitSchema = { + path: './src' + }; + + const testSetup = async (options?: { + isStandalone?: boolean; + starterKitSchema?: StarterKitSchema; + }) => { + const appTree = await createWorkspace(options?.isStandalone); + const starterKitSchemaOptions: StarterKitSchema = + options?.starterKitSchema || defaultOptions; + const tree: UnitTestTree = await runner.runSchematic( + 'starter-kit', + starterKitSchemaOptions, + appTree + ); + return { appTree, tree }; + }; + it('should generate store in default root folder', async () => { - const options: StarterKitSchema = { - spec: true, - path: './src' - }; - const tree: UnitTestTree = await runner - .runSchematicAsync('starter-kit', options) - .toPromise(); + // Arrange + const { tree } = await testSetup({ + starterKitSchema: { + ...defaultOptions, + spec: true + } + }); + + // Act const files: string[] = tree.files; - expect(files).toEqual([ - '/src/store/store.config.ts', - '/src/store/store.module.ts', - '/src/store/auth/auth.actions.ts', - '/src/store/auth/auth.state.spec.ts', - '/src/store/auth/auth.state.ts', - '/src/store/dashboard/index.ts', - '/src/store/dashboard/states/dictionary/dictionary.actions.ts', - '/src/store/dashboard/states/dictionary/dictionary.state.spec.ts', - '/src/store/dashboard/states/dictionary/dictionary.state.ts', - '/src/store/dashboard/states/user/user.actions.ts', - '/src/store/dashboard/states/user/user.state.spec.ts', - '/src/store/dashboard/states/user/user.state.ts' - ]); + + // Assert + expect(files).toEqual( + expect.arrayContaining([ + '/src/store/store.config.ts', + '/src/store/store.module.ts', + '/src/store/auth/auth.actions.ts', + '/src/store/auth/auth.state.spec.ts', + '/src/store/auth/auth.state.ts', + '/src/store/dashboard/index.ts', + '/src/store/dashboard/states/dictionary/dictionary.actions.ts', + '/src/store/dashboard/states/dictionary/dictionary.state.spec.ts', + '/src/store/dashboard/states/dictionary/dictionary.state.ts', + '/src/store/dashboard/states/user/user.actions.ts', + '/src/store/dashboard/states/user/user.state.spec.ts', + '/src/store/dashboard/states/user/user.state.ts' + ]) + ); }); it('should generate store in default root folder with spec false', async () => { - const options: StarterKitSchema = { - spec: false, - path: './src' - }; - const tree: UnitTestTree = await runner - .runSchematicAsync('starter-kit', options) - .toPromise(); + // Arrange + const { tree } = await testSetup({ + starterKitSchema: { + ...defaultOptions, + spec: false + } + }); + + // Act const files: string[] = tree.files; - expect(files).toEqual([ - '/src/store/store.config.ts', - '/src/store/store.module.ts', - '/src/store/auth/auth.actions.ts', - '/src/store/auth/auth.state.ts', - '/src/store/dashboard/index.ts', - '/src/store/dashboard/states/dictionary/dictionary.actions.ts', - '/src/store/dashboard/states/dictionary/dictionary.state.ts', - '/src/store/dashboard/states/user/user.actions.ts', - '/src/store/dashboard/states/user/user.state.ts' - ]); + + // Assert + expect(files).toEqual( + expect.arrayContaining([ + '/src/store/store.config.ts', + '/src/store/store.module.ts', + '/src/store/auth/auth.actions.ts', + '/src/store/auth/auth.state.ts', + '/src/store/dashboard/index.ts', + '/src/store/dashboard/states/dictionary/dictionary.actions.ts', + '/src/store/dashboard/states/dictionary/dictionary.state.ts', + '/src/store/dashboard/states/user/user.actions.ts', + '/src/store/dashboard/states/user/user.state.ts' + ]) + ); + }); + + it('should provideStore if the application is standalone', async () => { + // Arrange + const { tree } = await testSetup({ + isStandalone: true, + starterKitSchema: { + ...defaultOptions, + spec: true + } + }); + + // Act + const content = tree.readContent( + '/src/store/dashboard/states/dictionary/dictionary.state.spec.ts' + ); + + // Assert + expect(content).toMatch(/provideStore\(\[DictionaryState\]\)/); + }); + + it('should import the module if the application is non standalone', async () => { + // Arrange + const { tree } = await testSetup({ + isStandalone: false, + starterKitSchema: { + ...defaultOptions, + spec: true + } + }); + + // Act + const content = tree.readContent( + '/src/store/dashboard/states/dictionary/dictionary.state.spec.ts' + ); + + // Assert + expect(content).toMatch(/NgxsModule.forRoot\(\[DictionaryState\]\)/); }); }); diff --git a/packages/store/schematics/src/starter-kit/starter-kit.factory.ts b/packages/store/schematics/src/starter-kit/starter-kit.factory.ts index 7734d6a2b..35d9e32fb 100644 --- a/packages/store/schematics/src/starter-kit/starter-kit.factory.ts +++ b/packages/store/schematics/src/starter-kit/starter-kit.factory.ts @@ -1,10 +1,22 @@ -import { Rule, url } from '@angular-devkit/schematics'; -import { StarterKitSchema } from './starter-kit.schema'; -import { normalizePath } from '../utils/normalize-options'; +import { Rule, Tree, url } from '@angular-devkit/schematics'; import { generateFiles } from '../utils/generate-utils'; +import { isStandaloneApp } from '../utils/ng-utils/ng-ast-utils'; +import { getProjectMainFile } from '../utils/ng-utils/project'; +import { normalizePath } from '../utils/normalize-options'; +import { StarterKitSchema } from './starter-kit.schema'; export function starterKit(options: StarterKitSchema): Rule { - const normalizedPath = normalizePath(options.path); + return (host: Tree) => { + const mainFile = getProjectMainFile(host, options.project); + const isStandalone = isStandaloneApp(host, mainFile); + + const normalizedPath = normalizePath(options.path); - return generateFiles(url('./files'), normalizedPath, options, options.spec); + return generateFiles( + url('./files'), + normalizedPath, + { ...options, isStandalone }, + options.spec + ); + }; } diff --git a/packages/store/schematics/src/starter-kit/starter-kit.schema.d.ts b/packages/store/schematics/src/starter-kit/starter-kit.schema.d.ts index 39c99ac68..75731ec47 100644 --- a/packages/store/schematics/src/starter-kit/starter-kit.schema.d.ts +++ b/packages/store/schematics/src/starter-kit/starter-kit.schema.d.ts @@ -7,4 +7,8 @@ export interface StarterKitSchema { * The spec flag */ spec?: boolean; + /** + * The application project name to add the Ngxs module/provider. + */ + project?: string; } diff --git a/packages/store/schematics/src/state/files/__name__.state.spec.ts__template__ b/packages/store/schematics/src/state/files/__name__.state.spec.ts__template__ index e3f4180ec..e1261ba62 100644 --- a/packages/store/schematics/src/state/files/__name__.state.spec.ts__template__ +++ b/packages/store/schematics/src/state/files/__name__.state.spec.ts__template__ @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing'; -import { NgxsModule, Store } from '@ngxs/store'; +import { <% if (isStandalone) { %> provideStore, <% } else { %> NgxsModule, <% } %> Store } from '@ngxs/store'; import { <%= classify(name) %>State, <%= classify(name) %>StateModel } from './<%= dasherize(name) %>.state'; describe('<%= classify(name) %> state', () => { @@ -7,7 +7,8 @@ describe('<%= classify(name) %> state', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([<%= classify(name) %>State])] + <% if (isStandalone) { %> providers: [provideStore([<%= classify(name) %>State])] + <% } else { %> imports: [NgxsModule.forRoot([<%= classify(name) %>State])] <% } %> }); store = TestBed.inject(Store); diff --git a/packages/store/schematics/src/state/schema.json b/packages/store/schematics/src/state/schema.json index d53d99e73..0ea6ccac2 100644 --- a/packages/store/schematics/src/state/schema.json +++ b/packages/store/schematics/src/state/schema.json @@ -27,6 +27,10 @@ "type": "boolean", "default": false, "description": "Flag to indicate if a dir is created." + }, + "project": { + "type": "string", + "description": "The name of the project." } }, "required": ["name"] diff --git a/packages/store/schematics/src/state/state.factory.spec.ts b/packages/store/schematics/src/state/state.factory.spec.ts index fa1f749d9..79bbcf50d 100644 --- a/packages/store/schematics/src/state/state.factory.spec.ts +++ b/packages/store/schematics/src/state/state.factory.spec.ts @@ -2,6 +2,7 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/te import { workspaceRoot } from '@nrwl/devkit'; import * as path from 'path'; +import { createWorkspace } from '../_testing'; import { StateSchema } from './state.schema'; describe('Generate ngxs state', () => { @@ -12,42 +13,113 @@ describe('Generate ngxs state', () => { const defaultOptions: StateSchema = { name: 'todos' }; + + const testSetup = async (options?: { + isStandalone?: boolean; + stateSchema?: StateSchema; + }) => { + const appTree = await createWorkspace(options?.isStandalone); + + const stateSchemaOptions: StateSchema = options?.stateSchema || defaultOptions; + const tree: UnitTestTree = await runner.runSchematic('state', stateSchemaOptions, appTree); + + return { appTree, tree }; + }; + it('should manage name only', async () => { - const options: StateSchema = { - ...defaultOptions - }; - const tree: UnitTestTree = await runner.runSchematicAsync('state', options).toPromise(); + // Arrange + const { tree } = await testSetup(); + + // Act const files: string[] = tree.files; - expect(files).toEqual(['/todos/todos.state.spec.ts', '/todos/todos.state.ts']); + + // Assert + expect(files).toEqual( + expect.arrayContaining(['/todos/todos.state.spec.ts', '/todos/todos.state.ts']) + ); }); it('should not create a separate folder if "flat" is set to "true"', async () => { - const options: StateSchema = { - ...defaultOptions, - flat: true - }; - const tree: UnitTestTree = await runner.runSchematicAsync('state', options).toPromise(); + // Arrange + const { tree } = await testSetup({ + stateSchema: { + ...defaultOptions, + flat: true + } + }); + + // Act const files: string[] = tree.files; - expect(files).toEqual(['/todos.state.spec.ts', '/todos.state.ts']); + + // Assert + expect(files).toEqual(expect.arrayContaining(['/todos.state.spec.ts', '/todos.state.ts'])); }); it('should manage name with spec true', async () => { - const options: StateSchema = { - ...defaultOptions, - spec: true - }; - const tree: UnitTestTree = await runner.runSchematicAsync('state', options).toPromise(); + // Arrange + const { tree } = await testSetup({ + stateSchema: { + ...defaultOptions, + spec: true + } + }); + + // Act const files: string[] = tree.files; - expect(files).toEqual(['/todos/todos.state.spec.ts', '/todos/todos.state.ts']); + + // Assert + expect(files).toEqual( + expect.arrayContaining(['/todos/todos.state.spec.ts', '/todos/todos.state.ts']) + ); }); it('should manage name with spec false', async () => { - const options: StateSchema = { - ...defaultOptions, - spec: false - }; - const tree: UnitTestTree = await runner.runSchematicAsync('state', options).toPromise(); + // Arrange + const { tree } = await testSetup({ + stateSchema: { + ...defaultOptions, + spec: false + } + }); + + // Act const files: string[] = tree.files; - expect(files).toEqual(['/todos/todos.state.ts']); + + // Assert + expect(files).toEqual(expect.arrayContaining(['/todos/todos.state.ts'])); + }); + + it('should provideStore if the application is standalone', async () => { + // Arrange + const { tree } = await testSetup({ + isStandalone: true, + stateSchema: { + ...defaultOptions, + spec: true + } + }); + + // Act + const content = tree.readContent('/todos/todos.state.spec.ts'); + + // Assert + expect(content).toMatch(/provideStore\(\[TodosState\]\)/); + }); + + it('should import the module if the application is non standalone', async () => { + // Arrange + const { tree } = await testSetup({ + isStandalone: false, + stateSchema: { + ...defaultOptions, + spec: true + } + }); + + // Act + const content = tree.readContent('/todos/todos.state.spec.ts'); + + // Assert + expect(content).toMatch(/NgxsModule.forRoot\(\[TodosState\]\)/); }); }); diff --git a/packages/store/schematics/src/state/state.factory.ts b/packages/store/schematics/src/state/state.factory.ts index aed49da97..3559ea2cb 100644 --- a/packages/store/schematics/src/state/state.factory.ts +++ b/packages/store/schematics/src/state/state.factory.ts @@ -1,19 +1,26 @@ -import { Rule, SchematicsException, url } from '@angular-devkit/schematics'; -import { StateSchema } from './state.schema'; +import { Rule, SchematicsException, Tree, url } from '@angular-devkit/schematics'; +import { join } from 'path'; import { isEmpty } from '../utils/common/properties'; -import { normalizeBaseOptions } from '../utils/normalize-options'; import { generateFiles } from '../utils/generate-utils'; -import { join } from 'path'; +import { isStandaloneApp } from '../utils/ng-utils/ng-ast-utils'; +import { getProjectMainFile } from '../utils/ng-utils/project'; +import { normalizeBaseOptions } from '../utils/normalize-options'; +import { StateSchema } from './state.schema'; export function state(options: StateSchema): Rule { - if (isEmpty(options.name)) { - throw new SchematicsException('Invalid options, "name" is required.'); - } + return (host: Tree) => { + if (isEmpty(options.name)) { + throw new SchematicsException('Invalid options, "name" is required.'); + } + + const mainFile = getProjectMainFile(host, options.project); + const isStandalone = isStandaloneApp(host, mainFile); - const normalizedOptions = normalizeBaseOptions(options); - const path = options.flat - ? normalizedOptions.path - : join(normalizedOptions.path, normalizedOptions.name); + const normalizedOptions = normalizeBaseOptions(options); + const path = options.flat + ? normalizedOptions.path + : join(normalizedOptions.path, normalizedOptions.name); - return generateFiles(url('./files'), path, options, options.spec); + return generateFiles(url('./files'), path, { ...options, isStandalone }, options.spec); + }; } diff --git a/packages/store/schematics/src/state/state.schema.d.ts b/packages/store/schematics/src/state/state.schema.d.ts index 441a1319c..afb185071 100644 --- a/packages/store/schematics/src/state/state.schema.d.ts +++ b/packages/store/schematics/src/state/state.schema.d.ts @@ -15,4 +15,8 @@ export interface StateSchema { * Flag to indicate if a dir is created. */ flat?: boolean; + /** + * The application project name to add the Ngxs module/provider. + */ + project?: string; } diff --git a/packages/store/schematics/src/store/files/__name__.state.spec.ts__template__ b/packages/store/schematics/src/store/files/__name__.state.spec.ts__template__ index 4e9ae01ad..41e9a565b 100644 --- a/packages/store/schematics/src/store/files/__name__.state.spec.ts__template__ +++ b/packages/store/schematics/src/store/files/__name__.state.spec.ts__template__ @@ -1,5 +1,5 @@ import { TestBed } from '@angular/core/testing'; -import { NgxsModule, Store } from '@ngxs/store'; +import { <% if (isStandalone) { %> provideStore, <% } else { %> NgxsModule, <% } %> Store } from '@ngxs/store'; import { <%= classify(name) %>State, <%= classify(name) %>StateModel } from './<%= dasherize(name) %>.state'; import { <%= classify(name) %>Action } from './<%= dasherize(name) %>.actions'; @@ -7,7 +7,8 @@ describe('<%= classify(name) %> store', () => { let store: Store; beforeEach(() => { TestBed.configureTestingModule({ - imports: [NgxsModule.forRoot([<%= classify(name) %>State])] + <% if (isStandalone) { %> providers: [provideStore([<%= classify(name) %>State])] + <% } else { %> imports: [NgxsModule.forRoot([<%= classify(name) %>State])] <% } %> }); store = TestBed.inject(Store); diff --git a/packages/store/schematics/src/store/schema.json b/packages/store/schematics/src/store/schema.json index b136a1437..c8b9bade9 100644 --- a/packages/store/schematics/src/store/schema.json +++ b/packages/store/schematics/src/store/schema.json @@ -27,6 +27,10 @@ "type": "boolean", "default": false, "description": "Flag to indicate if a dir is created." + }, + "project": { + "type": "string", + "description": "The name of the project." } }, "required": ["name"] diff --git a/packages/store/schematics/src/store/store.factory.spec.ts b/packages/store/schematics/src/store/store.factory.spec.ts index d807ddaac..18fd4d539 100644 --- a/packages/store/schematics/src/store/store.factory.spec.ts +++ b/packages/store/schematics/src/store/store.factory.spec.ts @@ -1,7 +1,7 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; import { workspaceRoot } from '@nrwl/devkit'; - import * as path from 'path'; +import { createWorkspace } from '../_testing'; import { StoreSchema } from './store.schema'; describe('NGXS Store', () => { @@ -12,49 +12,91 @@ describe('NGXS Store', () => { const defaultOptions: StoreSchema = { name: 'todos' }; + + const testSetup = async (options?: { + isStandalone?: boolean; + storeSchema?: StoreSchema; + }) => { + const appTree = await createWorkspace(options?.isStandalone); + + const storeSchemaOptions: StoreSchema = options?.storeSchema || defaultOptions; + const tree: UnitTestTree = await runner.runSchematic('store', storeSchemaOptions, appTree); + + return { appTree, tree }; + }; + it('should manage name only', async () => { - const options: StoreSchema = { - ...defaultOptions - }; - const tree: UnitTestTree = await runner.runSchematicAsync('store', options).toPromise(); + // Arrange + const { tree } = await testSetup(); + + // Act const files: string[] = tree.files; - expect(files).toEqual([ - '/todos/todos.actions.ts', - '/todos/todos.state.spec.ts', - '/todos/todos.state.ts' - ]); + + // Assert + expect(files).toEqual( + expect.arrayContaining([ + '/todos/todos.actions.ts', + '/todos/todos.state.spec.ts', + '/todos/todos.state.ts' + ]) + ); }); + it('should not create a separate folder if "flat" is set to "true"', async () => { - const options: StoreSchema = { - ...defaultOptions, - flat: true - }; - const tree: UnitTestTree = await runner.runSchematicAsync('store', options).toPromise(); + // Arrange + const { tree } = await testSetup({ + storeSchema: { + ...defaultOptions, + flat: true + } + }); + + // Act const files: string[] = tree.files; - expect(files).toEqual(['/todos.actions.ts', '/todos.state.spec.ts', '/todos.state.ts']); + + // Assert + expect(files).toEqual( + expect.arrayContaining(['/todos.actions.ts', '/todos.state.spec.ts', '/todos.state.ts']) + ); }); it('should manage name with spec false', async () => { - const options: StoreSchema = { - ...defaultOptions, - spec: false - }; - const tree: UnitTestTree = await runner.runSchematicAsync('store', options).toPromise(); + // Arrange + const { tree } = await testSetup({ + storeSchema: { + ...defaultOptions, + spec: false + } + }); + + // Act const files: string[] = tree.files; - expect(files).toEqual(['/todos/todos.actions.ts', '/todos/todos.state.ts']); + + // Assert + expect(files).toEqual( + expect.arrayContaining(['/todos/todos.actions.ts', '/todos/todos.state.ts']) + ); }); it('should manage name with spec true', async () => { - const options: StoreSchema = { - ...defaultOptions, - spec: true - }; - const tree: UnitTestTree = await runner.runSchematicAsync('store', options).toPromise(); + // Arrange + const { tree } = await testSetup({ + storeSchema: { + ...defaultOptions, + spec: true + } + }); + + // Act const files: string[] = tree.files; - expect(files).toEqual([ - '/todos/todos.actions.ts', - '/todos/todos.state.spec.ts', - '/todos/todos.state.ts' - ]); + + // Assert + expect(files).toEqual( + expect.arrayContaining([ + '/todos/todos.actions.ts', + '/todos/todos.state.spec.ts', + '/todos/todos.state.ts' + ]) + ); }); }); diff --git a/packages/store/schematics/src/store/store.factory.ts b/packages/store/schematics/src/store/store.factory.ts index 165e48bb0..486043446 100644 --- a/packages/store/schematics/src/store/store.factory.ts +++ b/packages/store/schematics/src/store/store.factory.ts @@ -1,19 +1,26 @@ -import { Rule, SchematicsException, url } from '@angular-devkit/schematics'; -import { StoreSchema } from './store.schema'; +import { Rule, SchematicsException, Tree, url } from '@angular-devkit/schematics'; +import { join } from 'path'; import { isEmpty } from '../utils/common/properties'; -import { normalizeBaseOptions } from '../utils/normalize-options'; import { generateFiles } from '../utils/generate-utils'; -import { join } from 'path'; +import { isStandaloneApp } from '../utils/ng-utils/ng-ast-utils'; +import { getProjectMainFile } from '../utils/ng-utils/project'; +import { normalizeBaseOptions } from '../utils/normalize-options'; +import { StoreSchema } from './store.schema'; export function store(options: StoreSchema): Rule { - if (isEmpty(options.name)) { - throw new SchematicsException('Invalid options, "name" is required.'); - } + return (host: Tree) => { + if (isEmpty(options.name)) { + throw new SchematicsException('Invalid options, "name" is required.'); + } + + const mainFile = getProjectMainFile(host, options.project); + const isStandalone = isStandaloneApp(host, mainFile); - const normalizedOptions = normalizeBaseOptions(options); - const path = options.flat - ? normalizedOptions.path - : join(normalizedOptions.path, normalizedOptions.name); + const normalizedOptions = normalizeBaseOptions(options); + const path = options.flat + ? normalizedOptions.path + : join(normalizedOptions.path, normalizedOptions.name); - return generateFiles(url('./files'), path, options, options.spec); + return generateFiles(url('./files'), path, { ...options, isStandalone }, options.spec); + }; } diff --git a/packages/store/schematics/src/store/store.schema.d.ts b/packages/store/schematics/src/store/store.schema.d.ts index 7b7135c24..8ca3e2c76 100644 --- a/packages/store/schematics/src/store/store.schema.d.ts +++ b/packages/store/schematics/src/store/store.schema.d.ts @@ -15,4 +15,8 @@ export interface StoreSchema { * Flag to indicate if a dir is created. */ flat?: boolean; + /** + * The application project name to add the Ngxs module/provider. + */ + project?: string; } diff --git a/packages/store/tsconfig.schematics.json b/packages/store/tsconfig.schematics.json index 9ed17688e..ad48fb4cc 100644 --- a/packages/store/tsconfig.schematics.json +++ b/packages/store/tsconfig.schematics.json @@ -24,6 +24,7 @@ "exclude": [ "schematics/templates/**/*", "schematics/**/*.test.ts", - "schematics/**/*.spec.ts" + "schematics/**/*.spec.ts", + "schematics/src/_testing/**/*" ] } From 59b0c4a95dfd20c154765765473860baca7f1543 Mon Sep 17 00:00:00 2001 From: Mark Whitfeld Date: Fri, 15 Dec 2023 17:24:00 +0200 Subject: [PATCH 3/3] chore: update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc5c248b1..2d03b8f42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ $ npm install @ngxs/store@dev ### To become next patch version +- Feat: schematics support a project option and standalone detection [#2089](https://github.com/ngxs/store/pull/2089) - Fix: Log feature states added before store is initialized [#2067](https://github.com/ngxs/store/pull/2067) - Fix: Show error when state initialization order is invalid [#2066](https://github.com/ngxs/store/pull/2066), [#2067](https://github.com/ngxs/store/pull/2067) - Fix: Router Plugin - Expose `NGXS_ROUTER_PLUGIN_OPTIONS` privately [#2037](https://github.com/ngxs/store/pull/2037)