Skip to content

Commit 78817c7

Browse files
timdeschryverbrandonroberts
authored andcommitted
feat(schematics): add selector schematics (#2160)
Closes #2140
1 parent 8110c32 commit 78817c7

File tree

11 files changed

+456
-4
lines changed

11 files changed

+456
-4
lines changed

modules/schematics/collection.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@
5757
"description": "Adds initial setup for state management"
5858
},
5959

60+
"selector": {
61+
"aliases": ["se"],
62+
"factory": "./src/selector",
63+
"schema": "./src/selector/schema.json",
64+
"description": "Add selectors"
65+
},
66+
6067
"ng-add": {
6168
"aliases": ["init"],
6269
"factory": "./src/ng-add",

modules/schematics/src/feature/index.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ describe('Feature Schematic', () => {
5252
expect(
5353
files.indexOf(`${projectPath}/src/app/foo.effects.spec.ts`)
5454
).toBeGreaterThanOrEqual(0);
55+
expect(
56+
files.indexOf(`${projectPath}/src/app/foo.selectors.ts`)
57+
).toBeGreaterThanOrEqual(0);
58+
expect(
59+
files.indexOf(`${projectPath}/src/app/foo.selectors.spec.ts`)
60+
).toBeGreaterThanOrEqual(0);
5561
});
5662

5763
it('should create all files of a feature to specified project if provided', () => {
@@ -82,6 +88,12 @@ describe('Feature Schematic', () => {
8288
expect(
8389
files.indexOf(`${specifiedProjectPath}/src/lib/foo.effects.spec.ts`)
8490
).toBeGreaterThanOrEqual(0);
91+
expect(
92+
files.indexOf(`${specifiedProjectPath}/src/lib/foo.selectors.ts`)
93+
).toBeGreaterThanOrEqual(0);
94+
expect(
95+
files.indexOf(`${specifiedProjectPath}/src/lib/foo.selectors.spec.ts`)
96+
).toBeGreaterThanOrEqual(0);
8597
});
8698

8799
it('should create all files of a feature within grouped folders if group is set', () => {
@@ -104,6 +116,12 @@ describe('Feature Schematic', () => {
104116
expect(
105117
files.indexOf(`${projectPath}/src/app/effects/foo.effects.spec.ts`)
106118
).toBeGreaterThanOrEqual(0);
119+
expect(
120+
files.indexOf(`${projectPath}/src/app/selectors/foo.selectors.ts`)
121+
).toBeGreaterThanOrEqual(0);
122+
expect(
123+
files.indexOf(`${projectPath}/src/app/selectors/foo.selectors.spec.ts`)
124+
).toBeGreaterThanOrEqual(0);
107125
});
108126

109127
it('should respect the path provided for the feature name', () => {

modules/schematics/src/feature/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ export default function(options: FeatureOptions): Rule {
4545
api: options.api,
4646
creators: options.creators,
4747
}),
48+
schematic('selector', {
49+
flat: options.flat,
50+
group: options.group,
51+
name: options.name,
52+
path: options.path,
53+
project: options.project,
54+
spec: options.spec,
55+
feature: true,
56+
}),
4857
])(host, context);
4958
};
5059
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<% if(feature) { %>import * as from<%= classify(name) %> from '<%= reducerPath %>';
2+
import { select<%= classify(name) %>State } from './<%= dasherize(name) %>.selectors';<% } %>
3+
4+
describe('<%= classify(name) %> Selectors', () => {
5+
it('should select the feature state', () => {
6+
<% if(feature) { %>const result = select<%= classify(name) %>State({
7+
[from<%= classify(name) %>.<%= camelize(name) %>FeatureKey]: {}
8+
});
9+
10+
expect(result).toEqual({});<% } %>
11+
});
12+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createFeatureSelector, createSelector } from '@ngrx/store';
2+
<% if(feature) { %>import * as from<%= classify(name) %> from '<%= reducerPath %>';
3+
4+
export const select<%= classify(name) %>State = createFeatureSelector<from<%= classify(name) %>.State>(
5+
from<%= classify(name) %>.<%= camelize(name) %>FeatureKey
6+
);<% } %>
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { tags } from '@angular-devkit/core';
2+
import {
3+
SchematicTestRunner,
4+
UnitTestTree,
5+
} from '@angular-devkit/schematics/testing';
6+
import * as path from 'path';
7+
import { Schema as SelectorOptions } from './schema';
8+
import {
9+
getTestProjectPath,
10+
createWorkspace,
11+
} from '../../../schematics-core/testing';
12+
13+
describe('Selector Schematic', () => {
14+
const schematicRunner = new SchematicTestRunner(
15+
'@ngrx/schematics',
16+
path.join(__dirname, '../../collection.json')
17+
);
18+
const defaultOptions: SelectorOptions = {
19+
name: 'foo',
20+
project: 'bar',
21+
spec: true,
22+
};
23+
24+
const projectPath = getTestProjectPath();
25+
26+
let appTree: UnitTestTree;
27+
28+
beforeEach(async () => {
29+
appTree = await createWorkspace(schematicRunner, appTree);
30+
});
31+
32+
it('should create selector files', () => {
33+
const tree = schematicRunner.runSchematic(
34+
'selector',
35+
defaultOptions,
36+
appTree
37+
);
38+
39+
const selectorsContent = tree.readContent(
40+
`${projectPath}/src/app/foo.selectors.ts`
41+
);
42+
const specContent = tree.readContent(
43+
`${projectPath}/src/app/foo.selectors.spec.ts`
44+
);
45+
46+
expect(cleanString(selectorsContent)).toBe(
47+
cleanString(
48+
tags.stripIndent`import { createFeatureSelector, createSelector } from '@ngrx/store';`
49+
)
50+
);
51+
52+
expect(cleanString(specContent)).toBe(
53+
cleanString(tags.stripIndent`
54+
describe('Foo Selectors', () => {
55+
it('should select the feature state', () => {
56+
**
57+
});
58+
});`)
59+
);
60+
});
61+
62+
it('should not create a spec file if spec is false', () => {
63+
const options = {
64+
...defaultOptions,
65+
spec: false,
66+
};
67+
const tree = schematicRunner.runSchematic('selector', options, appTree);
68+
69+
expect(
70+
tree.files.includes(`${projectPath}/src/app/foo.selectors.spec.ts`)
71+
).toBeFalsy();
72+
});
73+
74+
it('should group selectors if group is true', () => {
75+
const options = {
76+
...defaultOptions,
77+
group: true,
78+
};
79+
const tree = schematicRunner.runSchematic('selector', options, appTree);
80+
81+
expect(
82+
tree.files.includes(`${projectPath}/src/app/selectors/foo.selectors.ts`)
83+
).toBeTruthy();
84+
expect(
85+
tree.files.includes(
86+
`${projectPath}/src/app/selectors/foo.selectors.spec.ts`
87+
)
88+
).toBeTruthy();
89+
});
90+
91+
it('should not flatten selectors if flat is false', () => {
92+
const options = {
93+
...defaultOptions,
94+
flat: false,
95+
};
96+
const tree = schematicRunner.runSchematic('selector', options, appTree);
97+
98+
expect(
99+
tree.files.includes(`${projectPath}/src/app/foo/foo.selectors.ts`)
100+
).toBeTruthy();
101+
expect(
102+
tree.files.includes(`${projectPath}/src/app/foo/foo.selectors.spec.ts`)
103+
).toBeTruthy();
104+
});
105+
106+
describe('With feature flag', () => {
107+
it('should create a selector', () => {
108+
const options = {
109+
...defaultOptions,
110+
feature: true,
111+
};
112+
113+
const tree = schematicRunner.runSchematic('selector', options, appTree);
114+
const selectorsContent = tree.readContent(
115+
`${projectPath}/src/app/foo.selectors.ts`
116+
);
117+
const specContent = tree.readContent(
118+
`${projectPath}/src/app/foo.selectors.spec.ts`
119+
);
120+
121+
expect(cleanString(selectorsContent)).toBe(
122+
cleanString(tags.stripIndent`
123+
import { createFeatureSelector, createSelector } from '@ngrx/store';
124+
import * as fromFoo from './foo.reducer';
125+
126+
export const selectFooState = createFeatureSelector<fromFoo.State>(
127+
fromFoo.fooFeatureKey
128+
);
129+
`)
130+
);
131+
132+
expect(cleanString(specContent)).toBe(
133+
cleanString(tags.stripIndent`
134+
import * as fromFoo from './foo.reducer';
135+
import { selectFooState } from './foo.selectors';
136+
137+
describe('Foo Selectors', () => {
138+
it('should select the feature state', () => {
139+
const result = selectFooState({
140+
[fromFoo.fooFeatureKey]: {}
141+
});
142+
143+
expect(result).toEqual({});
144+
});
145+
});
146+
`)
147+
);
148+
});
149+
150+
it('should group and nest the selectors within a feature', () => {
151+
const options = {
152+
...defaultOptions,
153+
feature: true,
154+
group: true,
155+
flat: false,
156+
};
157+
158+
const tree = schematicRunner.runSchematic('selector', options, appTree);
159+
const selectorPath = `${projectPath}/src/app/selectors/foo/foo.selectors.ts`;
160+
const specPath = `${projectPath}/src/app/selectors/foo/foo.selectors.spec.ts`;
161+
162+
expect(tree.files.includes(selectorPath)).toBeTruthy();
163+
expect(tree.files.includes(specPath)).toBeTruthy();
164+
165+
const selectorContent = tree.readContent(selectorPath);
166+
expect(selectorContent).toMatch(
167+
/import\ \* as fromFoo from\ \'\.\.\/\.\.\/reducers\/foo\/foo\.reducer';/
168+
);
169+
170+
const specContent = tree.readContent(specPath);
171+
expect(specContent).toMatch(
172+
/import\ \* as fromFoo from\ \'\.\.\/\.\.\/reducers\/foo\/foo\.reducer';/
173+
);
174+
expect(specContent).toMatch(
175+
/import\ \{ selectFooState \} from\ \'\.\/foo\.selectors';/
176+
);
177+
});
178+
});
179+
180+
function cleanString(value: string) {
181+
// ** to mark an empty line (VSCode removes whitespace lines)
182+
return value
183+
.replace(/\r\n/g, '\n')
184+
.replace(/\*\*/g, '')
185+
.trim();
186+
}
187+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {
2+
Rule,
3+
SchematicContext,
4+
Tree,
5+
apply,
6+
applyTemplates,
7+
branchAndMerge,
8+
chain,
9+
filter,
10+
mergeWith,
11+
move,
12+
noop,
13+
url,
14+
} from '@angular-devkit/schematics';
15+
import {
16+
getProjectPath,
17+
parseName,
18+
stringUtils,
19+
} from '@ngrx/schematics/schematics-core';
20+
import { Schema as SelectorOptions } from './schema';
21+
22+
export default function(options: SelectorOptions): Rule {
23+
return (host: Tree, context: SchematicContext) => {
24+
options.path = getProjectPath(host, options);
25+
26+
const parsedPath = parseName(options.path, options.name || '');
27+
options.name = parsedPath.name;
28+
options.path = parsedPath.path;
29+
30+
const templateSource = apply(url('./files'), [
31+
options.spec
32+
? noop()
33+
: filter(path => !path.endsWith('.spec.ts.template')),
34+
applyTemplates({
35+
...stringUtils,
36+
'if-flat': (s: string) =>
37+
stringUtils.group(
38+
options.flat ? '' : s,
39+
options.group ? 'selectors' : ''
40+
),
41+
reducerPath: `${relativePath(options)}${stringUtils.dasherize(
42+
options.name
43+
)}.reducer`,
44+
...(options as object),
45+
} as any),
46+
move(parsedPath.path),
47+
]);
48+
49+
return chain([branchAndMerge(chain([mergeWith(templateSource)]))])(
50+
host,
51+
context
52+
);
53+
};
54+
}
55+
56+
function relativePath(options: SelectorOptions) {
57+
if (options.feature) {
58+
return stringUtils.featurePath(
59+
options.group,
60+
options.flat,
61+
'reducers',
62+
stringUtils.dasherize(options.name)
63+
);
64+
}
65+
66+
return '';
67+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"id": "SchematicsNgRxSelector",
4+
"title": "NgRx Selector Options Schema",
5+
"type": "object",
6+
"properties": {
7+
"name": {
8+
"description": "The name of the selector.",
9+
"type": "string",
10+
"$default": {
11+
"$source": "argv",
12+
"index": 0
13+
}
14+
},
15+
"path": {
16+
"type": "string",
17+
"format": "path",
18+
"description": "The path to create the selectors.",
19+
"visible": false
20+
},
21+
"project": {
22+
"type": "string",
23+
"description": "The name of the project.",
24+
"aliases": ["p"]
25+
},
26+
"spec": {
27+
"type": "boolean",
28+
"description": "Specifies if a spec file is generated.",
29+
"default": false
30+
},
31+
"flat": {
32+
"type": "boolean",
33+
"default": true,
34+
"description": "Flag to indicate if a dir is created."
35+
},
36+
"group": {
37+
"type": "boolean",
38+
"default": false,
39+
"description": "Group selector file within 'selectors' folder",
40+
"aliases": ["g"]
41+
},
42+
"feature": {
43+
"type": "boolean",
44+
"default": false,
45+
"description": "Flag to indicate if part of a feature schematic.",
46+
"visible": false
47+
}
48+
},
49+
"required": []
50+
}

0 commit comments

Comments
 (0)