Skip to content

Commit

Permalink
Merge branch 'master' into fix/features-last
Browse files Browse the repository at this point in the history
  • Loading branch information
markwhitfeld authored Dec 15, 2023
2 parents 3b57ef5 + 59b0c4a commit 19288fb
Show file tree
Hide file tree
Showing 31 changed files with 669 additions and 253 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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
Loading

0 comments on commit 19288fb

Please sign in to comment.