Skip to content

Commit

Permalink
feat(schematics): add selector schematics (#2160)
Browse files Browse the repository at this point in the history
Closes #2140
  • Loading branch information
timdeschryver authored and brandonroberts committed Oct 17, 2019
1 parent 8110c32 commit 78817c7
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 4 deletions.
7 changes: 7 additions & 0 deletions modules/schematics/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
"description": "Adds initial setup for state management"
},

"selector": {
"aliases": ["se"],
"factory": "./src/selector",
"schema": "./src/selector/schema.json",
"description": "Add selectors"
},

"ng-add": {
"aliases": ["init"],
"factory": "./src/ng-add",
Expand Down
18 changes: 18 additions & 0 deletions modules/schematics/src/feature/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ describe('Feature Schematic', () => {
expect(
files.indexOf(`${projectPath}/src/app/foo.effects.spec.ts`)
).toBeGreaterThanOrEqual(0);
expect(
files.indexOf(`${projectPath}/src/app/foo.selectors.ts`)
).toBeGreaterThanOrEqual(0);
expect(
files.indexOf(`${projectPath}/src/app/foo.selectors.spec.ts`)
).toBeGreaterThanOrEqual(0);
});

it('should create all files of a feature to specified project if provided', () => {
Expand Down Expand Up @@ -82,6 +88,12 @@ describe('Feature Schematic', () => {
expect(
files.indexOf(`${specifiedProjectPath}/src/lib/foo.effects.spec.ts`)
).toBeGreaterThanOrEqual(0);
expect(
files.indexOf(`${specifiedProjectPath}/src/lib/foo.selectors.ts`)
).toBeGreaterThanOrEqual(0);
expect(
files.indexOf(`${specifiedProjectPath}/src/lib/foo.selectors.spec.ts`)
).toBeGreaterThanOrEqual(0);
});

it('should create all files of a feature within grouped folders if group is set', () => {
Expand All @@ -104,6 +116,12 @@ describe('Feature Schematic', () => {
expect(
files.indexOf(`${projectPath}/src/app/effects/foo.effects.spec.ts`)
).toBeGreaterThanOrEqual(0);
expect(
files.indexOf(`${projectPath}/src/app/selectors/foo.selectors.ts`)
).toBeGreaterThanOrEqual(0);
expect(
files.indexOf(`${projectPath}/src/app/selectors/foo.selectors.spec.ts`)
).toBeGreaterThanOrEqual(0);
});

it('should respect the path provided for the feature name', () => {
Expand Down
9 changes: 9 additions & 0 deletions modules/schematics/src/feature/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ export default function(options: FeatureOptions): Rule {
api: options.api,
creators: options.creators,
}),
schematic('selector', {
flat: options.flat,
group: options.group,
name: options.name,
path: options.path,
project: options.project,
spec: options.spec,
feature: true,
}),
])(host, context);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<% if(feature) { %>import * as from<%= classify(name) %> from '<%= reducerPath %>';
import { select<%= classify(name) %>State } from './<%= dasherize(name) %>.selectors';<% } %>

describe('<%= classify(name) %> Selectors', () => {
it('should select the feature state', () => {
<% if(feature) { %>const result = select<%= classify(name) %>State({
[from<%= classify(name) %>.<%= camelize(name) %>FeatureKey]: {}
});

expect(result).toEqual({});<% } %>
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createFeatureSelector, createSelector } from '@ngrx/store';
<% if(feature) { %>import * as from<%= classify(name) %> from '<%= reducerPath %>';

export const select<%= classify(name) %>State = createFeatureSelector<from<%= classify(name) %>.State>(
from<%= classify(name) %>.<%= camelize(name) %>FeatureKey
);<% } %>
187 changes: 187 additions & 0 deletions modules/schematics/src/selector/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { tags } from '@angular-devkit/core';
import {
SchematicTestRunner,
UnitTestTree,
} from '@angular-devkit/schematics/testing';
import * as path from 'path';
import { Schema as SelectorOptions } from './schema';
import {
getTestProjectPath,
createWorkspace,
} from '../../../schematics-core/testing';

describe('Selector Schematic', () => {
const schematicRunner = new SchematicTestRunner(
'@ngrx/schematics',
path.join(__dirname, '../../collection.json')
);
const defaultOptions: SelectorOptions = {
name: 'foo',
project: 'bar',
spec: true,
};

const projectPath = getTestProjectPath();

let appTree: UnitTestTree;

beforeEach(async () => {
appTree = await createWorkspace(schematicRunner, appTree);
});

it('should create selector files', () => {
const tree = schematicRunner.runSchematic(
'selector',
defaultOptions,
appTree
);

const selectorsContent = tree.readContent(
`${projectPath}/src/app/foo.selectors.ts`
);
const specContent = tree.readContent(
`${projectPath}/src/app/foo.selectors.spec.ts`
);

expect(cleanString(selectorsContent)).toBe(
cleanString(
tags.stripIndent`import { createFeatureSelector, createSelector } from '@ngrx/store';`
)
);

expect(cleanString(specContent)).toBe(
cleanString(tags.stripIndent`
describe('Foo Selectors', () => {
it('should select the feature state', () => {
**
});
});`)
);
});

it('should not create a spec file if spec is false', () => {
const options = {
...defaultOptions,
spec: false,
};
const tree = schematicRunner.runSchematic('selector', options, appTree);

expect(
tree.files.includes(`${projectPath}/src/app/foo.selectors.spec.ts`)
).toBeFalsy();
});

it('should group selectors if group is true', () => {
const options = {
...defaultOptions,
group: true,
};
const tree = schematicRunner.runSchematic('selector', options, appTree);

expect(
tree.files.includes(`${projectPath}/src/app/selectors/foo.selectors.ts`)
).toBeTruthy();
expect(
tree.files.includes(
`${projectPath}/src/app/selectors/foo.selectors.spec.ts`
)
).toBeTruthy();
});

it('should not flatten selectors if flat is false', () => {
const options = {
...defaultOptions,
flat: false,
};
const tree = schematicRunner.runSchematic('selector', options, appTree);

expect(
tree.files.includes(`${projectPath}/src/app/foo/foo.selectors.ts`)
).toBeTruthy();
expect(
tree.files.includes(`${projectPath}/src/app/foo/foo.selectors.spec.ts`)
).toBeTruthy();
});

describe('With feature flag', () => {
it('should create a selector', () => {
const options = {
...defaultOptions,
feature: true,
};

const tree = schematicRunner.runSchematic('selector', options, appTree);
const selectorsContent = tree.readContent(
`${projectPath}/src/app/foo.selectors.ts`
);
const specContent = tree.readContent(
`${projectPath}/src/app/foo.selectors.spec.ts`
);

expect(cleanString(selectorsContent)).toBe(
cleanString(tags.stripIndent`
import { createFeatureSelector, createSelector } from '@ngrx/store';
import * as fromFoo from './foo.reducer';
export const selectFooState = createFeatureSelector<fromFoo.State>(
fromFoo.fooFeatureKey
);
`)
);

expect(cleanString(specContent)).toBe(
cleanString(tags.stripIndent`
import * as fromFoo from './foo.reducer';
import { selectFooState } from './foo.selectors';
describe('Foo Selectors', () => {
it('should select the feature state', () => {
const result = selectFooState({
[fromFoo.fooFeatureKey]: {}
});
expect(result).toEqual({});
});
});
`)
);
});

it('should group and nest the selectors within a feature', () => {
const options = {
...defaultOptions,
feature: true,
group: true,
flat: false,
};

const tree = schematicRunner.runSchematic('selector', options, appTree);
const selectorPath = `${projectPath}/src/app/selectors/foo/foo.selectors.ts`;
const specPath = `${projectPath}/src/app/selectors/foo/foo.selectors.spec.ts`;

expect(tree.files.includes(selectorPath)).toBeTruthy();
expect(tree.files.includes(specPath)).toBeTruthy();

const selectorContent = tree.readContent(selectorPath);
expect(selectorContent).toMatch(
/import\ \* as fromFoo from\ \'\.\.\/\.\.\/reducers\/foo\/foo\.reducer';/
);

const specContent = tree.readContent(specPath);
expect(specContent).toMatch(
/import\ \* as fromFoo from\ \'\.\.\/\.\.\/reducers\/foo\/foo\.reducer';/
);
expect(specContent).toMatch(
/import\ \{ selectFooState \} from\ \'\.\/foo\.selectors';/
);
});
});

function cleanString(value: string) {
// ** to mark an empty line (VSCode removes whitespace lines)
return value
.replace(/\r\n/g, '\n')
.replace(/\*\*/g, '')
.trim();
}
});
67 changes: 67 additions & 0 deletions modules/schematics/src/selector/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
Rule,
SchematicContext,
Tree,
apply,
applyTemplates,
branchAndMerge,
chain,
filter,
mergeWith,
move,
noop,
url,
} from '@angular-devkit/schematics';
import {
getProjectPath,
parseName,
stringUtils,
} from '@ngrx/schematics/schematics-core';
import { Schema as SelectorOptions } from './schema';

export default function(options: SelectorOptions): Rule {
return (host: Tree, context: SchematicContext) => {
options.path = getProjectPath(host, options);

const parsedPath = parseName(options.path, options.name || '');
options.name = parsedPath.name;
options.path = parsedPath.path;

const templateSource = apply(url('./files'), [
options.spec
? noop()
: filter(path => !path.endsWith('.spec.ts.template')),
applyTemplates({
...stringUtils,
'if-flat': (s: string) =>
stringUtils.group(
options.flat ? '' : s,
options.group ? 'selectors' : ''
),
reducerPath: `${relativePath(options)}${stringUtils.dasherize(
options.name
)}.reducer`,
...(options as object),
} as any),
move(parsedPath.path),
]);

return chain([branchAndMerge(chain([mergeWith(templateSource)]))])(
host,
context
);
};
}

function relativePath(options: SelectorOptions) {
if (options.feature) {
return stringUtils.featurePath(
options.group,
options.flat,
'reducers',
stringUtils.dasherize(options.name)
);
}

return '';
}
50 changes: 50 additions & 0 deletions modules/schematics/src/selector/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsNgRxSelector",
"title": "NgRx Selector Options Schema",
"type": "object",
"properties": {
"name": {
"description": "The name of the selector.",
"type": "string",
"$default": {
"$source": "argv",
"index": 0
}
},
"path": {
"type": "string",
"format": "path",
"description": "The path to create the selectors.",
"visible": false
},
"project": {
"type": "string",
"description": "The name of the project.",
"aliases": ["p"]
},
"spec": {
"type": "boolean",
"description": "Specifies if a spec file is generated.",
"default": false
},
"flat": {
"type": "boolean",
"default": true,
"description": "Flag to indicate if a dir is created."
},
"group": {
"type": "boolean",
"default": false,
"description": "Group selector file within 'selectors' folder",
"aliases": ["g"]
},
"feature": {
"type": "boolean",
"default": false,
"description": "Flag to indicate if part of a feature schematic.",
"visible": false
}
},
"required": []
}
Loading

0 comments on commit 78817c7

Please sign in to comment.