Skip to content

Commit 9da4aac

Browse files
ngfelixlbrandonroberts
authored andcommitted
feat(schematics): implement router store ng-add (#1487)
Closes #1479
1 parent 7f57f11 commit 9da4aac

File tree

7 files changed

+282
-1
lines changed

7 files changed

+282
-1
lines changed

modules/router-store/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
"keywords": [
1010
"RxJS",
1111
"Angular",
12-
"Redux"
12+
"Redux",
13+
"Schematics",
14+
"Angular CLI"
1315
],
1416
"author": "NgRx",
1517
"license": "MIT",
@@ -24,6 +26,7 @@
2426
"@ngrx/store": "0.0.0-PLACEHOLDER",
2527
"rxjs": "RXJS_VERSION"
2628
},
29+
"schematics": "MODULE_SCHEMATICS_COLLECTION",
2730
"ng-update": {
2831
"packageGroup": "NG_UPDATE_PACKAGE_GROUP",
2932
"migrations": "NG_UPDATE_MIGRATIONS"

modules/router-store/schematics/BUILD

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "npm_package", "ts_library")
4+
5+
ts_library(
6+
name = "schematics",
7+
srcs = glob(
8+
[
9+
"**/*.ts",
10+
],
11+
exclude = [
12+
"**/*.spec.ts",
13+
"**/files/**/*",
14+
],
15+
),
16+
module_name = "@ngrx/router-store/schematics",
17+
deps = [
18+
"//modules/router-store/schematics-core",
19+
"@npm//@angular-devkit/schematics",
20+
"@npm//typescript",
21+
],
22+
)
23+
24+
npm_package(
25+
name = "npm_package",
26+
srcs = [
27+
":collection.json",
28+
] + glob([
29+
"**/files/**/*",
30+
"**/schema.json",
31+
]),
32+
deps = [
33+
":schematics",
34+
],
35+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"schematics": {
3+
"ng-add": {
4+
"aliases": ["init"],
5+
"factory": "./ng-add",
6+
"schema": "./ng-add/schema.json",
7+
"description": "Register @ngrx/router-store within your application"
8+
}
9+
}
10+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
SchematicTestRunner,
3+
UnitTestTree,
4+
} from '@angular-devkit/schematics/testing';
5+
import * as path from 'path';
6+
import { Schema as RouterStoreOptions } from './schema';
7+
import {
8+
getTestProjectPath,
9+
createWorkspace,
10+
} from '../../../schematics-core/testing';
11+
12+
describe('Router Store ng-add Schematic', () => {
13+
const schematicRunner = new SchematicTestRunner(
14+
'@ngrx/router-store',
15+
path.join(__dirname, '../collection.json')
16+
);
17+
const defaultOptions: RouterStoreOptions = {
18+
skipPackageJson: false,
19+
module: 'app',
20+
};
21+
22+
const projectPath = getTestProjectPath();
23+
24+
let appTree: UnitTestTree;
25+
26+
beforeEach(() => {
27+
appTree = createWorkspace(schematicRunner, appTree);
28+
});
29+
30+
it('should update package.json', () => {
31+
const options = { ...defaultOptions };
32+
33+
const tree = schematicRunner.runSchematic('ng-add', options, appTree);
34+
const packageJson = JSON.parse(tree.readContent('/package.json'));
35+
36+
expect(packageJson.dependencies['@ngrx/router-store']).toBeDefined();
37+
});
38+
39+
it('should skip package.json update', () => {
40+
const options = { ...defaultOptions, skipPackageJson: true };
41+
42+
const tree = schematicRunner.runSchematic('ng-add', options, appTree);
43+
const packageJson = JSON.parse(tree.readContent('/package.json'));
44+
45+
expect(packageJson.dependencies['@ngrx/router-store']).toBeUndefined();
46+
});
47+
48+
it('should be provided by default', () => {
49+
const options = { ...defaultOptions };
50+
51+
const tree = schematicRunner.runSchematic('ng-add', options, appTree);
52+
const content = tree.readContent(`${projectPath}/src/app/app.module.ts`);
53+
expect(content).toMatch(
54+
/import { StoreRouterConnectingModule } from '@ngrx\/router-store';/
55+
);
56+
expect(content).toMatch(/StoreRouterConnectingModule.forRoot\(\)/);
57+
});
58+
59+
it('should import into a specified module', () => {
60+
const options = { ...defaultOptions };
61+
62+
const tree = schematicRunner.runSchematic('ng-add', options, appTree);
63+
const content = tree.readContent(`${projectPath}/src/app/app.module.ts`);
64+
expect(content).toMatch(
65+
/import { StoreRouterConnectingModule } from '@ngrx\/router-store';/
66+
);
67+
});
68+
69+
it('should fail if specified module does not exist', () => {
70+
const options = { ...defaultOptions, module: '/src/app/app.moduleXXX.ts' };
71+
let thrownError: Error | null = null;
72+
try {
73+
schematicRunner.runSchematic('ng-add', options, appTree);
74+
} catch (err) {
75+
thrownError = err;
76+
}
77+
expect(thrownError).toBeDefined();
78+
});
79+
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
Rule,
3+
SchematicContext,
4+
SchematicsException,
5+
Tree,
6+
branchAndMerge,
7+
chain,
8+
noop,
9+
} from '@angular-devkit/schematics';
10+
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
11+
import * as ts from 'typescript';
12+
import {
13+
InsertChange,
14+
addImportToModule,
15+
addPackageToPackageJson,
16+
buildRelativePath,
17+
findModuleFromOptions,
18+
getProjectPath,
19+
insertImport,
20+
parseName,
21+
platformVersion,
22+
stringUtils,
23+
} from '@ngrx/router-store/schematics-core';
24+
import { Schema as RouterStoreOptions } from './schema';
25+
26+
function addImportToNgModule(options: RouterStoreOptions): Rule {
27+
return (host: Tree) => {
28+
const modulePath = options.module;
29+
30+
if (!modulePath) {
31+
return host;
32+
}
33+
34+
if (!host.exists(modulePath)) {
35+
throw new Error('Specified module does not exist');
36+
}
37+
38+
const text = host.read(modulePath);
39+
if (text === null) {
40+
throw new SchematicsException(`File ${modulePath} does not exist.`);
41+
}
42+
const sourceText = text.toString('utf-8');
43+
44+
const source = ts.createSourceFile(
45+
modulePath,
46+
sourceText,
47+
ts.ScriptTarget.Latest,
48+
true
49+
);
50+
51+
const [routerStoreNgModuleImport] = addImportToModule(
52+
source,
53+
modulePath,
54+
`StoreRouterConnectingModule.forRoot()`,
55+
`@ngrx/router-store`
56+
);
57+
58+
const changes = [
59+
insertImport(
60+
source,
61+
modulePath,
62+
'StoreRouterConnectingModule',
63+
'@ngrx/router-store'
64+
),
65+
routerStoreNgModuleImport,
66+
];
67+
const recorder = host.beginUpdate(modulePath);
68+
69+
for (const change of changes) {
70+
if (change instanceof InsertChange) {
71+
recorder.insertLeft(change.pos, change.toAdd);
72+
}
73+
}
74+
host.commitUpdate(recorder);
75+
76+
return host;
77+
};
78+
}
79+
80+
function addNgRxRouterStoreToPackageJson() {
81+
return (host: Tree, context: SchematicContext) => {
82+
addPackageToPackageJson(
83+
host,
84+
'dependencies',
85+
'@ngrx/router-store',
86+
platformVersion
87+
);
88+
context.addTask(new NodePackageInstallTask());
89+
return host;
90+
};
91+
}
92+
93+
export default function(options: RouterStoreOptions): Rule {
94+
return (host: Tree, context: SchematicContext) => {
95+
options.path = getProjectPath(host, options);
96+
97+
if (options.module) {
98+
options.module = findModuleFromOptions(host, {
99+
name: '',
100+
module: options.module,
101+
path: options.path,
102+
});
103+
}
104+
105+
const parsedPath = parseName(options.path, '');
106+
options.path = parsedPath.path;
107+
108+
return chain([
109+
branchAndMerge(chain([addImportToNgModule(options)])),
110+
options && options.skipPackageJson
111+
? noop()
112+
: addNgRxRouterStoreToPackageJson(),
113+
])(host, context);
114+
};
115+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"id": "SchematicsNgRxRouterStore",
4+
"title": "NgRx Router Store Schema",
5+
"type": "object",
6+
"properties": {
7+
"skipPackageJson": {
8+
"type": "boolean",
9+
"default": false,
10+
"description": "Do not add @ngrx/router-store as dependency to package.json (e.g., --skipPackageJson)."
11+
},
12+
"path": {
13+
"type": "string",
14+
"format": "path",
15+
"description": "The path to create the router store.",
16+
"visible": false
17+
},
18+
"project": {
19+
"type": "string",
20+
"description": "The name of the project.",
21+
"visible": false,
22+
"aliases": ["p"]
23+
},
24+
"module": {
25+
"type": "string",
26+
"default": "app",
27+
"description": "Allows specification of the declaring module.",
28+
"alias": "m",
29+
"subtype": "filepath"
30+
}
31+
},
32+
"required": []
33+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface Schema {
2+
skipPackageJson?: boolean;
3+
path?: string;
4+
project?: string;
5+
module?: string;
6+
}

0 commit comments

Comments
 (0)