fix(schematics): ngrx schematics should generate enhanced ngrx files
@nrwl/schematics no longer uses the @ngrx/schematics to generate NgRx feature files. * `ngrx/files/__directory__` templates are used * Templates replicate the simple outputs generated from @ngrx/schematics:feature * Templates add significant Nx enhancements. The following standard files will be scaffolded: * `<feature>.actions.ts` * `<feature>.effects.ts` + `<feature>.effects.spec.ts` * `<feature>.reducer.ts` + `<feature>.reducer.spec.ts` The following new files will also be scaffolded: * `<feature>.selectors.ts` + `<feature>.selectors.spec.ts` Changes include: * Change the action/enums to generate a trio of enums for each *feature*: `Load<Feature>`, `<Feature>Loaded`, and `<Feature>LoadError` * Add code generators for `<feature>.selectors.ts` * Add code generators for unit and integration testing `*.spec.ts` files * Update the public barrel [`index.ts`] when adding ngrx to a library * Use `StoreModule.forFeature()` when adding ngrx feature (without using the `--root` option) * Use the Effect to respond tp `load<Feature>$` and dispatch `<Feature>Loaded` or `<Feature>LoadError` * Update the Action to export `<feature>Actions` map of all action classes * fix `ng-add.test.ts` tests for latest Angular CLI scaffolding * fix `application.spec.ts` expect fails Fixes #472, Fixes #618, Fixes #317, Fixes #561, Refs #380.
This commit is contained in:
parent
a55412caae
commit
04e99b06ae
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,4 +7,5 @@ test
|
||||
.DS_Store
|
||||
tmp
|
||||
*.log
|
||||
.ng_pkg_build
|
||||
.ng_pkg_build
|
||||
jest.debug.config.js
|
||||
|
||||
@ -97,10 +97,6 @@ describe('Nrwl Convert to Nx Workspace', () => {
|
||||
updatedPackageJson.dependencies['@ngrx/store-devtools']
|
||||
).toBeDefined();
|
||||
expect(updatedPackageJson.dependencies['rxjs-compat']).toBeDefined();
|
||||
|
||||
expect(
|
||||
updatedPackageJson.devDependencies['@ngrx/schematics']
|
||||
).toBeDefined();
|
||||
expect(updatedPackageJson.devDependencies['@angular/cli']).toBeDefined();
|
||||
|
||||
const nxJson = readJson('nx.json');
|
||||
@ -203,6 +199,11 @@ describe('Nrwl Convert to Nx Workspace', () => {
|
||||
);
|
||||
expect(updatedAngularCLIJson.projects['proj-e2e'].architect.e2e).toEqual({
|
||||
builder: '@angular-devkit/build-angular:protractor',
|
||||
configurations: {
|
||||
production: {
|
||||
devServerTarget: 'proj:serve:production'
|
||||
}
|
||||
},
|
||||
options: {
|
||||
protractorConfig: 'apps/proj-e2e/protractor.conf.js',
|
||||
devServerTarget: 'proj:serve'
|
||||
|
||||
@ -6,12 +6,25 @@ describe('ngrx', () => {
|
||||
() => {
|
||||
newProject();
|
||||
newApp('myapp');
|
||||
|
||||
// Generate root ngrx state management
|
||||
runCLI(
|
||||
'generate ngrx app --module=apps/myapp/src/app/app.module.ts --root --collection=@nrwl/schematics'
|
||||
'generate ngrx users --module=apps/myapp/src/app/app.module.ts --root --collection=@nrwl/schematics'
|
||||
);
|
||||
|
||||
// Generate feature library and ngrx state within that library
|
||||
runCLI('g @nrwl/schematics:lib feature-flights --prefix=fl');
|
||||
runCLI(
|
||||
'generate ngrx flights --module=libs/feature-flights/src/lib/feature-flights.module.ts --collection=@nrwl/schematics'
|
||||
);
|
||||
|
||||
expect(runCLI('build')).toContain('chunk {main} main.js,');
|
||||
expect(runCLI('test --no-watch')).toContain('Executed 5 of 5 SUCCESS');
|
||||
expect(runCLI('test myapp --no-watch')).toContain(
|
||||
'Executed 10 of 10 SUCCESS'
|
||||
);
|
||||
expect(runCLI('test feature-flights --no-watch')).toContain(
|
||||
'Executed 8 of 8 SUCCESS'
|
||||
);
|
||||
},
|
||||
1000000
|
||||
);
|
||||
|
||||
@ -65,6 +65,11 @@ export function copyMissingPackages(): void {
|
||||
`rm -rf tmp/${projectName}/node_modules/@angular-devkit/core/node_modules`
|
||||
);
|
||||
|
||||
execSync(`rm tmp/${projectName}/node_modules/.bin/semver`);
|
||||
execSync(
|
||||
`cp -a node_modules/.bin/semver tmp/${projectName}/node_modules/.bin/semver`
|
||||
);
|
||||
|
||||
const libIndex = `./tmp/${projectName}/node_modules/@schematics/angular/library/index.js`;
|
||||
const content = readFileSync(libIndex).toString();
|
||||
const updatedContent = content.replace(
|
||||
|
||||
30
package.json
30
package.json
@ -17,9 +17,10 @@
|
||||
"checkformat": "prettier \"{packages,e2e}/**/*.ts\" --list-different"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jasmine-marbles": "0.3.1",
|
||||
"@angular-devkit/build-angular": "^0.6.8",
|
||||
"@angular-devkit/core": "^0.6.1",
|
||||
"@angular-devkit/schematics": "^0.6.1",
|
||||
"@angular/cli": "6.0.1",
|
||||
"@angular-devkit/build-angular": "~0.6.1",
|
||||
"@angular/common": "6.0.1",
|
||||
"@angular/compiler": "6.0.1",
|
||||
"@angular/compiler-cli": "6.0.1",
|
||||
@ -28,16 +29,15 @@
|
||||
"@angular/platform-browser-dynamic": "6.0.1",
|
||||
"@angular/router": "6.0.1",
|
||||
"@angular/upgrade": "6.0.1",
|
||||
"@ngrx/effects": "5.2.0",
|
||||
"@ngrx/router-store": "5.2.0",
|
||||
"@ngrx/schematics": "5.2.0",
|
||||
"@ngrx/store": "5.2.0",
|
||||
"@ngrx/store-devtools": "5.2.0",
|
||||
"@ngrx/effects": "6.0.1",
|
||||
"@ngrx/router-store": "6.0.1",
|
||||
"@ngrx/schematics": "6.0.1",
|
||||
"@ngrx/store": "6.0.1",
|
||||
"@ngrx/store-devtools": "6.0.1",
|
||||
"@schematics/angular": "^0.6.1",
|
||||
"@types/jasmine": "~2.8.6",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/node": "~8.9.4",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"@types/prettier": "^1.10.0",
|
||||
"@types/yargs": "^11.0.0",
|
||||
"angular": "1.6.6",
|
||||
@ -47,7 +47,11 @@
|
||||
"fs-extra": "5.0.0",
|
||||
"graphviz": "^0.0.8",
|
||||
"husky": "^0.14.3",
|
||||
"jest": "20.0.4",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-marbles": "0.3.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"jest": "^23.4.0",
|
||||
"jest-jasmine2": "^23.4.1",
|
||||
"karma": "~1.7.1",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-jasmine": "~1.1.1",
|
||||
@ -58,8 +62,8 @@
|
||||
"precise-commits": "1.0.2",
|
||||
"prettier": "1.10.2",
|
||||
"release-it": "^7.4.0",
|
||||
"rxjs": "^6.0.0",
|
||||
"rxjs-compat": "^6.0.0",
|
||||
"rxjs": "^6.1.0",
|
||||
"rxjs-compat": "^6.1.0",
|
||||
"semver": "5.4.1",
|
||||
"strip-json-comments": "2.0.1",
|
||||
"tmp": "0.0.33",
|
||||
@ -68,7 +72,7 @@
|
||||
"viz.js": "^1.8.1",
|
||||
"yargs": "^11.0.0",
|
||||
"yargs-parser": "10.0.0",
|
||||
"zone.js": "^0.8.19"
|
||||
"zone.js": "^0.8.26"
|
||||
},
|
||||
"author": "Victor Savkin",
|
||||
"license": "MIT",
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"license": "MIT",
|
||||
"schematics": "./src/collection.json",
|
||||
"dependencies": {
|
||||
"@ngrx/schematics": "5.2.0",
|
||||
"@ngrx/schematics": "6.0.1",
|
||||
"@schematics/angular": "0.4.6",
|
||||
"app-root-path": "^2.0.1",
|
||||
"npm-run-all": "4.1.2",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nx-bazel",
|
||||
"version": "0.1",
|
||||
"extends": "@ngrx/schematics",
|
||||
"extends": "@schematics/angular",
|
||||
"schematics": {
|
||||
"application": {
|
||||
"factory": "./collection/application",
|
||||
|
||||
@ -23,6 +23,5 @@
|
||||
"node_modules/@angular/tsc-wrapped/**",
|
||||
"node_modules/@nrwl/bazel/**",
|
||||
"node_modules/@nrwl/schematics/**",
|
||||
"node_modules/@ngrx/schematics/**",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,10 +38,10 @@
|
||||
"rxjs": "<%= rxjsVersion %>",
|
||||
"zone.js": "^0.8.19",
|
||||
"@nrwl/nx": "<%= nxVersion %>",
|
||||
"@ngrx/effects": "5.2.0",
|
||||
"@ngrx/router-store": "5.2.0",
|
||||
"@ngrx/store": "5.2.0",
|
||||
"@ngrx/store-devtools": "5.2.0"
|
||||
"@ngrx/effects": "<%= ngrxVersion %>",
|
||||
"@ngrx/router-store": "<%= ngrxVersion %>",
|
||||
"@ngrx/store": "<%= ngrxVersion %>",
|
||||
"@ngrx/store-devtools": "<%= ngrxVersion %>"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "<%= angularCliVersion %>",
|
||||
|
||||
@ -385,10 +385,10 @@ const updatePackageJson = updateJsonInTree('package.json', json => {
|
||||
'zone.js': '^0.8.26',
|
||||
'core-js': '^2.5.4',
|
||||
// End Angular Versions
|
||||
'@ngrx/effects': '5.2.0',
|
||||
'@ngrx/router-store': '5.2.0',
|
||||
'@ngrx/store': '5.2.0',
|
||||
'@ngrx/store-devtools': '5.2.0',
|
||||
'@ngrx/effects': '6.0.1',
|
||||
'@ngrx/router-store': '6.0.1',
|
||||
'@ngrx/store': '6.0.1',
|
||||
'@ngrx/store-devtools': '6.0.1',
|
||||
'@nrwl/nx': '6.0.2'
|
||||
};
|
||||
json.devDependencies = {
|
||||
@ -397,7 +397,6 @@ const updatePackageJson = updateJsonInTree('package.json', json => {
|
||||
'@angular/compiler-cli': '6.0.1',
|
||||
'@angular/language-service': '6.0.1',
|
||||
// End Angular Versions
|
||||
'@ngrx/schematics': '5.2.0',
|
||||
typescript: '2.7.2',
|
||||
'jasmine-marbles': '0.3.1',
|
||||
'@types/jasmine': '~2.8.6',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nrwl/schematics",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "Nrwl Extensions for Angular: Schematics",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -20,7 +20,6 @@
|
||||
},
|
||||
"main": "index.js",
|
||||
"types": "index.d.js",
|
||||
"peerDependencies": {},
|
||||
"author": "Victor Savkin",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
@ -34,7 +33,6 @@
|
||||
"migrations": "./migrations/migrations.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ngrx/schematics": "5.2.0",
|
||||
"@types/yargs": "^11.0.0",
|
||||
"app-root-path": "^2.0.1",
|
||||
"cosmiconfig": "4.0.0",
|
||||
@ -50,7 +48,8 @@
|
||||
"yargs": "^11.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@schematics/angular": "^0.6.0",
|
||||
"@angular-devkit/schematics": "^0.6.0"
|
||||
"@schematics/angular": "^0.6.8",
|
||||
"@angular-devkit/core": "^0.6.8",
|
||||
"@angular-devkit/schematics": "^0.6.8"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "nx",
|
||||
"version": "0.1",
|
||||
"extends": "@ngrx/schematics",
|
||||
"extends": "@schematics/angular",
|
||||
"schematics": {
|
||||
"ng-add": {
|
||||
"factory": "./collection/ng-add",
|
||||
|
||||
@ -99,27 +99,33 @@ describe('app', () => {
|
||||
{ name: 'myApp' },
|
||||
appTree
|
||||
);
|
||||
expect(
|
||||
JSON.parse(noPrefix.read('angular.json').toString()).projects['my-app']
|
||||
.prefix
|
||||
).toEqual('proj');
|
||||
expect(
|
||||
noPrefix.read('apps/my-app-e2e/src/app.e2e-spec.ts').toString()
|
||||
).toContain('Welcome to proj!');
|
||||
|
||||
const withPrefix = schematicRunner.runSchematic(
|
||||
'app',
|
||||
{ name: 'myApp', prefix: 'custom' },
|
||||
appTree
|
||||
);
|
||||
expect(
|
||||
JSON.parse(withPrefix.read('angular.json').toString()).projects[
|
||||
'my-app'
|
||||
].prefix
|
||||
).toEqual('custom');
|
||||
expect(
|
||||
withPrefix.read('apps/my-app-e2e/src/app.e2e-spec.ts').toString()
|
||||
).toContain('Welcome to custom!');
|
||||
|
||||
// Testing without prefix
|
||||
|
||||
let appE2eSpec = noPrefix
|
||||
.read('apps/my-app-e2e/src/app.e2e-spec.ts')
|
||||
.toString();
|
||||
let angularJson = JSON.parse(noPrefix.read('angular.json').toString());
|
||||
let myAppPrefix = angularJson.projects['my-app'].prefix;
|
||||
|
||||
expect(myAppPrefix).toEqual('proj');
|
||||
expect(appE2eSpec).toContain('Welcome to my-app!');
|
||||
|
||||
// Testing WITH prefix
|
||||
|
||||
appE2eSpec = withPrefix
|
||||
.read('apps/my-app-e2e/src/app.e2e-spec.ts')
|
||||
.toString();
|
||||
angularJson = JSON.parse(withPrefix.read('angular.json').toString());
|
||||
myAppPrefix = angularJson.projects['my-app'].prefix;
|
||||
|
||||
expect(myAppPrefix).toEqual('custom');
|
||||
expect(appE2eSpec).toContain('Welcome to my-app!');
|
||||
});
|
||||
});
|
||||
|
||||
@ -161,48 +167,50 @@ describe('app', () => {
|
||||
});
|
||||
|
||||
it('should generate files', () => {
|
||||
const hasJsonValue = ({ path, expectedValue, lookupFn }) => {
|
||||
const content = getFileContent(tree, path);
|
||||
const config = JSON.parse(stripJsonComments(content));
|
||||
|
||||
expect(lookupFn(config)).toEqual(expectedValue);
|
||||
};
|
||||
const tree = schematicRunner.runSchematic(
|
||||
'app',
|
||||
{ name: 'myApp', directory: 'myDir' },
|
||||
appTree
|
||||
);
|
||||
expect(tree.exists(`apps/my-dir/my-app/karma.conf.js`)).toBeTruthy();
|
||||
expect(tree.exists('apps/my-dir/my-app/src/main.ts')).toBeTruthy();
|
||||
expect(
|
||||
tree.exists('apps/my-dir/my-app/src/app/app.module.ts')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
tree.exists('apps/my-dir/my-app/src/app/app.component.ts')
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
getFileContent(tree, 'apps/my-dir/my-app/src/app/app.module.ts')
|
||||
).toContain('class AppModule');
|
||||
|
||||
const tsconfigApp = JSON.parse(
|
||||
stripJsonComments(
|
||||
getFileContent(tree, 'apps/my-dir/my-app/tsconfig.app.json')
|
||||
)
|
||||
);
|
||||
expect(tsconfigApp.compilerOptions.outDir).toEqual(
|
||||
'../../../dist/out-tsc/apps/my-dir/my-app'
|
||||
);
|
||||
const appModulePath = 'apps/my-dir/my-app/src/app/app.module.ts';
|
||||
expect(getFileContent(tree, appModulePath)).toContain('class AppModule');
|
||||
|
||||
const tslintJson = JSON.parse(
|
||||
stripJsonComments(
|
||||
getFileContent(tree, 'apps/my-dir/my-app/tslint.json')
|
||||
)
|
||||
);
|
||||
expect(tslintJson.extends).toEqual('../../../tslint.json');
|
||||
// Make sure these exist
|
||||
[
|
||||
`apps/my-dir/my-app/karma.conf.js`,
|
||||
'apps/my-dir/my-app/src/main.ts',
|
||||
'apps/my-dir/my-app/src/app/app.module.ts',
|
||||
'apps/my-dir/my-app/src/app/app.component.ts',
|
||||
'apps/my-dir/my-app-e2e/src/app.po.ts'
|
||||
].forEach(path => {
|
||||
expect(tree.exists(path)).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(tree.exists('apps/my-dir/my-app-e2e/src/app.po.ts')).toBeTruthy();
|
||||
const tsconfigE2E = JSON.parse(
|
||||
stripJsonComments(
|
||||
getFileContent(tree, 'apps/my-dir/my-app-e2e/tsconfig.e2e.json')
|
||||
)
|
||||
);
|
||||
expect(tsconfigE2E.compilerOptions.outDir).toEqual(
|
||||
'../../../dist/out-tsc/apps/my-dir/my-app-e2e'
|
||||
);
|
||||
// Make sure these have properties
|
||||
[
|
||||
{
|
||||
path: 'apps/my-dir/my-app/tsconfig.app.json',
|
||||
lookupFn: json => json.compilerOptions.outDir,
|
||||
expectedValue: '../../../dist/out-tsc/apps/my-dir/my-app'
|
||||
},
|
||||
{
|
||||
path: 'apps/my-dir/my-app-e2e/tsconfig.e2e.json',
|
||||
lookupFn: json => json.compilerOptions.outDir,
|
||||
expectedValue: '../../../dist/out-tsc/apps/my-dir/my-app-e2e'
|
||||
},
|
||||
{
|
||||
path: 'apps/my-dir/my-app/tslint.json',
|
||||
lookupFn: json => json.extends,
|
||||
expectedValue: '../../../tslint.json'
|
||||
}
|
||||
].forEach(hasJsonValue);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -99,9 +99,6 @@ function updatePackageJson() {
|
||||
if (!packageJson.dependencies['rxjs-compat']) {
|
||||
packageJson.dependencies['rxjs-compat'] = rxjsVersion;
|
||||
}
|
||||
if (!packageJson.devDependencies['@ngrx/schematics']) {
|
||||
packageJson.devDependencies['@ngrx/schematics'] = ngrxSchematicsVersion;
|
||||
}
|
||||
if (!packageJson.devDependencies['@nrwl/schematics']) {
|
||||
packageJson.devDependencies['@nrwl/schematics'] = schematicsVersion;
|
||||
}
|
||||
|
||||
@ -51,7 +51,6 @@
|
||||
"@angular/compiler-cli": "<%= angularVersion %>",
|
||||
"@angular/language-service": "<%= angularVersion %>",
|
||||
"@angular-devkit/build-angular": "~0.6.1",
|
||||
"@ngrx/schematics": "<%= ngrxSchematicsVersion %>",
|
||||
"@nrwl/schematics": "<%= schematicsVersion %>",
|
||||
"jasmine-marbles": "<%= jasmineMarblesVersion %>",
|
||||
"@types/jasmine": "~2.8.6",
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import {Action} from "@ngrx/store";
|
||||
|
||||
export enum <%= className %>ActionTypes {
|
||||
Load<%= className %> = "[<%= className %>] Load <%= className %>",
|
||||
<%= className %>Loaded = "[<%= className %>] <%= className %> Loaded",
|
||||
<%= className %>LoadError = "[<%= className %>] <%= className %> Load Error"
|
||||
}
|
||||
|
||||
export class Load<%= className %> implements Action {
|
||||
readonly type = <%= className %>ActionTypes.Load<%= className %>;
|
||||
}
|
||||
|
||||
export class <%= className %>LoadError implements Action {
|
||||
readonly type = <%= className %>ActionTypes.Load<%= className %>;
|
||||
constructor(public payload: any) { }
|
||||
}
|
||||
|
||||
export class <%= className %>Loaded implements Action {
|
||||
readonly type = <%= className %>ActionTypes.<%= className %>Loaded;
|
||||
constructor(public payload: any[]) { }
|
||||
}
|
||||
|
||||
export type <%= className %>Action = Load<%= className %> | <%= className %>Loaded | <%= className %>LoadError;
|
||||
|
||||
export const from<%= className %>Actions = {
|
||||
Load<%= className %>,
|
||||
<%= className %>Loaded,
|
||||
<%= className %>LoadError
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import {TestBed, async} from '@angular/core/testing';
|
||||
|
||||
import {Observable} from 'rxjs';
|
||||
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import {StoreModule} from '@ngrx/store';
|
||||
import {provideMockActions} from '@ngrx/effects/testing';
|
||||
|
||||
import { NxModule } from '@nrwl/nx';
|
||||
import {DataPersistence} from '@nrwl/nx';
|
||||
import {hot} from '@nrwl/nx/testing';
|
||||
|
||||
import { <%= className %>Effects } from './<%= fileName %>.effects';
|
||||
import { Load<%= className %>, <%= className %>Loaded } from './<%= fileName %>.actions';
|
||||
|
||||
describe('<%= className %>Effects', () => {
|
||||
let actions: Observable<any>;
|
||||
let effects: <%= className %>Effects;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
NxModule.forRoot(),
|
||||
StoreModule.forRoot({}),
|
||||
EffectsModule.forRoot([])
|
||||
],
|
||||
providers: [
|
||||
<%= className %>Effects,
|
||||
DataPersistence,
|
||||
provideMockActions(() => actions)
|
||||
],
|
||||
});
|
||||
|
||||
effects = TestBed.get(<%= className %>Effects);
|
||||
});
|
||||
|
||||
describe('load<%= className %>$', () => {
|
||||
it('should work', () => {
|
||||
actions = hot('-a-|', {a: new Load<%= className %>()});
|
||||
expect(effects.load<%= className %>$).toBeObservable(
|
||||
hot('-a-|', {a: new <%= className %>Loaded([])})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Effect, Actions } from '@ngrx/effects';
|
||||
import { DataPersistence } from '@nrwl/nx';
|
||||
|
||||
import { <%= className %>State } from './<%= fileName %>.reducer';
|
||||
import { Load<%= className %>, <%= className %>Loaded, <%= className %>LoadError, <%= className %>ActionTypes } from './<%= fileName %>.actions';
|
||||
|
||||
@Injectable()
|
||||
export class <%= className %>Effects {
|
||||
@Effect() load<%= className %>$ = this.dataPersistence.fetch(<%= className %>ActionTypes.Load<%= className %>, {
|
||||
run: (action: Load<%= className %>, state: <%= className %>State) => {
|
||||
// Your custom REST 'load' logic goes here. For now just return an empty list...
|
||||
return new <%= className %>Loaded([]);
|
||||
},
|
||||
|
||||
onError: (action: Load<%= className %>, error) => {
|
||||
console.error('Error', error);
|
||||
return new <%= className %>LoadError(error);
|
||||
}
|
||||
});
|
||||
|
||||
constructor(
|
||||
private actions$: Actions,
|
||||
private dataPersistence: DataPersistence<<%= className %>State>) { }
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { <%= className %>Loaded } from './<%= fileName %>.actions';
|
||||
import { <%= className %>State, <%= className %>, initialState, <%= propertyName %>Reducer } from './<%= fileName %>.reducer';
|
||||
|
||||
describe('<%= className %> Reducer', () => {
|
||||
const get<%= className %>Id = (it) => it['id'];
|
||||
let create<%= className %>;
|
||||
|
||||
beforeEach(() => {
|
||||
create<%= className %> = ( id:string, name = '' ): <%= className %> => ({
|
||||
id,
|
||||
name: name || `name-${id}`
|
||||
});
|
||||
});
|
||||
|
||||
describe('valid <%= className %> actions ', () => {
|
||||
it('should return set the list of known <%= className %>', () => {
|
||||
const <%= propertyName %>s = [create<%= className %>( 'PRODUCT-AAA' ),create<%= className %>( 'PRODUCT-zzz' )];
|
||||
const action = new <%= className %>Loaded(<%= propertyName %>s);
|
||||
const result : <%= className %>State = <%= propertyName %>Reducer(initialState, action);
|
||||
const selId : string = get<%= className %>Id(result.list[1]);
|
||||
|
||||
expect(result.loaded).toBe(true);
|
||||
expect(result.list.length).toBe(2);
|
||||
expect(selId).toBe('PRODUCT-zzz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('unknown action', () => {
|
||||
it('should return the initial state', () => {
|
||||
const action = {} as any;
|
||||
const result = <%= propertyName %>Reducer(initialState, action);
|
||||
|
||||
expect(result).toBe(initialState);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,40 @@
|
||||
import { <%= className %>Action, <%= className %>ActionTypes } from './<%= fileName %>.actions';
|
||||
|
||||
/**
|
||||
* Interface for the '<%= className %>' data used in
|
||||
* - <%= className %>State, and
|
||||
* - <%= propertyName %>Reducer
|
||||
*
|
||||
* Note: remove if already defined in another module
|
||||
*/
|
||||
export interface <%= className %> {
|
||||
}
|
||||
|
||||
export interface <%= className %>State {
|
||||
list : <%= className %>[]; // analogous to a sql normalized table
|
||||
loaded : boolean; // has the <%= className %> list been loaded
|
||||
selectedId ?: string | number; // which <%= className %> record has been selected
|
||||
error ?: any; // last none error (if any)
|
||||
};
|
||||
|
||||
export const initialState: <%= className %>State = {
|
||||
list : [ ],
|
||||
loaded : false
|
||||
};
|
||||
|
||||
export function <%= propertyName %>Reducer(
|
||||
state: <%= className %>State = initialState,
|
||||
action: <%= className %>Action): <%= className %>State
|
||||
{
|
||||
switch (action.type) {
|
||||
case <%= className %>ActionTypes.<%= className %>Loaded: {
|
||||
state = {
|
||||
...state,
|
||||
list : action.payload,
|
||||
loaded: true
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { <%= className %>, <%= className %>State } from './<%= fileName %>.reducer';
|
||||
import { <%= propertyName %>Query } from './<%= fileName %>.selectors';
|
||||
|
||||
describe('<%= className %> Selectors', () => {
|
||||
const ERROR_MSG = 'No Error Available';
|
||||
const get<%= className %>Id = (it) => it['id'];
|
||||
|
||||
let storeState;
|
||||
|
||||
beforeEach(() => {
|
||||
const create<%= className %> = ( id:string, name = '' ): <%= className %> => ({
|
||||
id,
|
||||
name: name || `name-${id}`
|
||||
});
|
||||
storeState = {
|
||||
<%= propertyName %> : {
|
||||
list : [
|
||||
create<%= className %>( 'PRODUCT-AAA' ),
|
||||
create<%= className %>( 'PRODUCT-BBB' ),
|
||||
create<%= className %>( 'PRODUCT-CCC' )
|
||||
],
|
||||
selectedId : 'PRODUCT-BBB',
|
||||
error : ERROR_MSG,
|
||||
loaded : true
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
describe('<%= className %> Selectors', () => {
|
||||
|
||||
it('getAll<%= className %>() should return the list of <%= className %>', () => {
|
||||
const results = <%= propertyName %>Query.getAll<%= className %>(storeState);
|
||||
const selId = get<%= className %>Id(results[1]);
|
||||
|
||||
expect(results.length).toBe(3);
|
||||
expect(selId).toBe('PRODUCT-BBB');
|
||||
});
|
||||
|
||||
it('getSelected<%= className %>() should return the selected <%= className %>', () => {
|
||||
const result = <%= propertyName %>Query.getSelected<%= className %>(storeState);
|
||||
const selId = get<%= className %>Id(result);
|
||||
|
||||
expect(selId).toBe('PRODUCT-BBB');
|
||||
});
|
||||
|
||||
it('getLoaded() should return the current \'loaded\' status', () => {
|
||||
const result = <%= propertyName %>Query.getLoaded(storeState);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('getError() should return the current \'error\' storeState', () => {
|
||||
const result = <%= propertyName %>Query.getError(storeState);
|
||||
|
||||
expect(result).toBe(ERROR_MSG);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
||||
import { <%= className %>State } from './<%= fileName %>.reducer';
|
||||
|
||||
// Lookup the '<%= className %>' feature state managed by NgRx
|
||||
const get<%= className %>State = createFeatureSelector<<%= className %>State>('<%= propertyName %>');
|
||||
|
||||
const getLoaded = createSelector( get<%= className %>State, (state:<%= className %>State) => state.loaded );
|
||||
const getError = createSelector( get<%= className %>State, (state:<%= className %>State) => state.error );
|
||||
|
||||
const getAll<%= className %> = createSelector( get<%= className %>State, getLoaded, (state:<%= className %>State, isLoaded) => {
|
||||
return isLoaded ? state.list : [ ];
|
||||
});
|
||||
const getSelectedId = createSelector( get<%= className %>State, (state:<%= className %>State) => state.selectedId );
|
||||
const getSelected<%= className %> = createSelector( getAll<%= className %>, getSelectedId, (<%= propertyName %>, id) => {
|
||||
let result = <%= propertyName %>.find(<%= propertyName %> => <%= propertyName %>['id'] == id);
|
||||
return result ? Object.assign({}, result) : undefined;
|
||||
});
|
||||
|
||||
export const <%= propertyName %>Query = {
|
||||
getLoaded,
|
||||
getError,
|
||||
getAll<%= className %>,
|
||||
getSelected<%= className %>
|
||||
};
|
||||
@ -1,6 +1,9 @@
|
||||
import {
|
||||
apply,
|
||||
chain,
|
||||
externalSchematic,
|
||||
url,
|
||||
mergeWith,
|
||||
template,
|
||||
move,
|
||||
Rule,
|
||||
Tree,
|
||||
@ -15,78 +18,14 @@ import { names, toFileName } from '../../utils/name-utils';
|
||||
import {
|
||||
addImportsToModule,
|
||||
addNgRxToPackageJson,
|
||||
RequestContext,
|
||||
updateNgrxActions,
|
||||
updateNgrxEffects,
|
||||
updateNgrxReducers
|
||||
addExportsToBarrel,
|
||||
RequestContext
|
||||
} from './rules';
|
||||
import { formatFiles } from '../../utils/rules/format-files';
|
||||
|
||||
function effectsSpec(className: string, fileName: string) {
|
||||
return `
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {StoreModule} from '@ngrx/store';
|
||||
import {provideMockActions} from '@ngrx/effects/testing';
|
||||
import {DataPersistence} from '@nrwl/nx';
|
||||
import {hot} from '@nrwl/nx/testing';
|
||||
|
||||
import {${className}Effects} from './${fileName}.effects';
|
||||
import {Load${className}, ${className}Loaded } from './${fileName}.actions';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
describe('${className}Effects', () => {
|
||||
let actions$: Observable<any>;
|
||||
let effects$: ${className}Effects;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
StoreModule.forRoot({}),
|
||||
],
|
||||
providers: [
|
||||
${className}Effects,
|
||||
DataPersistence,
|
||||
provideMockActions(() => actions$)
|
||||
],
|
||||
});
|
||||
|
||||
effects$ = TestBed.get(${className}Effects);
|
||||
});
|
||||
|
||||
describe('someEffect', () => {
|
||||
it('should work', () => {
|
||||
actions$ = hot('-a-|', {a: new Load${className}({})});
|
||||
expect(effects$.load${className}$).toBeObservable(
|
||||
hot('-a-|', {a: new ${className}Loaded({})})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
function reducerSpec(
|
||||
className: string,
|
||||
fileName: string,
|
||||
propertyName: string
|
||||
) {
|
||||
return `
|
||||
import { ${className}Loaded } from './${fileName}.actions';
|
||||
import { ${propertyName}Reducer, initialState } from './${fileName}.reducer';
|
||||
|
||||
describe('${propertyName}Reducer', () => {
|
||||
it('should work', () => {
|
||||
const action: ${className}Loaded = new ${className}Loaded({});
|
||||
const actual = ${propertyName}Reducer(initialState, action);
|
||||
expect(actual).toEqual({});
|
||||
});
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rule to generate the Nx 'ngrx' Collection
|
||||
* Note: see https://nrwl.io/nx/guide-setting-up-ngrx for guide to generated files
|
||||
*/
|
||||
export default function generateNgrxCollection(_options: Schema): Rule {
|
||||
return (host: Tree, context: SchematicContext) => {
|
||||
@ -97,23 +36,19 @@ export default function generateNgrxCollection(_options: Schema): Rule {
|
||||
options,
|
||||
host
|
||||
};
|
||||
const fileGeneration = !options.onlyEmptyRoot
|
||||
? [generateNgrxFilesFromTemplates(options)]
|
||||
: [];
|
||||
|
||||
const fileGeneration = options.onlyEmptyRoot
|
||||
? []
|
||||
: [
|
||||
generateNgrxFiles(requestContext),
|
||||
generateNxFiles(requestContext),
|
||||
updateNgrxActions(requestContext),
|
||||
updateNgrxReducers(requestContext),
|
||||
updateNgrxEffects(requestContext)
|
||||
];
|
||||
|
||||
const moduleModification = options.onlyAddFiles
|
||||
? []
|
||||
: [addImportsToModule(requestContext)];
|
||||
const packageJsonModification = options.skipPackageJson
|
||||
? []
|
||||
: [addNgRxToPackageJson()];
|
||||
const moduleModification = !options.onlyAddFiles
|
||||
? [
|
||||
addImportsToModule(requestContext),
|
||||
addExportsToBarrel(requestContext.options)
|
||||
]
|
||||
: [];
|
||||
const packageJsonModification = !options.skipPackageJson
|
||||
? [addNgRxToPackageJson()]
|
||||
: [];
|
||||
|
||||
return chain([
|
||||
...fileGeneration,
|
||||
@ -129,67 +64,25 @@ export default function generateNgrxCollection(_options: Schema): Rule {
|
||||
// ********************************************************
|
||||
|
||||
/**
|
||||
* Generate the Nx files that are NOT created by the @ngrx/schematic(s)
|
||||
* Generate 'feature' scaffolding: actions, reducer, effects, interfaces, selectors
|
||||
*/
|
||||
function generateNxFiles(context: RequestContext) {
|
||||
return (host: Tree) => {
|
||||
const n = names(context.featureName);
|
||||
host.overwrite(
|
||||
path.join(
|
||||
context.moduleDir,
|
||||
context.options.directory,
|
||||
`${context.featureName}.effects.spec.ts`
|
||||
),
|
||||
effectsSpec(n.className, n.fileName)
|
||||
);
|
||||
host.overwrite(
|
||||
path.join(
|
||||
context.moduleDir,
|
||||
context.options.directory,
|
||||
`${context.featureName}.reducer.spec.ts`
|
||||
),
|
||||
reducerSpec(n.className, n.fileName, n.propertyName)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Using @ngrx/schematics, generate scaffolding for 'feature': action, reducer, effect files
|
||||
*/
|
||||
function generateNgrxFiles(context: RequestContext) {
|
||||
return chain([
|
||||
externalSchematic('@ngrx/schematics', 'feature', {
|
||||
name: context.featureName,
|
||||
sourceDir: './',
|
||||
flat: false
|
||||
}),
|
||||
moveToNxMonoTree(
|
||||
context.featureName,
|
||||
context.moduleDir,
|
||||
context.options.directory
|
||||
)
|
||||
function generateNgrxFilesFromTemplates(options: Schema) {
|
||||
const name = options.name;
|
||||
const moduleDir = path.dirname(options.module);
|
||||
const templateSource = apply(url('./files'), [
|
||||
template({ ...options, tmpl: '', ...names(name) }),
|
||||
move(moduleDir)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngrx/schematics generates files in:
|
||||
* `/apps/<ngrxFeatureName>/`
|
||||
*
|
||||
* For Nx monorepo, however, we need to move the files to either
|
||||
* a) apps/<appName>/src/app/<directory>, or
|
||||
* b) libs/<libName>/src/<directory>
|
||||
*/
|
||||
function moveToNxMonoTree(
|
||||
ngrxFeatureName: string,
|
||||
nxDir: string,
|
||||
directory: string
|
||||
): Rule {
|
||||
return move(`app/${ngrxFeatureName}`, path.join(nxDir, directory));
|
||||
return mergeWith(templateSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the parent 'directory' for the specified
|
||||
*/
|
||||
function normalizeOptions(options: Schema): Schema {
|
||||
return { ...options, directory: toFileName(options.directory) };
|
||||
return {
|
||||
...options,
|
||||
directory: toFileName(options.directory)
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,203 +0,0 @@
|
||||
# ngrx
|
||||
--------
|
||||
|
||||
## Overview
|
||||
|
||||
Generates a ngrx feature set containing an `init`, `interfaces`, `actions`, `reducer` and `effects` files.
|
||||
|
||||
You use this schematic to build out a new ngrx feature area that provides a new piece of state.
|
||||
|
||||
## Command
|
||||
|
||||
```sh
|
||||
ng generate ngrx FeatureName [options]
|
||||
```
|
||||
|
||||
##### OR
|
||||
|
||||
```sh
|
||||
ng generate f FeatureName [options]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
Specifies the name of the ngrx feature (e.g., Products, User, etc.)
|
||||
|
||||
- `name`
|
||||
- Type: `string`
|
||||
- Required: true
|
||||
|
||||
Path to Angular Module. Also used to determine the parent directory for the new **+state**
|
||||
directory; unless the `--directory` option is used to override the dir name.
|
||||
|
||||
> e.g. --module=apps/myapp/src/app/app.module.ts
|
||||
|
||||
- `--module`
|
||||
- Type: `string`
|
||||
- Required: true
|
||||
|
||||
Specifies the directory name used to nest the **ngrx** files within a folder.
|
||||
|
||||
- `--directory`
|
||||
- Type: `string`
|
||||
- Default: `+state`
|
||||
|
||||
#### Examples
|
||||
|
||||
Generate a `User` feature set and register it within an `Angular Module`.
|
||||
|
||||
```sh
|
||||
ng generate ngrx User -m apps/myapp/src/app/app.module.ts
|
||||
ng g ngrx Producrts -m libs/mylib/src/mylib.module.ts
|
||||
```
|
||||
|
||||
|
||||
Generate a `User` feature set within a `user` folder and register it with the `user.module.ts` file in the same `user` folder.
|
||||
|
||||
```sh
|
||||
ng g ngrx User -m apps/myapp/src/app/app.module.ts -directory user
|
||||
```
|
||||
|
||||
## Generated Files
|
||||
|
||||
The files generated are shown below and include placeholders for the *feature* name specified.
|
||||
|
||||
> The <Feature> notation used be below indicates a placeholder for the actual *feature* name.
|
||||
|
||||
* [<feature>.actions.ts](#featureactionsts)
|
||||
* [<feature>.reducer.ts](#featurereducerts)
|
||||
* [<feature>.effects.ts](#featureeffectsts)
|
||||
* [<feature>.selectors.ts](#featureselectorsts)
|
||||
* [<feature>.facade.ts](#featurefacadests)
|
||||
|
||||
* [../app.module.ts](#appmodulets)
|
||||
|
||||
#### <feature>.actions.ts
|
||||
|
||||
```ts
|
||||
import {Action} from "@ngrx/store";
|
||||
|
||||
export enum <Feature>ActionTypes {
|
||||
<Feature> = "[<Feature>] Action",
|
||||
Load<Feature> = "[<Feature>] Load Data",
|
||||
<Feature>Loaded = "[<Feature>] Data Loaded"
|
||||
}
|
||||
|
||||
export class <Feature> implements Action {
|
||||
readonly type = <Feature>ActionTypes.<Feature>;
|
||||
}
|
||||
|
||||
export class Load<Feature> implements Action {
|
||||
readonly type = <Feature>ActionTypes.Load<Feature>;
|
||||
constructor(public payload: any) { }
|
||||
}
|
||||
|
||||
export class DataLoaded implements Action {
|
||||
readonly type = <Feature>ActionTypes.<Feature>Loaded;
|
||||
constructor(public payload: any) { }
|
||||
}
|
||||
|
||||
export type <Feature>Actions = <Feature> | Load<Feature> | <Feature>Loaded;
|
||||
```
|
||||
|
||||
#### <feature>.reducer.ts
|
||||
```ts
|
||||
import { <Feature> } from './<feature>.interfaces';
|
||||
import { <Feature>Action, <Feature>ActionTypes } from './<feature>.actions';
|
||||
|
||||
/**
|
||||
* Interface for the '<Feature>' data used in
|
||||
* - <Feature>State, and
|
||||
* - <feature>Reducer
|
||||
*/
|
||||
export interface <Feature>Data {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to the part of the Store containing <Feature>State
|
||||
* and other information related to <Feature>Data.
|
||||
*/
|
||||
export interface <Feature>State {
|
||||
readonly <feature>: <Feature>Data;
|
||||
}
|
||||
|
||||
export const initialState: <Feature>Data = { };
|
||||
|
||||
export function <feature>Reducer(state: <Feature>Data = initialState, action: <Feature>Actions): <Feature>Data {
|
||||
switch (action.type) {
|
||||
case <Feature>ActionTypes.<Feature>Loaded: {
|
||||
return { ...state, ...action.payload };
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### <feature>.effects.ts
|
||||
```ts
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Effect, Actions } from '@ngrx/effects';
|
||||
import { DataPersistence } from '@nrwl/nx';
|
||||
|
||||
import { <Feature> } from './<feature>.interfaces';
|
||||
import { Load<Feature>, <Feature>Loaded, <Feature>ActionTypes } from './<feature>.actions';
|
||||
|
||||
@Injectable()
|
||||
export class <Feature>Effects {
|
||||
@Effect() load<Feature>$ = this.dataPersistence.fetch(<Feature>ActionTypes.Load<Feature>, {
|
||||
run: (action: Load<Feature>, state: <Feature>) => {
|
||||
return new <Feature>Loaded({});
|
||||
},
|
||||
|
||||
onError: (action: Load<Feature>, error) => {
|
||||
console.error('Error', error);
|
||||
}
|
||||
});
|
||||
|
||||
constructor(
|
||||
private actions: Actions,
|
||||
private dataPersistence: DataPersistence<Feature>) { }
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### ../app.module.ts
|
||||
```ts
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { AppComponent } from './app.component';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import {
|
||||
<feature>Reducer,
|
||||
<Feature>State,
|
||||
<Feature>Data,
|
||||
initialState as <feature>InitialState
|
||||
} from './+state/<Feature>.reducer';
|
||||
import { <Feature>Effects } from './+state/<Feature>.effects';
|
||||
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
||||
import { environment } from '../environments/environment';
|
||||
import { StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||
import { storeFreeze } from 'ngrx-store-freeze';
|
||||
|
||||
@NgModule({
|
||||
imports: [BrowserModule, RouterModule.forRoot([]),
|
||||
StoreModule.forRoot({ <feature>: <feature>Reducer }, {
|
||||
initialState: { <feature>: <feature>InitialState },
|
||||
metaReducers: !environment.production ? [storeFreeze] : []
|
||||
}),
|
||||
EffectsModule.forRoot([<Feature>Effects]),
|
||||
!environment.production ? StoreDevtoolsModule.instrument() : [],
|
||||
StoreRouterConnectingModule],
|
||||
declarations: [AppComponent],
|
||||
bootstrap: [AppComponent],
|
||||
providers: [<Feature>Effects]
|
||||
})
|
||||
export class AppModule {
|
||||
}
|
||||
|
||||
```
|
||||
@ -10,8 +10,10 @@ import * as path from 'path';
|
||||
import { findModuleParent } from '../../utils/name-utils';
|
||||
import {
|
||||
createApp,
|
||||
createLib,
|
||||
createEmptyWorkspace,
|
||||
AppConfig,
|
||||
getLibConfig,
|
||||
getAppConfig
|
||||
} from '../../utils/testing-utils';
|
||||
|
||||
@ -70,16 +72,16 @@ describe('ngrx', () => {
|
||||
expect(appModule).toContain('EffectsModule.forRoot');
|
||||
expect(appModule).toContain('!environment.production ? [storeFreeze] : []');
|
||||
|
||||
expect(appModule).toContain('appReducer, initialState');
|
||||
expect(appModule).not.toContain('AppData');
|
||||
expect(appModule).not.toContain('AppState');
|
||||
expect(appModule).toContain('app: appReducer');
|
||||
expect(appModule).toContain('initialState : { app : appInitialState }');
|
||||
|
||||
[
|
||||
'/apps/myapp/src/app/+state/app.actions.ts',
|
||||
'/apps/myapp/src/app/+state/app.effects.ts',
|
||||
'/apps/myapp/src/app/+state/app.effects.spec.ts',
|
||||
'/apps/myapp/src/app/+state/app.reducer.ts',
|
||||
'/apps/myapp/src/app/+state/app.reducer.spec.ts'
|
||||
'/apps/myapp/src/app/+state/app.reducer.spec.ts',
|
||||
'/apps/myapp/src/app/+state/app.selectors.ts'
|
||||
].forEach(fileName => {
|
||||
expect(tree.exists(fileName)).toBeTruthy();
|
||||
});
|
||||
@ -116,7 +118,8 @@ describe('ngrx', () => {
|
||||
const appModule = getFileContent(tree, '/apps/myapp/src/app/app.module.ts');
|
||||
expect(appModule).toContain('StoreModule.forFeature');
|
||||
expect(appModule).toContain('EffectsModule.forFeature');
|
||||
expect(appModule).toContain('initialState: stateInitialState');
|
||||
expect(appModule).toContain("'state', stateReducer");
|
||||
expect(appModule).toContain('{ initialState: stateInitialState }');
|
||||
expect(appModule).not.toContain(
|
||||
'!environment.production ? [storeFreeze] : []'
|
||||
);
|
||||
@ -166,9 +169,13 @@ describe('ngrx', () => {
|
||||
'!environment.production ? [storeFreeze] : []'
|
||||
);
|
||||
|
||||
expect(
|
||||
tree.exists(`/apps/myapp/src/app/+state/state.actions.ts`)
|
||||
).toBeTruthy();
|
||||
[
|
||||
'/apps/myapp/src/app/+state/state.effects.ts',
|
||||
'/apps/myapp/src/app/+state/state.reducer.ts',
|
||||
'/apps/myapp/src/app/+state/state.selectors.ts'
|
||||
].forEach(fileName => {
|
||||
expect(tree.exists(fileName)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should update package.json', () => {
|
||||
@ -197,142 +204,169 @@ describe('ngrx', () => {
|
||||
},
|
||||
appTree
|
||||
)
|
||||
).toThrow("should have required property 'module'");
|
||||
).toThrow('Specified module does not exist');
|
||||
});
|
||||
|
||||
it('should create the ngrx files', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const hasFile = file => expect(tree.exists(file)).toBeTruthy();
|
||||
const tree = buildNgrxTree(appConfig);
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
describe('code generation', () => {
|
||||
it('should scaffold the ngrx "user" files', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const hasFile = file => expect(tree.exists(file)).toBeTruthy();
|
||||
const tree = buildNgrxTree(appConfig);
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
|
||||
hasFile(`${statePath}/user.actions.ts`);
|
||||
hasFile(`${statePath}/user.effects.ts`);
|
||||
hasFile(`${statePath}/user.effects.spec.ts`);
|
||||
hasFile(`${statePath}/user.reducer.ts`);
|
||||
hasFile(`${statePath}/user.reducer.spec.ts`);
|
||||
});
|
||||
hasFile(`${statePath}/user.actions.ts`);
|
||||
hasFile(`${statePath}/user.effects.ts`);
|
||||
hasFile(`${statePath}/user.effects.spec.ts`);
|
||||
hasFile(`${statePath}/user.reducer.ts`);
|
||||
hasFile(`${statePath}/user.reducer.spec.ts`);
|
||||
hasFile(`${statePath}/user.selectors.ts`);
|
||||
});
|
||||
|
||||
it('should create ngrx action enums', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig);
|
||||
it('should build the ngrx actions', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig, 'users');
|
||||
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const content = getFileContent(tree, `${statePath}/user.actions.ts`);
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const content = getFileContent(tree, `${statePath}/users.actions.ts`);
|
||||
|
||||
expect(content).toContain('UserActionTypes');
|
||||
expect(content).toContain("LoadUser = '[User] Load Data'");
|
||||
expect(content).toContain("UserLoaded = '[User] Data Loaded'");
|
||||
});
|
||||
expect(content).toContain('UsersActionTypes');
|
||||
|
||||
it('should create ngrx action classes', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig);
|
||||
expect(content).toContain('LoadUsers = "[Users] Load Users"');
|
||||
expect(content).toContain('UsersLoaded = "[Users] Users Loaded"');
|
||||
expect(content).toContain('UsersLoadError = "[Users] Users Load Error"');
|
||||
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const content = getFileContent(tree, `${statePath}/user.actions.ts`);
|
||||
expect(content).toContain('class LoadUsers implements Action');
|
||||
expect(content).toContain('class UsersLoaded implements Action');
|
||||
expect(content).toContain(
|
||||
'type UsersAction = LoadUsers | UsersLoaded | UsersLoadError'
|
||||
);
|
||||
expect(content).toContain('export const fromUsersActions');
|
||||
});
|
||||
|
||||
expect(content).toContain('class LoadUser implements Action');
|
||||
expect(content).toContain('class UserLoaded implements Action');
|
||||
});
|
||||
it('should build the ngrx selectors', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig, 'users');
|
||||
|
||||
it('should enhance the ngrx action type', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig);
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const content = getFileContent(tree, `${statePath}/users.selectors.ts`);
|
||||
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const content = getFileContent(tree, `${statePath}/user.actions.ts`);
|
||||
expect(content).toContain(
|
||||
'type UserActions = User | LoadUser | UserLoaded'
|
||||
);
|
||||
});
|
||||
[
|
||||
`import { UsersState } from './users.reducer'`,
|
||||
`export const usersQuery`
|
||||
].forEach(text => {
|
||||
expect(content).toContain(text);
|
||||
});
|
||||
});
|
||||
|
||||
it('should enhance the ngrx reducer', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig);
|
||||
it('should build the ngrx reducer', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig, 'user');
|
||||
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const content = getFileContent(tree, `${statePath}/user.reducer.ts`);
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const content = getFileContent(tree, `${statePath}/user.reducer.ts`);
|
||||
|
||||
expect(content).not.toContain('function reducer');
|
||||
expect(content).not.toContain('function reducer');
|
||||
|
||||
[
|
||||
`import { UserActions, UserActionTypes } from \'./user.actions\'`,
|
||||
`export interface UserData`,
|
||||
`export interface UserState`,
|
||||
`readonly user: UserData`,
|
||||
`const initialState: UserData`,
|
||||
'function userReducer(state = initialState, action: UserActions): UserData',
|
||||
'case UserActionTypes.UserLoaded'
|
||||
].forEach(text => {
|
||||
expect(content).toContain(text);
|
||||
[
|
||||
`import { UserAction, UserActionTypes } from \'./user.actions\'`,
|
||||
`export interface User`,
|
||||
`export interface UserState`,
|
||||
'export function userReducer',
|
||||
'state: UserState = initialState',
|
||||
'action: UserAction): UserState',
|
||||
'case UserActionTypes.UserLoaded'
|
||||
].forEach(text => {
|
||||
expect(content).toContain(text);
|
||||
});
|
||||
});
|
||||
|
||||
it('should build the ngrx effects', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig, 'users');
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const content = getFileContent(tree, `${statePath}/users.effects.ts`);
|
||||
|
||||
[
|
||||
`import { DataPersistence } from \'@nrwl/nx\'`,
|
||||
`import { LoadUsers, UsersLoaded, UsersLoadError, UsersActionTypes } from \'./users.actions\'`,
|
||||
`loadUsers$`,
|
||||
`run: (action: LoadUsers, state: UsersState)`,
|
||||
`return new UsersLoaded([])`,
|
||||
`return new UsersLoadError(error)`,
|
||||
'private actions$: Actions',
|
||||
'private dataPersistence: DataPersistence<UsersState>)'
|
||||
].forEach(text => {
|
||||
expect(content).toContain(text);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should produce proper specs for the ngrx reducer', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig);
|
||||
describe('spec test', () => {
|
||||
it('should produce proper specs for the ngrx reducer', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig);
|
||||
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const contents = tree.readContent(`${statePath}/user.reducer.spec.ts`);
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const contents = tree.readContent(`${statePath}/user.reducer.spec.ts`);
|
||||
|
||||
expect(contents).toContain(`describe('userReducer', () => {`);
|
||||
expect(contents).toContain(
|
||||
`const action: UserLoaded = new UserLoaded({});`
|
||||
);
|
||||
expect(contents).toContain(
|
||||
`const actual = userReducer(initialState, action);`
|
||||
);
|
||||
});
|
||||
expect(contents).toContain(`describe('User Reducer', () => {`);
|
||||
expect(contents).toContain(
|
||||
'const result = userReducer(initialState, action);'
|
||||
);
|
||||
});
|
||||
|
||||
it('should produce proper specs for the ngrx reducer for a name with a dash', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = schematicRunner.runSchematic(
|
||||
'ngrx',
|
||||
{
|
||||
name: 'super-user',
|
||||
module: appConfig.appModule
|
||||
},
|
||||
appTree
|
||||
);
|
||||
it('should update the barrel API with exports for ngrx selector, and reducer', () => {
|
||||
appTree = createLib(appTree, 'flights');
|
||||
let libConfig = getLibConfig();
|
||||
let tree = schematicRunner.runSchematic(
|
||||
'ngrx',
|
||||
{
|
||||
name: 'super-users',
|
||||
module: libConfig.module
|
||||
},
|
||||
appTree
|
||||
);
|
||||
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const contents = tree.readContent(
|
||||
`${statePath}/super-user.reducer.spec.ts`
|
||||
);
|
||||
const barrel = tree.readContent(libConfig.barrel);
|
||||
expect(barrel).toContain(
|
||||
`export * from './lib/+state/super-users.selectors';`
|
||||
);
|
||||
expect(barrel).toContain(
|
||||
`export * from './lib/+state/super-users.reducer';`
|
||||
);
|
||||
});
|
||||
|
||||
expect(contents).toContain(`describe('superUserReducer', () => {`);
|
||||
expect(contents).toContain(
|
||||
`const action: SuperUserLoaded = new SuperUserLoaded({});`
|
||||
);
|
||||
expect(contents).toContain(
|
||||
`const actual = superUserReducer(initialState, action);`
|
||||
);
|
||||
});
|
||||
it('should produce proper specs for the ngrx reducer for a name with a dash', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = schematicRunner.runSchematic(
|
||||
'ngrx',
|
||||
{
|
||||
name: 'super-users',
|
||||
module: appConfig.appModule
|
||||
},
|
||||
appTree
|
||||
);
|
||||
|
||||
it('should enhance the ngrx effects', () => {
|
||||
const appConfig = getAppConfig();
|
||||
const tree = buildNgrxTree(appConfig);
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const content = getFileContent(tree, `${statePath}/user.effects.ts`);
|
||||
const statePath = `${findModuleParent(appConfig.appModule)}/+state`;
|
||||
const contents = tree.readContent(
|
||||
`${statePath}/super-users.reducer.spec.ts`
|
||||
);
|
||||
|
||||
[
|
||||
`import { DataPersistence } from \'@nrwl/nx\'`,
|
||||
`import { UserActions, UserActionTypes, LoadUser, UserLoaded } from \'./user.actions\'`,
|
||||
`loadUser$`,
|
||||
`run: (action: LoadUser, state: UserState)`,
|
||||
`return new UserLoaded(state)`,
|
||||
'constructor(private actions$: Actions, private dataPersistence: DataPersistence<UserState>)'
|
||||
].forEach(text => {
|
||||
expect(content).toContain(text);
|
||||
expect(contents).toContain(`describe('SuperUsers Reducer', () => {`);
|
||||
expect(contents).toContain(
|
||||
`const result = superUsersReducer(initialState, action);`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function buildNgrxTree(appConfig: AppConfig): UnitTestTree {
|
||||
function buildNgrxTree(
|
||||
appConfig: AppConfig,
|
||||
featureName: string = 'user'
|
||||
): UnitTestTree {
|
||||
return schematicRunner.runSchematic(
|
||||
'ngrx',
|
||||
{
|
||||
name: 'user',
|
||||
name: featureName,
|
||||
module: appConfig.appModule
|
||||
},
|
||||
appTree
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
import * as ts from 'typescript';
|
||||
import * as path from 'path';
|
||||
import { Rule, Tree } from '@angular-devkit/schematics';
|
||||
|
||||
import { names } from '../../../utils/name-utils';
|
||||
import { insert, addGlobal } from '../../../utils/ast-utils';
|
||||
import { Schema } from '../schema';
|
||||
|
||||
/**
|
||||
* Add ngrx feature exports to the public barrel in the feature library
|
||||
*/
|
||||
export function addExportsToBarrel(options: Schema): Rule {
|
||||
return (host: Tree) => {
|
||||
if (!host.exists(options.module)) {
|
||||
throw new Error('Specified module does not exist');
|
||||
}
|
||||
|
||||
// Only update the public barrel for feature libraries
|
||||
if (options.root != true) {
|
||||
const moduleDir = path.dirname(options.module);
|
||||
const indexFilePath = path.join(moduleDir, '../index.ts');
|
||||
|
||||
const buffer = host.read(indexFilePath);
|
||||
if (!!buffer) {
|
||||
// AST to 'index.ts' barrel for the public API
|
||||
const indexSource = buffer!.toString('utf-8');
|
||||
const indexSourceFile = ts.createSourceFile(
|
||||
indexFilePath,
|
||||
indexSource,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
// Public API for the feature interfaces, selectors
|
||||
const { fileName } = names(options.name);
|
||||
const statePath = `./lib/${options.directory}/${fileName}`;
|
||||
|
||||
insert(host, indexFilePath, [
|
||||
...addGlobal(
|
||||
indexSourceFile,
|
||||
indexFilePath,
|
||||
`export * from '${statePath}.reducer';`
|
||||
),
|
||||
...addGlobal(
|
||||
indexSourceFile,
|
||||
indexFilePath,
|
||||
`export * from '${statePath}.selectors';`
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return host;
|
||||
};
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { Rule, Tree } from '@angular-devkit/schematics';
|
||||
import { Change } from '@ngrx/schematics/src/utility/change';
|
||||
import { Change } from '@schematics/angular/utility/change';
|
||||
import { insertImport } from '@schematics/angular/utility/route-utils';
|
||||
import * as ts from 'typescript';
|
||||
import {
|
||||
@ -39,7 +39,7 @@ export function addImportsToModule(context: RequestContext): Rule {
|
||||
const featureName = `${toPropertyName(context.featureName)}`;
|
||||
const reducerName = `${toPropertyName(context.featureName)}Reducer`;
|
||||
const effectsName = `${toClassName(context.featureName)}Effects`;
|
||||
const reducerImports = `${reducerName}, initialState as ${featureName}InitialState`;
|
||||
const reducerImports = `initialState as ${featureName}InitialState, ${reducerName}`;
|
||||
|
||||
const storeReducers = `{ ${featureName}: ${reducerName} }`;
|
||||
const storeInitState = `initialState : { ${featureName} : ${featureName}InitialState }`;
|
||||
@ -95,7 +95,7 @@ export function addImportsToModule(context: RequestContext): Rule {
|
||||
addImport.apply(this, effectsModule),
|
||||
addImport(reducerImports, reducerPath),
|
||||
addImport(effectsName, effectsPath),
|
||||
...addProviderToModule(source, modulePath, effectsName)
|
||||
...addProviderToModule(source, modulePath, `${effectsName}`)
|
||||
];
|
||||
|
||||
if (context.options.root) {
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
export { RequestContext } from './request-context';
|
||||
export { updateNgrxReducers } from './update-reducers';
|
||||
export { updateNgrxActions } from './update-actions';
|
||||
export { updateNgrxEffects } from './update-effects';
|
||||
export { addImportsToModule } from './add-imports-to-module';
|
||||
export { addNgRxToPackageJson } from './add-ngrx-to-package-json';
|
||||
export { addExportsToBarrel } from './add-exports-barrel';
|
||||
|
||||
@ -1,103 +0,0 @@
|
||||
import * as ts from 'typescript';
|
||||
import { SchematicsException, Rule, Tree } from '@angular-devkit/schematics';
|
||||
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
|
||||
import { toClassName } from '../../../utils/name-utils';
|
||||
import {
|
||||
insert,
|
||||
addClass,
|
||||
addEnumeratorValues,
|
||||
addUnionTypes
|
||||
} from '../../../utils/ast-utils';
|
||||
|
||||
import { RequestContext, buildNameToNgrxFile } from './request-context';
|
||||
|
||||
/**
|
||||
* Add custom actions to <featureName>.actions.ts
|
||||
* See Ngrx Enhancement doc: https://bit.ly/2I5QwxQ
|
||||
*
|
||||
* Desired output:
|
||||
*
|
||||
* ```
|
||||
* import {Action} from "@ngrx/store";
|
||||
*
|
||||
* export enum <Feature>ActionTypes {
|
||||
* <Feature> = "[<Feature>] Action",
|
||||
* Load<Feature> = "[<Feature>] Load Data",
|
||||
* <Feature>Loaded = "[<Feature>] Data Loaded"
|
||||
* }
|
||||
*
|
||||
* export class <Feature> implements Action {
|
||||
* readonly type = <Feature>ActionTypes.<Feature>;
|
||||
* }
|
||||
*
|
||||
* export class Load<Feature> implements Action {
|
||||
* readonly type = <Feature>ActionTypes.Load<Feature>;
|
||||
* constructor(public payload: any) { }
|
||||
* }
|
||||
*
|
||||
* export class <Feature>LOADED implements Action {
|
||||
* readonly type = <Feature>ActionTypes.<Feature>LOADED;
|
||||
* constructor(public payload: any) { }
|
||||
* }
|
||||
*
|
||||
* export type <FeatureActions> = <Feature> | Load<Feature> | <Feature>Loaded
|
||||
*
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
export function updateNgrxActions(context: RequestContext): Rule {
|
||||
return (host: Tree) => {
|
||||
const clazzName = toClassName(context.featureName);
|
||||
const componentPath = buildNameToNgrxFile(context, 'actions.ts');
|
||||
const text = host.read(componentPath);
|
||||
|
||||
if (text === null) {
|
||||
throw new SchematicsException(`File ${componentPath} does not exist.`);
|
||||
}
|
||||
|
||||
const sourceText = text.toString('utf-8');
|
||||
const source = ts.createSourceFile(
|
||||
componentPath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
insert(host, componentPath, [
|
||||
...addEnumeratorValues(source, componentPath, `${clazzName}ActionTypes`, [
|
||||
{
|
||||
name: `Load${clazzName}`,
|
||||
value: `[${clazzName}] Load Data`
|
||||
},
|
||||
{
|
||||
name: `${clazzName}Loaded`,
|
||||
value: `[${clazzName}] Data Loaded`
|
||||
}
|
||||
]),
|
||||
addClass(
|
||||
source,
|
||||
componentPath,
|
||||
`Load${clazzName}`,
|
||||
stripIndents`
|
||||
export class Load${clazzName} implements Action {
|
||||
readonly type = ${clazzName}ActionTypes.Load${clazzName};
|
||||
constructor(public payload: any) { }
|
||||
}`
|
||||
),
|
||||
addClass(
|
||||
source,
|
||||
componentPath,
|
||||
`${clazzName}Loaded`,
|
||||
stripIndents`
|
||||
export class ${clazzName}Loaded implements Action {
|
||||
readonly type = ${clazzName}ActionTypes.${clazzName}Loaded;
|
||||
constructor(public payload: any) { }
|
||||
}`
|
||||
),
|
||||
addUnionTypes(source, componentPath, `${clazzName}Actions`, [
|
||||
`Load${clazzName}`,
|
||||
`${clazzName}Loaded`
|
||||
])
|
||||
]);
|
||||
};
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
import * as ts from 'typescript';
|
||||
import { SchematicsException, Rule, Tree } from '@angular-devkit/schematics';
|
||||
import { InsertChange } from '@schematics/angular/utility/change';
|
||||
import { findNodes } from '@schematics/angular/utility/ast-utils';
|
||||
import { insertImport } from '@schematics/angular/utility/route-utils';
|
||||
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
|
||||
|
||||
import { toClassName } from '../../../utils/name-utils';
|
||||
import { insert } from '../../../utils/ast-utils';
|
||||
import { RequestContext, buildNameToNgrxFile } from './request-context';
|
||||
|
||||
/**
|
||||
*
|
||||
* Desired output:
|
||||
*
|
||||
* ```
|
||||
* import { Injectable } from '@angular/core';
|
||||
* import { Effect, Actions } from '@ngrx/effects';
|
||||
* import { DataPersistence } from '@nrwl/nx';
|
||||
*
|
||||
* import { <Feature>, <Feature>State } from './<feature>.reducer';
|
||||
* import { Load<Feature>, <Feature>Loaded, <Feature>ActionTypes } from './<feature>.actions';
|
||||
*
|
||||
* @Injectable()
|
||||
* export class <Feature>Effects {
|
||||
* @Effect() load<Feature>$ = this.dataPersistence.fetch(<Feature>ActionTypes.Load<Feature>, {
|
||||
* run: (action: Load<Feature>, state: <Feature>State) => {
|
||||
* return new <Feature>Loaded({});
|
||||
* },
|
||||
*
|
||||
* onError: (action: Load<Feature>, error) => {
|
||||
* console.error('Error', error);
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* constructor(
|
||||
* private actions: Actions,
|
||||
* private dataPersistence: DataPersistence<<Feature>State>) { }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export function updateNgrxEffects(context: RequestContext): Rule {
|
||||
return (host: Tree) => {
|
||||
const clazzName = toClassName(context.featureName);
|
||||
const componentPath = buildNameToNgrxFile(context, 'effects.ts');
|
||||
const featureReducer = `./${context.featureName}.reducer`;
|
||||
const text = host.read(componentPath);
|
||||
|
||||
if (text === null) {
|
||||
throw new SchematicsException(`File ${componentPath} does not exist.`);
|
||||
}
|
||||
|
||||
const modulePath = context.options.module;
|
||||
const sourceText = text.toString('utf-8');
|
||||
const source = ts.createSourceFile(
|
||||
componentPath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
const updateConstructor = () => {
|
||||
const astConstructor = findNodes(source, ts.SyntaxKind.Constructor)[0];
|
||||
const lastParameter = findNodes(
|
||||
astConstructor,
|
||||
ts.SyntaxKind.Parameter
|
||||
).pop();
|
||||
|
||||
return new InsertChange(
|
||||
componentPath,
|
||||
lastParameter.end,
|
||||
stripIndents`, private dataPersistence: DataPersistence<${clazzName}State>`
|
||||
);
|
||||
};
|
||||
const addEffect$ = () => {
|
||||
const toInsert = `\n
|
||||
@Effect()
|
||||
load${clazzName}$ = this.dataPersistence.fetch(${clazzName}ActionTypes.Load${clazzName}, {
|
||||
run: (action: Load${clazzName}, state: ${clazzName}State) => {
|
||||
return new ${clazzName}Loaded(state);
|
||||
},
|
||||
|
||||
onError: (action: Load${clazzName}, error) => {
|
||||
console.error('Error', error);
|
||||
}
|
||||
});`;
|
||||
const astConstructor = findNodes(source, ts.SyntaxKind.Constructor)[0];
|
||||
return new InsertChange(componentPath, astConstructor.pos, toInsert);
|
||||
};
|
||||
|
||||
const actionsFile = `./${context.featureName}.actions`;
|
||||
const actionImports = `Load${clazzName}, ${clazzName}Loaded`;
|
||||
|
||||
insert(host, componentPath, [
|
||||
insertImport(source, modulePath, actionImports, actionsFile),
|
||||
insertImport(source, modulePath, `${clazzName}State`, featureReducer),
|
||||
insertImport(source, modulePath, 'DataPersistence', `@nrwl/nx`),
|
||||
updateConstructor(),
|
||||
addEffect$()
|
||||
]);
|
||||
};
|
||||
}
|
||||
@ -1,186 +0,0 @@
|
||||
import * as ts from 'typescript';
|
||||
import { SchematicsException, Rule, Tree } from '@angular-devkit/schematics';
|
||||
import {
|
||||
Change,
|
||||
ReplaceChange,
|
||||
InsertChange
|
||||
} from '@schematics/angular/utility/change';
|
||||
import {
|
||||
findNodes,
|
||||
insertAfterLastOccurrence
|
||||
} from '@schematics/angular/utility/ast-utils';
|
||||
import { insertImport } from '@schematics/angular/utility/route-utils';
|
||||
import { toClassName, toPropertyName } from '../../../utils/name-utils';
|
||||
import { insert, findNodesOfType } from '../../../utils/ast-utils';
|
||||
import { RequestContext, buildNameToNgrxFile } from './request-context';
|
||||
|
||||
/**
|
||||
* Update ngrx-generated Reducer to confirm to DataLoaded action to <featureName>.reducer.ts
|
||||
*
|
||||
* Desired output:
|
||||
*
|
||||
* ```
|
||||
* import { <Feature>Actions, <Feature>ActionTypes } from './<feature>.actions';
|
||||
*
|
||||
* export interface <Feature>State {
|
||||
* }
|
||||
*
|
||||
* export const initialState: <Feature>State = {
|
||||
* };
|
||||
*
|
||||
* export function <feature>Reducer(
|
||||
* state : <Feature>State = initialState,
|
||||
* action: <Feature>Actions ) : <Feature>State
|
||||
* {
|
||||
* switch (action.type) {
|
||||
* case <Feature>ActionTypes.<Feature>Loaded: {
|
||||
* return { ...state, ...action.payload };
|
||||
* }
|
||||
* default: {
|
||||
* return state;
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
*
|
||||
*/
|
||||
export function updateNgrxReducers(context: RequestContext): Rule {
|
||||
return (host: Tree) => {
|
||||
const clazzName = toClassName(context.featureName);
|
||||
const propertyName = toPropertyName(context.featureName);
|
||||
const componentPath = buildNameToNgrxFile(context, 'reducer.ts');
|
||||
const text = host.read(componentPath);
|
||||
|
||||
if (text === null) {
|
||||
throw new SchematicsException(`File ${componentPath} does not exist.`);
|
||||
}
|
||||
|
||||
const modulePath = context.options.module;
|
||||
const sourceText = text.toString('utf-8');
|
||||
const source = ts.createSourceFile(
|
||||
componentPath,
|
||||
sourceText,
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
const renameStateInterface = () => {
|
||||
const name = findNodesOfType(
|
||||
source,
|
||||
ts.SyntaxKind.InterfaceDeclaration,
|
||||
(it: ts.InterfaceDeclaration) => it.name.getText() === 'State',
|
||||
(it: ts.InterfaceDeclaration) => it.name,
|
||||
true
|
||||
);
|
||||
return new ReplaceChange(
|
||||
componentPath,
|
||||
name.pos,
|
||||
'State',
|
||||
`${clazzName}Data`
|
||||
);
|
||||
};
|
||||
const addInterfaceComments = () => {
|
||||
const node = findNodes(source, ts.SyntaxKind.InterfaceDeclaration, 1)[0];
|
||||
const toAdd = `
|
||||
/**
|
||||
* Interface for the '${clazzName}' data used in
|
||||
* - ${clazzName}State, and
|
||||
* - ${propertyName}Reducer
|
||||
*/`;
|
||||
return new InsertChange(componentPath, node.pos + 1, `\n ${toAdd}`);
|
||||
};
|
||||
const addFeatureState = () => {
|
||||
const node = findNodes(source, ts.SyntaxKind.VariableStatement, 1)[0];
|
||||
const toAdd = `
|
||||
/**
|
||||
* Interface to the part of the Store containing ${clazzName}State
|
||||
* and other information related to ${clazzName}Data.
|
||||
*/
|
||||
export interface ${clazzName}State {
|
||||
readonly ${propertyName}: ${clazzName}Data;
|
||||
}`;
|
||||
return new InsertChange(componentPath, node.pos, `\n${toAdd}`);
|
||||
};
|
||||
const renameInitialState = () => {
|
||||
const getIdentifier = node => node.typeName;
|
||||
const target = findNodes(source, ts.SyntaxKind.VariableStatement, 1);
|
||||
const name = findNodesOfType(
|
||||
target[0],
|
||||
ts.SyntaxKind.TypeReference,
|
||||
it => {
|
||||
return getIdentifier(it).getText() === 'State';
|
||||
},
|
||||
it => getIdentifier(it),
|
||||
true
|
||||
);
|
||||
return new ReplaceChange(
|
||||
componentPath,
|
||||
name.pos,
|
||||
'State',
|
||||
`${clazzName}Data`
|
||||
);
|
||||
};
|
||||
const updateReducerFn = () => {
|
||||
let actions: Change[] = [];
|
||||
findNodes(source, ts.SyntaxKind.FunctionDeclaration)
|
||||
.filter((it: ts.FunctionDeclaration) => it.name.getText() === 'reducer')
|
||||
.map((it: ts.FunctionDeclaration) => {
|
||||
const fnName: ts.Identifier = it.name;
|
||||
const typeName = findNodes(it, ts.SyntaxKind.Identifier).reduce(
|
||||
(result: ts.Identifier, it: ts.Identifier): ts.Identifier => {
|
||||
return !!result
|
||||
? result
|
||||
: it.getText() === 'State' ? it : undefined;
|
||||
},
|
||||
undefined
|
||||
);
|
||||
|
||||
actions = [
|
||||
new ReplaceChange(
|
||||
componentPath,
|
||||
fnName.pos,
|
||||
fnName.getText(),
|
||||
`${propertyName}Reducer`
|
||||
),
|
||||
new ReplaceChange(
|
||||
componentPath,
|
||||
typeName.pos,
|
||||
typeName.getText(),
|
||||
`${clazzName}Data`
|
||||
)
|
||||
];
|
||||
});
|
||||
|
||||
return actions;
|
||||
};
|
||||
const updateSwitchStatement = () => {
|
||||
const toInsert = `
|
||||
|
||||
case ${clazzName}ActionTypes.${clazzName}Loaded: {
|
||||
return { ...state, ...action.payload };
|
||||
}`;
|
||||
return insertAfterLastOccurrence(
|
||||
findNodes(source, ts.SyntaxKind.SwitchStatement),
|
||||
toInsert,
|
||||
componentPath,
|
||||
0,
|
||||
ts.SyntaxKind.CaseClause
|
||||
);
|
||||
};
|
||||
|
||||
insert(host, componentPath, [
|
||||
addInterfaceComments(),
|
||||
addFeatureState(),
|
||||
renameStateInterface(),
|
||||
renameInitialState(),
|
||||
insertImport(
|
||||
source,
|
||||
modulePath,
|
||||
`${clazzName}Actions`,
|
||||
`./${context.featureName}.actions`
|
||||
),
|
||||
...updateReducerFn(),
|
||||
updateSwitchStatement()
|
||||
]);
|
||||
};
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
export interface Schema {
|
||||
name: string;
|
||||
onlyEmptyRoot: boolean;
|
||||
root: boolean;
|
||||
skipFormat: boolean;
|
||||
onlyAddFiles: boolean;
|
||||
module: string;
|
||||
skipPackageJson: boolean;
|
||||
directory: string;
|
||||
root: boolean;
|
||||
onlyEmptyRoot: boolean;
|
||||
onlyAddFiles: boolean;
|
||||
skipFormat: boolean;
|
||||
skipPackageJson: boolean;
|
||||
}
|
||||
|
||||
@ -6,16 +6,29 @@
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name of the ngrx feature (e.g., Products, User, etc.).",
|
||||
"description": "Name of the NgRx feature (e.g., Products, Users, etc.). Recommended to use plural form for name.",
|
||||
"$default": {
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
}
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"default": "+state",
|
||||
"description":
|
||||
"Override the name of the folder used to contain/group the NgRx files: contains actions, effects, reducers. selectors. (e.g., +state)"
|
||||
},
|
||||
"module": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description":
|
||||
"Path to ngModule; host directory will contain the new '+state' directory (e.g., src/libs/mylib/mylib.module.ts)."
|
||||
"Path to ngModule; host directory will contain the new '+state' directory (e.g., libs/comments/src/lib/comments-state.module.ts)."
|
||||
},
|
||||
"root": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description":
|
||||
"Add StoreModule.forRoot and EffectsModule.forRoot() instead of forFeature (e.g., --root)."
|
||||
},
|
||||
"onlyAddFiles": {
|
||||
"type": "boolean",
|
||||
@ -23,18 +36,6 @@
|
||||
"description":
|
||||
"Only add new NgRx files, without changing the module file (e.g., --onlyAddFiles)."
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"default": "+state",
|
||||
"description":
|
||||
"The directory name for the ngrx files: contains actions, effects, reducers. (e.g., +state)"
|
||||
},
|
||||
"root": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description":
|
||||
"Add StoreModule.forRoot and EffectsModule.forRoot instead of forFeature (e.g., --root)."
|
||||
},
|
||||
"onlyEmptyRoot": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
@ -50,7 +51,7 @@
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description":
|
||||
"Do not add ngrx dependencies to package.json (e.g., --skipPackageJson)"
|
||||
"Do not add NgRx dependencies to package.json (e.g., --skipPackageJson)"
|
||||
}
|
||||
},
|
||||
"required": ["module"]
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
export const angularCliVersion = '6.0.1';
|
||||
export const angularVersion = '6.0.1';
|
||||
export const angularJsVersion = '1.6.6';
|
||||
export const ngrxVersion = '5.2.0';
|
||||
export const routerStoreVersion = '5.2.0';
|
||||
export const ngrxStoreFreezeVersion = '0.2.2';
|
||||
export const ngrxVersion = '6.0.1';
|
||||
export const routerStoreVersion = '6.0.1';
|
||||
export const ngrxStoreFreezeVersion = '0.2.4';
|
||||
export const nxVersion = '*';
|
||||
export const schematicsVersion = '*';
|
||||
export const angularCliSchema =
|
||||
'./node_modules/@nrwl/schematics/src/schema.json';
|
||||
export const latestMigration = '20180507-create-nx-json';
|
||||
export const prettierVersion = '1.10.2';
|
||||
export const prettierVersion = '1.13.7';
|
||||
export const typescriptVersion = '2.7.2';
|
||||
export const rxjsVersion = '6.0.0';
|
||||
export const jasmineMarblesVersion = '0.3.1';
|
||||
export const ngrxSchematicsVersion = '5.2.0';
|
||||
export const ngrxSchematicsVersion = '6.0.1';
|
||||
|
||||
export const libVersions = {
|
||||
angularVersion,
|
||||
|
||||
@ -26,6 +26,28 @@ import { toFileName } from './name-utils';
|
||||
import { serializeJson } from './fileutils';
|
||||
import * as stripJsonComments from 'strip-json-comments';
|
||||
|
||||
export function addReexport(
|
||||
source: ts.SourceFile,
|
||||
modulePath: string,
|
||||
reexportedFileName: string,
|
||||
token: string
|
||||
): Change[] {
|
||||
const allExports = findNodes(source, ts.SyntaxKind.ExportDeclaration);
|
||||
if (allExports.length > 0) {
|
||||
const m = allExports.filter(
|
||||
(e: ts.ExportDeclaration) =>
|
||||
e.moduleSpecifier.getText(source).indexOf(reexportedFileName) > -1
|
||||
);
|
||||
if (m.length > 0) {
|
||||
const mm: ts.ExportDeclaration = <any>m[0];
|
||||
return [
|
||||
new InsertChange(modulePath, mm.exportClause.end - 1, `, ${token} `)
|
||||
];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// This should be moved to @schematics/angular once it allows to pass custom expressions as providers
|
||||
function _addSymbolToNgModuleMetadata(
|
||||
source: ts.SourceFile,
|
||||
@ -317,28 +339,6 @@ export function addImportToTestBed(
|
||||
}
|
||||
}
|
||||
|
||||
export function addReexport(
|
||||
source: ts.SourceFile,
|
||||
modulePath: string,
|
||||
reexportedFileName: string,
|
||||
token: string
|
||||
): Change[] {
|
||||
const allExports = findNodes(source, ts.SyntaxKind.ExportDeclaration);
|
||||
if (allExports.length > 0) {
|
||||
const m = allExports.filter(
|
||||
(e: ts.ExportDeclaration) =>
|
||||
e.moduleSpecifier.getText(source).indexOf(reexportedFileName) > -1
|
||||
);
|
||||
if (m.length > 0) {
|
||||
const mm: ts.ExportDeclaration = <any>m[0];
|
||||
return [
|
||||
new InsertChange(modulePath, mm.exportClause.end - 1, `, ${token} `)
|
||||
];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getBootstrapComponent(
|
||||
source: ts.SourceFile,
|
||||
moduleClassName: string
|
||||
@ -698,6 +698,12 @@ export function addClass(
|
||||
return new NoopChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* e.g
|
||||
* ```ts
|
||||
* export type <Feature>Actions = <Feature> | Load<Feature>s | <Feature>sLoaded | <Feature>sLoadError;
|
||||
* ```
|
||||
*/
|
||||
export function addUnionTypes(
|
||||
source: ts.SourceFile,
|
||||
modulePath: string,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { statSync } from 'fs';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@ -75,6 +76,14 @@ function directoryExists(name) {
|
||||
}
|
||||
}
|
||||
|
||||
export function fileExists(filePath: string): boolean {
|
||||
try {
|
||||
return statSync(filePath).isFile();
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function createDirectory(directoryPath: string) {
|
||||
const parentPath = path.resolve(directoryPath, '..');
|
||||
if (!directoryExists(parentPath)) {
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* Build dictionary of names:
|
||||
*/
|
||||
export function names(name: string): any {
|
||||
return {
|
||||
name,
|
||||
@ -9,10 +12,16 @@ export function names(name: string): any {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* hypenated to UpperCamelCase
|
||||
*/
|
||||
export function toClassName(str: string): string {
|
||||
return toCapitalCase(toPropertyName(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hypenated to lowerCamelCase
|
||||
*/
|
||||
export function toPropertyName(s: string): string {
|
||||
return s
|
||||
.replace(
|
||||
@ -22,6 +31,9 @@ export function toPropertyName(s: string): string {
|
||||
.replace(/^([A-Z])/, m => m.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Upper camelCase to lowercase, hypenated
|
||||
*/
|
||||
export function toFileName(s: string): string {
|
||||
return s
|
||||
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
||||
|
||||
@ -17,36 +17,42 @@ describe('updateKarmaConf', () => {
|
||||
);
|
||||
tree = createEmptyWorkspace(Tree.empty());
|
||||
tree.create('apps/projectName/karma.conf.js', '');
|
||||
schematicRunner
|
||||
.callRule(
|
||||
updateJsonInTree('/angular.json', angularJson => {
|
||||
angularJson.projects.projectName = {
|
||||
root: 'apps/projectName',
|
||||
architect: {
|
||||
test: {
|
||||
options: {
|
||||
karmaConfig: 'apps/projectName/karma.conf.js'
|
||||
}
|
||||
const process$ = schematicRunner.callRule(
|
||||
updateJsonInTree('/angular.json', angularJson => {
|
||||
angularJson.projects.projectName = {
|
||||
root: 'apps/projectName',
|
||||
architect: {
|
||||
test: {
|
||||
options: {
|
||||
karmaConfig: 'apps/projectName/karma.conf.js'
|
||||
}
|
||||
}
|
||||
};
|
||||
return angularJson;
|
||||
}),
|
||||
tree
|
||||
)
|
||||
.subscribe(done);
|
||||
}
|
||||
};
|
||||
return angularJson;
|
||||
}),
|
||||
tree
|
||||
);
|
||||
|
||||
process$.subscribe(
|
||||
_ => done(),
|
||||
error => {
|
||||
console.log(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should overwrite the karma.conf.js', done => {
|
||||
schematicRunner
|
||||
.callRule(updateKarmaConf({ projectName: 'projectName' }), tree)
|
||||
.subscribe(result => {
|
||||
const contents = result
|
||||
.read('apps/projectName/karma.conf.js')
|
||||
.toString();
|
||||
const replaceKarmaConf = updateKarmaConf({ projectName: 'projectName' });
|
||||
schematicRunner.callRule(replaceKarmaConf, tree).subscribe(result => {
|
||||
const contents = result.read('apps/projectName/karma.conf.js');
|
||||
expect(contents.toString()).toEqual(UPDATED_KARMA_CONF);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
expect(contents).toEqual(
|
||||
`// Karma configuration file, see link for more information
|
||||
const UPDATED_KARMA_CONF = `// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
const { join } = require('path');
|
||||
@ -62,9 +68,4 @@ module.exports = function(config) {
|
||||
}
|
||||
});
|
||||
};
|
||||
`
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
`;
|
||||
|
||||
@ -1,12 +1,6 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { Rule, Tree, SchematicContext } from '@angular-devkit/schematics';
|
||||
import { offsetFromRoot } from '../common';
|
||||
import {
|
||||
createOrUpdate,
|
||||
getProjectConfig,
|
||||
updateJsonInTree
|
||||
} from '../ast-utils';
|
||||
import { createOrUpdate, getProjectConfig } from '../ast-utils';
|
||||
|
||||
/**
|
||||
* This returns a Rule which changes the default Angular CLI Generated karma.conf.js
|
||||
|
||||
@ -1,15 +1,25 @@
|
||||
import { Tree } from '@angular-devkit/schematics';
|
||||
import { names } from './name-utils';
|
||||
|
||||
export interface AppConfig {
|
||||
appName: string; // name of app or lib
|
||||
appName: string; // name of app
|
||||
appModule: string; // app/app.module.ts in the above sourceDir
|
||||
}
|
||||
export interface LibConfig {
|
||||
name: string;
|
||||
module: string;
|
||||
barrel: string;
|
||||
}
|
||||
|
||||
var appConfig: AppConfig; // configure built in createApp()
|
||||
var libConfig: LibConfig;
|
||||
|
||||
export function getAppConfig(): AppConfig {
|
||||
return appConfig;
|
||||
}
|
||||
export function getLibConfig(): LibConfig {
|
||||
return libConfig;
|
||||
}
|
||||
|
||||
export function createEmptyWorkspace(tree: Tree): Tree {
|
||||
tree.create(
|
||||
@ -116,3 +126,35 @@ export function createApp(
|
||||
);
|
||||
return tree;
|
||||
}
|
||||
|
||||
export function createLib(tree: Tree, libName: string): Tree {
|
||||
const { name, className, fileName, propertyName } = names(libName);
|
||||
|
||||
libConfig = {
|
||||
name,
|
||||
module: `/libs/${propertyName}/src/lib/${fileName}.module.ts`,
|
||||
barrel: `/libs/${propertyName}/src/index.ts`
|
||||
};
|
||||
|
||||
tree.create(
|
||||
libConfig.module,
|
||||
`
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
export class ${className}Module { }
|
||||
`
|
||||
);
|
||||
tree.create(
|
||||
libConfig.barrel,
|
||||
`
|
||||
export * from './lib/${fileName}.module';
|
||||
`
|
||||
);
|
||||
return tree;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user