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:
Thomas Burleson 2018-05-11 07:52:23 -05:00 committed by Victor Savkin
parent a55412caae
commit 04e99b06ae
41 changed files with 2327 additions and 2245 deletions

3
.gitignore vendored
View File

@ -7,4 +7,5 @@ test
.DS_Store
tmp
*.log
.ng_pkg_build
.ng_pkg_build
jest.debug.config.js

View File

@ -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'

View File

@ -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
);

View File

@ -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(

View File

@ -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",

View File

@ -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",

View File

@ -1,7 +1,7 @@
{
"name": "nx-bazel",
"version": "0.1",
"extends": "@ngrx/schematics",
"extends": "@schematics/angular",
"schematics": {
"application": {
"factory": "./collection/application",

View File

@ -23,6 +23,5 @@
"node_modules/@angular/tsc-wrapped/**",
"node_modules/@nrwl/bazel/**",
"node_modules/@nrwl/schematics/**",
"node_modules/@ngrx/schematics/**",
]
}
}

View File

@ -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 %>",

View File

@ -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',

View File

@ -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"
}
}

View File

@ -1,7 +1,7 @@
{
"name": "nx",
"version": "0.1",
"extends": "@ngrx/schematics",
"extends": "@schematics/angular",
"schematics": {
"ng-add": {
"factory": "./collection/ng-add",

View File

@ -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);
});
});

View File

@ -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;
}

View File

@ -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",

View File

@ -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
}

View File

@ -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([])})
);
});
});
});

View File

@ -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>) { }
}

View File

@ -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);
});
});
});

View File

@ -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;
}

View File

@ -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);
});
});
});

View File

@ -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 %>
};

View File

@ -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)
};
}

View File

@ -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 &lt;Feature&gt; notation used be below indicates a placeholder for the actual *feature* name.
* [&lt;feature&gt;.actions.ts](#featureactionsts)
* [&lt;feature&gt;.reducer.ts](#featurereducerts)
* [&lt;feature&gt;.effects.ts](#featureeffectsts)
* [&lt;feature&gt;.selectors.ts](#featureselectorsts)
* [&lt;feature&gt;.facade.ts](#featurefacadests)
* [../app.module.ts](#appmodulets)
#### &lt;feature&gt;.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;
```
#### &lt;feature&gt;.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;
}
}
}
```
#### &lt;feature&gt;.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 {
}
```

View File

@ -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

View File

@ -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;
};
}

View File

@ -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) {

View File

@ -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';

View File

@ -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`
])
]);
};
}

View File

@ -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$()
]);
};
}

View File

@ -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()
]);
};
}

View File

@ -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;
}

View File

@ -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"]

View File

@ -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,

View File

@ -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,

View File

@ -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)) {

View File

@ -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')

View File

@ -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();
});
});
});
`;

View File

@ -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

View File

@ -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;
}

2781
yarn.lock

File diff suppressed because it is too large Load Diff