diff --git a/modules/router-store/package.json b/modules/router-store/package.json index 3e318865df..6ec5cb66da 100644 --- a/modules/router-store/package.json +++ b/modules/router-store/package.json @@ -9,7 +9,9 @@ "keywords": [ "RxJS", "Angular", - "Redux" + "Redux", + "Schematics", + "Angular CLI" ], "author": "NgRx", "license": "MIT", @@ -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" diff --git a/modules/router-store/schematics/BUILD b/modules/router-store/schematics/BUILD new file mode 100644 index 0000000000..69d183a411 --- /dev/null +++ b/modules/router-store/schematics/BUILD @@ -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", + ], +) diff --git a/modules/router-store/schematics/collection.json b/modules/router-store/schematics/collection.json new file mode 100644 index 0000000000..87afd051f8 --- /dev/null +++ b/modules/router-store/schematics/collection.json @@ -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" + } + } +} diff --git a/modules/router-store/schematics/ng-add/index.spec.ts b/modules/router-store/schematics/ng-add/index.spec.ts new file mode 100644 index 0000000000..ffd6f59052 --- /dev/null +++ b/modules/router-store/schematics/ng-add/index.spec.ts @@ -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(); + }); +}); diff --git a/modules/router-store/schematics/ng-add/index.ts b/modules/router-store/schematics/ng-add/index.ts new file mode 100644 index 0000000000..3cbe2faec4 --- /dev/null +++ b/modules/router-store/schematics/ng-add/index.ts @@ -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); + }; +} diff --git a/modules/router-store/schematics/ng-add/schema.json b/modules/router-store/schematics/ng-add/schema.json new file mode 100644 index 0000000000..018ca45acf --- /dev/null +++ b/modules/router-store/schematics/ng-add/schema.json @@ -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": [] +} diff --git a/modules/router-store/schematics/ng-add/schema.ts b/modules/router-store/schematics/ng-add/schema.ts new file mode 100644 index 0000000000..14969b929e --- /dev/null +++ b/modules/router-store/schematics/ng-add/schema.ts @@ -0,0 +1,6 @@ +export interface Schema { + skipPackageJson?: boolean; + path?: string; + project?: string; + module?: string; +}