Skip to content

Commit

Permalink
feat(schematics): implement router store ng-add (#1487)
Browse files Browse the repository at this point in the history
Closes #1479
  • Loading branch information
ngfelixl authored and brandonroberts committed Dec 31, 2018
1 parent 7f57f11 commit 9da4aac
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 1 deletion.
5 changes: 4 additions & 1 deletion modules/router-store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
"keywords": [
"RxJS",
"Angular",
"Redux"
"Redux",
"Schematics",
"Angular CLI"
],
"author": "NgRx",
"license": "MIT",
Expand All @@ -24,6 +26,7 @@
"@ngrx/store": "0.0.0-PLACEHOLDER",
"rxjs": "RXJS_VERSION"
},
"schematics": "MODULE_SCHEMATICS_COLLECTION",
"ng-update": {
"packageGroup": "NG_UPDATE_PACKAGE_GROUP",
"migrations": "NG_UPDATE_MIGRATIONS"
Expand Down
35 changes: 35 additions & 0 deletions modules/router-store/schematics/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package(default_visibility = ["//visibility:public"])

load("//tools:defaults.bzl", "npm_package", "ts_library")

ts_library(
name = "schematics",
srcs = glob(
[
"**/*.ts",
],
exclude = [
"**/*.spec.ts",
"**/files/**/*",
],
),
module_name = "@ngrx/router-store/schematics",
deps = [
"//modules/router-store/schematics-core",
"@npm//@angular-devkit/schematics",
"@npm//typescript",
],
)

npm_package(
name = "npm_package",
srcs = [
":collection.json",
] + glob([
"**/files/**/*",
"**/schema.json",
]),
deps = [
":schematics",
],
)
10 changes: 10 additions & 0 deletions modules/router-store/schematics/collection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"schematics": {
"ng-add": {
"aliases": ["init"],
"factory": "./ng-add",
"schema": "./ng-add/schema.json",
"description": "Register @ngrx/router-store within your application"
}
}
}
79 changes: 79 additions & 0 deletions modules/router-store/schematics/ng-add/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
SchematicTestRunner,
UnitTestTree,
} from '@angular-devkit/schematics/testing';
import * as path from 'path';
import { Schema as RouterStoreOptions } from './schema';
import {
getTestProjectPath,
createWorkspace,
} from '../../../schematics-core/testing';

describe('Router Store ng-add Schematic', () => {
const schematicRunner = new SchematicTestRunner(
'@ngrx/router-store',
path.join(__dirname, '../collection.json')
);
const defaultOptions: RouterStoreOptions = {
skipPackageJson: false,
module: 'app',
};

const projectPath = getTestProjectPath();

let appTree: UnitTestTree;

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

it('should update package.json', () => {
const options = { ...defaultOptions };

const tree = schematicRunner.runSchematic('ng-add', options, appTree);
const packageJson = JSON.parse(tree.readContent('/package.json'));

expect(packageJson.dependencies['@ngrx/router-store']).toBeDefined();
});

it('should skip package.json update', () => {
const options = { ...defaultOptions, skipPackageJson: true };

const tree = schematicRunner.runSchematic('ng-add', options, appTree);
const packageJson = JSON.parse(tree.readContent('/package.json'));

expect(packageJson.dependencies['@ngrx/router-store']).toBeUndefined();
});

it('should be provided by default', () => {
const options = { ...defaultOptions };

const tree = schematicRunner.runSchematic('ng-add', options, appTree);
const content = tree.readContent(`${projectPath}/src/app/app.module.ts`);
expect(content).toMatch(
/import { StoreRouterConnectingModule } from '@ngrx\/router-store';/
);
expect(content).toMatch(/StoreRouterConnectingModule.forRoot\(\)/);
});

it('should import into a specified module', () => {
const options = { ...defaultOptions };

const tree = schematicRunner.runSchematic('ng-add', options, appTree);
const content = tree.readContent(`${projectPath}/src/app/app.module.ts`);
expect(content).toMatch(
/import { StoreRouterConnectingModule } from '@ngrx\/router-store';/
);
});

it('should fail if specified module does not exist', () => {
const options = { ...defaultOptions, module: '/src/app/app.moduleXXX.ts' };
let thrownError: Error | null = null;
try {
schematicRunner.runSchematic('ng-add', options, appTree);
} catch (err) {
thrownError = err;
}
expect(thrownError).toBeDefined();
});
});
115 changes: 115 additions & 0 deletions modules/router-store/schematics/ng-add/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
Rule,
SchematicContext,
SchematicsException,
Tree,
branchAndMerge,
chain,
noop,
} from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
import * as ts from 'typescript';
import {
InsertChange,
addImportToModule,
addPackageToPackageJson,
buildRelativePath,
findModuleFromOptions,
getProjectPath,
insertImport,
parseName,
platformVersion,
stringUtils,
} from '@ngrx/router-store/schematics-core';
import { Schema as RouterStoreOptions } from './schema';

function addImportToNgModule(options: RouterStoreOptions): Rule {
return (host: Tree) => {
const modulePath = options.module;

if (!modulePath) {
return host;
}

if (!host.exists(modulePath)) {
throw new Error('Specified module does not exist');
}

const text = host.read(modulePath);
if (text === null) {
throw new SchematicsException(`File ${modulePath} does not exist.`);
}
const sourceText = text.toString('utf-8');

const source = ts.createSourceFile(
modulePath,
sourceText,
ts.ScriptTarget.Latest,
true
);

const [routerStoreNgModuleImport] = addImportToModule(
source,
modulePath,
`StoreRouterConnectingModule.forRoot()`,
`@ngrx/router-store`
);

const changes = [
insertImport(
source,
modulePath,
'StoreRouterConnectingModule',
'@ngrx/router-store'
),
routerStoreNgModuleImport,
];
const recorder = host.beginUpdate(modulePath);

for (const change of changes) {
if (change instanceof InsertChange) {
recorder.insertLeft(change.pos, change.toAdd);
}
}
host.commitUpdate(recorder);

return host;
};
}

function addNgRxRouterStoreToPackageJson() {
return (host: Tree, context: SchematicContext) => {
addPackageToPackageJson(
host,
'dependencies',
'@ngrx/router-store',
platformVersion
);
context.addTask(new NodePackageInstallTask());
return host;
};
}

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

if (options.module) {
options.module = findModuleFromOptions(host, {
name: '',
module: options.module,
path: options.path,
});
}

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

return chain([
branchAndMerge(chain([addImportToNgModule(options)])),
options && options.skipPackageJson
? noop()
: addNgRxRouterStoreToPackageJson(),
])(host, context);
};
}
33 changes: 33 additions & 0 deletions modules/router-store/schematics/ng-add/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchematicsNgRxRouterStore",
"title": "NgRx Router Store Schema",
"type": "object",
"properties": {
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add @ngrx/router-store as dependency to package.json (e.g., --skipPackageJson)."
},
"path": {
"type": "string",
"format": "path",
"description": "The path to create the router store.",
"visible": false
},
"project": {
"type": "string",
"description": "The name of the project.",
"visible": false,
"aliases": ["p"]
},
"module": {
"type": "string",
"default": "app",
"description": "Allows specification of the declaring module.",
"alias": "m",
"subtype": "filepath"
}
},
"required": []
}
6 changes: 6 additions & 0 deletions modules/router-store/schematics/ng-add/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Schema {
skipPackageJson?: boolean;
path?: string;
project?: string;
module?: string;
}

0 comments on commit 9da4aac

Please sign in to comment.