diff --git a/modules/effects/schematics/ng-add/index.spec.ts b/modules/effects/schematics/ng-add/index.spec.ts index fe93cd0270..566bfc76ad 100644 --- a/modules/effects/schematics/ng-add/index.spec.ts +++ b/modules/effects/schematics/ng-add/index.spec.ts @@ -39,7 +39,7 @@ describe('Effect ng-add Schematic', () => { const options = { ...defaultOptions }; const tree = schematicRunner.runSchematic('ng-add', options, appTree); - const packageJson = JSON.parse(getFileContent(tree, '/package.json')); + const packageJson = JSON.parse(tree.readContent('/package.json')); expect(packageJson.dependencies['@ngrx/effects']).toBeDefined(); }); @@ -48,7 +48,7 @@ describe('Effect ng-add Schematic', () => { const options = { ...defaultOptions, skipPackageJson: true }; const tree = schematicRunner.runSchematic('ng-add', options, appTree); - const packageJson = JSON.parse(getFileContent(tree, '/package.json')); + const packageJson = JSON.parse(tree.readContent('/package.json')); expect(packageJson.dependencies['@ngrx/effects']).toBeUndefined(); }); diff --git a/modules/store-devtools/BUILD b/modules/store-devtools/BUILD index f1cb949c1a..9ec1c79866 100644 --- a/modules/store-devtools/BUILD +++ b/modules/store-devtools/BUILD @@ -23,6 +23,7 @@ ng_package( entry_point = "modules/store-devtools/index.js", packages = [ "//modules/store-devtools/migrations:npm_package", + "//modules/store-devtools/schematics:npm_package", "//modules/store-devtools/schematics-core:npm_package", ], deps = [ diff --git a/modules/store-devtools/package.json b/modules/store-devtools/package.json index b8aa7af36c..cc6d5520f1 100644 --- a/modules/store-devtools/package.json +++ b/modules/store-devtools/package.json @@ -23,6 +23,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/store-devtools/schematics/BUILD b/modules/store-devtools/schematics/BUILD new file mode 100644 index 0000000000..8f06bffd4b --- /dev/null +++ b/modules/store-devtools/schematics/BUILD @@ -0,0 +1,33 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library", "npm_package") + +ts_library( + name = "schematics", + srcs = glob( + [ + "**/*.ts", + ], + exclude = [ + "**/*.spec.ts", + "**/files/**/*", + ], + ), + module_name = "@ngrx/store-devtools/schematics", + deps = [ + "//modules/store-devtools/schematics-core", + ], +) + +npm_package( + name = "npm_package", + srcs = [ + ":collection.json", + ] + glob([ + "**/files/**/*", + "**/schema.json", + ]), + deps = [ + ":schematics", + ], +) diff --git a/modules/store-devtools/schematics/collection.json b/modules/store-devtools/schematics/collection.json new file mode 100644 index 0000000000..0aa0458ed8 --- /dev/null +++ b/modules/store-devtools/schematics/collection.json @@ -0,0 +1,10 @@ +{ + "schematics": { + "ng-add": { + "aliases": ["init"], + "factory": "./ng-add", + "schema": "./ng-add/schema.json", + "description": "Adds initial setup for store-devtools" + } + } +} diff --git a/modules/store-devtools/schematics/ng-add/index.spec.ts b/modules/store-devtools/schematics/ng-add/index.spec.ts new file mode 100644 index 0000000000..b3486bd1d2 --- /dev/null +++ b/modules/store-devtools/schematics/ng-add/index.spec.ts @@ -0,0 +1,129 @@ +import { + SchematicTestRunner, + UnitTestTree, +} from '@angular-devkit/schematics/testing'; +import { getFileContent } from '@schematics/angular/utility/test'; +import * as path from 'path'; +import { Schema as StoreDevtoolsOptions } from './schema'; +import { + getTestProjectPath, + createWorkspace, +} from '../../../schematics-core/testing'; + +describe('Store-Devtools ng-add Schematic', () => { + const schematicRunner = new SchematicTestRunner( + '@ngrx/store-devtools', + path.join(__dirname, '../collection.json') + ); + const defaultOptions: StoreDevtoolsOptions = { + skipPackageJson: false, + project: 'bar', + 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/store-devtools']).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/store-devtools']).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 { StoreDevtoolsModule } from '@ngrx\/store-devtools';/ + ); + expect(content).toMatch( + /StoreDevtoolsModule.instrument\({ maxAge: 25, logOnly: environment.production }\)/ + ); + }); + + 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 { StoreDevtoolsModule } from '@ngrx\/store-devtools';/ + ); + }); + + it('should import the environments correctly', () => { + const options = { ...defaultOptions, module: 'app.module.ts' }; + + const tree = schematicRunner.runSchematic('ng-add', options, appTree); + const content = tree.readContent(`${projectPath}/src/app/app.module.ts`); + expect(content).toMatch( + /import { environment } from '..\/environments\/environment';/ + ); + }); + + 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(); + }); + + it('should fail if negative maxAges', () => { + const options = { ...defaultOptions, maxAge: -4 }; + + let thrownError: Error | null = null; + try { + schematicRunner.runSchematic('ng-add', options, appTree); + } catch (err) { + thrownError = err; + } + expect(thrownError).toBeDefined(); + }); + + it('should fail if maxAge of 1', () => { + const options = { ...defaultOptions, maxAge: -4 }; + + let thrownError: Error | null = null; + try { + schematicRunner.runSchematic('ng-add', options, appTree); + } catch (err) { + thrownError = err; + } + expect(thrownError).toBeDefined(); + }); + + it('should support a custom maxAge', () => { + const options = { + ...defaultOptions, + name: 'State', + maxAge: 5, + }; + + const tree = schematicRunner.runSchematic('ng-add', options, appTree); + const content = tree.readContent(`${projectPath}/src/app/app.module.ts`); + expect(content).toMatch(/maxAge: 5/); + }); +}); diff --git a/modules/store-devtools/schematics/ng-add/index.ts b/modules/store-devtools/schematics/ng-add/index.ts new file mode 100644 index 0000000000..de61421b40 --- /dev/null +++ b/modules/store-devtools/schematics/ng-add/index.ts @@ -0,0 +1,140 @@ +import { + Rule, + SchematicContext, + SchematicsException, + Tree, + branchAndMerge, + chain, + filter, + noop, +} from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; +import { + InsertChange, + addImportToModule, + buildRelativePath, + findModuleFromOptions, + getProjectPath, + insertImport, + addPackageToPackageJson, + platformVersion, + parseName, +} from '@ngrx/store-devtools/schematics-core'; +import { Path, dirname } from '@angular-devkit/core'; +import * as ts from 'typescript'; +import { Schema as StoreDevtoolsOptions } from './schema'; + +function addImportToNgModule(options: StoreDevtoolsOptions): 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 [instrumentNgModuleImport] = addImportToModule( + source, + modulePath, + `StoreDevtoolsModule.instrument({ maxAge: ${ + options.maxAge + }, logOnly: environment.production })`, + modulePath + ); + + const srcPath = dirname(options.path as Path); + const environmentsPath = buildRelativePath( + modulePath, + `/${srcPath}/environments/environment` + ); + + const changes = [ + insertImport( + source, + modulePath, + 'StoreDevtoolsModule', + '@ngrx/store-devtools' + ), + insertImport(source, modulePath, 'environment', environmentsPath), + instrumentNgModuleImport, + ]; + 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 addNgRxStoreDevToolsToPackageJson() { + return (host: Tree, context: SchematicContext) => { + addPackageToPackageJson( + host, + 'dependencies', + '@ngrx/store-devtools', + platformVersion + ); + context.addTask(new NodePackageInstallTask()); + return host; + }; +} + +export default function(options: StoreDevtoolsOptions): 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; + + if (options.maxAge! < 0 || options.maxAge === 1) { + throw new SchematicsException( + `maxAge should be an integer greater than 1.` + ); + } + + return chain([ + branchAndMerge( + chain([ + filter( + path => + path.endsWith('.module.ts') && + !path.endsWith('-routing.module.ts') + ), + addImportToNgModule(options), + ]) + ), + options && options.skipPackageJson + ? noop() + : addNgRxStoreDevToolsToPackageJson(), + ])(host, context); + }; +} diff --git a/modules/store-devtools/schematics/ng-add/schema.json b/modules/store-devtools/schematics/ng-add/schema.json new file mode 100644 index 0000000000..91f93cb406 --- /dev/null +++ b/modules/store-devtools/schematics/ng-add/schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "SchematicsNgRxRootState", + "title": "NgRx Root State Management Options Schema", + "type": "object", + "properties": { + "skipPackageJson": { + "type": "boolean", + "default": false, + "description": + "Do not add @ngrx/store as dependency to package.json (e.g., --skipPackageJson)." + }, + "path": { + "type": "string", + "format": "path", + "description": "The path to create the state.", + "visible": false + }, + "module": { + "type": "string", + "default": "app", + "description": "Allows specification of the declaring module.", + "alias": "m", + "subtype": "filepath" + }, + "maxAge": { + "type": "number", + "default": 25, + "description": + "number (>1) | 0 - maximum allowed actions to be stored in the history tree. The oldest actions are removed once maxAge is reached. It's critical for performance. 0 is infinite. Default is 25 for performance reasons." + } + }, + "required": [] +} diff --git a/modules/store-devtools/schematics/ng-add/schema.ts b/modules/store-devtools/schematics/ng-add/schema.ts new file mode 100644 index 0000000000..6f3a2090a8 --- /dev/null +++ b/modules/store-devtools/schematics/ng-add/schema.ts @@ -0,0 +1,7 @@ +export interface Schema { + skipPackageJson?: boolean; + path?: string; + project?: string; + module?: string; + maxAge?: number; +} diff --git a/modules/store/schematics/ng-add/index.spec.ts b/modules/store/schematics/ng-add/index.spec.ts index 50423fa36d..156a345693 100644 --- a/modules/store/schematics/ng-add/index.spec.ts +++ b/modules/store/schematics/ng-add/index.spec.ts @@ -33,7 +33,7 @@ describe('Store ng-add Schematic', () => { const options = { ...defaultOptions }; const tree = schematicRunner.runSchematic('ng-add', options, appTree); - const packageJson = JSON.parse(getFileContent(tree, '/package.json')); + const packageJson = JSON.parse(tree.readContent('/package.json')); expect(packageJson.dependencies['@ngrx/store']).toBeDefined(); }); @@ -42,7 +42,7 @@ describe('Store ng-add Schematic', () => { const options = { ...defaultOptions, skipPackageJson: true }; const tree = schematicRunner.runSchematic('ng-add', options, appTree); - const packageJson = JSON.parse(getFileContent(tree, '/package.json')); + const packageJson = JSON.parse(tree.readContent('/package.json')); expect(packageJson.dependencies['@ngrx/store']).toBeUndefined(); });