Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: state, store and starter-kit schematics support a project option #2089

Merged
merged 4 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions docs/concepts/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:

Expand Down
15 changes: 9 additions & 6 deletions docs/concepts/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:

Expand Down
11 changes: 7 additions & 4 deletions docs/introduction/starter-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**:

Expand Down
4 changes: 2 additions & 2 deletions packages/store/internals/testing/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
1 change: 1 addition & 0 deletions packages/store/schematics/src/_testing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './schematics';
77 changes: 77 additions & 0 deletions packages/store/schematics/src/_testing/schematics.ts
Original file line number Diff line number Diff line change
@@ -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`);
}
184 changes: 117 additions & 67 deletions packages/store/schematics/src/ng-add/ng-add.factory.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
Loading