diff --git a/packages/schematics/angular/library/index.ts b/packages/schematics/angular/library/index.ts index 354f23b6bdf9..8ae1642f2db4 100644 --- a/packages/schematics/angular/library/index.ts +++ b/packages/schematics/angular/library/index.ts @@ -227,7 +227,7 @@ export default function (options: LibraryOptions): Rule { commonModule: false, flat: true, path: sourceDir, - project: projectName, + project: options.name, }), schematic('component', { name: options.name, @@ -237,13 +237,13 @@ export default function (options: LibraryOptions): Rule { flat: true, path: sourceDir, export: true, - project: projectName, + project: options.name, }), schematic('service', { name: options.name, flat: true, path: sourceDir, - project: projectName, + project: options.name, }), options.lintFix ? applyLintFix(sourceDir) : noop(), (_tree: Tree, context: SchematicContext) => { diff --git a/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__-routing.module.ts.template b/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__-routing.module.ts.template index 217c659c2250..fe292137bb21 100644 --- a/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__-routing.module.ts.template +++ b/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__-routing.module.ts.template @@ -1,7 +1,9 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; +<% if (lazyRoute) { %> +import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';<% } %> -const routes: Routes = []; +const routes: Routes = [<% if (lazyRoute) { %>{ path: '', component: <%= classify(name) %>Component }<% } %>]; @NgModule({ imports: [RouterModule.for<%= routingScope %>(routes)], diff --git a/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts.template b/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts.template index 3ba2010a36e9..c7eea239b596 100644 --- a/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts.template +++ b/packages/schematics/angular/module/files/__name@dasherize@if-flat__/__name@dasherize__.module.ts.template @@ -1,13 +1,19 @@ import { NgModule } from '@angular/core';<% if (commonModule) { %> -import { CommonModule } from '@angular/common';<% } %><% if (routing) { %> - +import { CommonModule } from '@angular/common';<% } %><% if (lazyRouteWithoutRouteModule) { %> +import { Routes, RouterModule } from '@angular/router';<% } %> +<% if (routing || lazyRouteWithRouteModule) { %> import { <%= classify(name) %>RoutingModule } from './<%= dasherize(name) %>-routing.module';<% } %> +<% if (lazyRouteWithoutRouteModule) { %> +const routes: Routes = [ + { path: '', component: <%= classify(name) %>Component } +];<% } %> @NgModule({ declarations: [], imports: [<% if (commonModule) { %> - CommonModule<%= routing ? ',' : '' %><% } %><% if (routing) { %> - <%= classify(name) %>RoutingModule<% } %> + CommonModule<%= routing || lazyRouteWithRouteModule ? ',' : '' %><% } %><% if (routing || lazyRouteWithRouteModule) { %> + <%= classify(name) %>RoutingModule<% } %><%= lazyRouteWithoutRouteModule ? ',' : '' %><% if (lazyRouteWithoutRouteModule) { %> + RouterModule.forChild(routes)<% } %> ] }) export class <%= classify(name) %>Module { } diff --git a/packages/schematics/angular/module/index.ts b/packages/schematics/angular/module/index.ts index e13c9b4a5cb5..83f5c5f67156 100644 --- a/packages/schematics/angular/module/index.ts +++ b/packages/schematics/angular/module/index.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { normalize, strings } from '@angular-devkit/core'; +import { Path, normalize, strings } from '@angular-devkit/core'; import { Rule, SchematicsException, @@ -17,17 +17,28 @@ import { mergeWith, move, noop, + schematic, url, } from '@angular-devkit/schematics'; import * as ts from '../third_party/github.com/Microsoft/TypeScript/lib/typescript'; -import { addImportToModule } from '../utility/ast-utils'; +import { addImportToModule, addRouteDeclarationToModule } from '../utility/ast-utils'; import { InsertChange } from '../utility/change'; import { buildRelativePath, findModuleFromOptions } from '../utility/find-module'; import { applyLintFix } from '../utility/lint-fix'; import { parseName } from '../utility/parse-name'; import { createDefaultPath } from '../utility/workspace'; -import { Schema as ModuleOptions } from './schema'; +import { RoutingScope, Schema as ModuleOptions } from './schema'; +function buildRelativeModulePath(options: ModuleOptions, modulePath: string): string { + const importModulePath = normalize( + `/${options.path}/` + + (options.flat ? '' : strings.dasherize(options.name) + '/') + + strings.dasherize(options.name) + + '.module', + ); + + return buildRelativePath(modulePath, importModulePath); +} function addDeclarationToNgModule(options: ModuleOptions): Rule { return (host: Tree) => { @@ -41,16 +52,10 @@ function addDeclarationToNgModule(options: ModuleOptions): Rule { if (text === null) { throw new SchematicsException(`File ${modulePath} does not exist.`); } - const sourceText = text.toString('utf-8'); + const sourceText = text.toString(); const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); - const importModulePath = normalize( - `/${options.path}/` - + (options.flat ? '' : strings.dasherize(options.name) + '/') - + strings.dasherize(options.name) - + '.module', - ); - const relativePath = buildRelativePath(modulePath, importModulePath); + const relativePath = buildRelativeModulePath(options, modulePath); const changes = addImportToModule(source, modulePath, strings.classify(`${options.name}Module`), @@ -68,6 +73,66 @@ function addDeclarationToNgModule(options: ModuleOptions): Rule { }; } +function addRouteDeclarationToNgModule( + options: ModuleOptions, + routingModulePath: Path | undefined, +): Rule { + return (host: Tree) => { + if (!options.route) { + return host; + } + if (!options.module) { + throw new Error('Module option required when creating a lazy loaded routing module.'); + } + + let path: string; + if (routingModulePath) { + path = routingModulePath; + } else { + path = options.module; + } + + const text = host.read(path); + if (!text) { + throw new Error(`Couldn't find the module nor its routing module.`); + } + + const sourceText = text.toString(); + const addDeclaration = addRouteDeclarationToModule( + ts.createSourceFile(path, sourceText, ts.ScriptTarget.Latest, true), + path, + buildRoute(options, options.module), + ) as InsertChange; + + const recorder = host.beginUpdate(path); + recorder.insertLeft(addDeclaration.pos, addDeclaration.toAdd); + host.commitUpdate(recorder); + + return host; + }; +} + +function getRoutingModulePath(host: Tree, options: ModuleOptions): Path | undefined { + let path: Path | undefined; + const modulePath = options.module as string; + const routingModuleName = modulePath.split('.')[0] + '-routing'; + const { module, ...rest } = options; + + try { + path = findModuleFromOptions(host, { module: routingModuleName, ...rest }); + } catch {} + + return path; +} + +function buildRoute(options: ModuleOptions, modulePath: string) { + const relativeModulePath = buildRelativeModulePath(options, modulePath); + const moduleName = `${strings.classify(options.name)}Module`; + const loadChildren = `() => import('${relativeModulePath}').then(m => m.${moduleName})`; + + return `{ path: '${options.route}', loadChildren: ${loadChildren} }`; +} + export default function (options: ModuleOptions): Rule { return async (host: Tree) => { if (options.path === undefined) { @@ -82,19 +147,41 @@ export default function (options: ModuleOptions): Rule { options.name = parsedPath.name; options.path = parsedPath.path; + let routingModulePath: Path | undefined; + const isLazyLoadedModuleGen = options.route && options.module; + if (isLazyLoadedModuleGen) { + options.routingScope = RoutingScope.Child; + routingModulePath = getRoutingModulePath(host, options); + } + const templateSource = apply(url('./files'), [ - options.routing ? noop() : filter(path => !path.endsWith('-routing.module.ts.template')), + options.routing || isLazyLoadedModuleGen && !!routingModulePath + ? noop() + : filter(path => !path.endsWith('-routing.module.ts.template')), applyTemplates({ ...strings, 'if-flat': (s: string) => options.flat ? '' : s, + lazyRoute: isLazyLoadedModuleGen, + lazyRouteWithoutRouteModule: isLazyLoadedModuleGen && !routingModulePath, + lazyRouteWithRouteModule: isLazyLoadedModuleGen && routingModulePath, ...options, }), move(parsedPath.path), ]); + const moduleDasherized = strings.dasherize(options.name); + const modulePath = + `${!options.flat ? moduleDasherized + '/' : ''}${moduleDasherized}.module.ts`; return chain([ - addDeclarationToNgModule(options), + !isLazyLoadedModuleGen ? addDeclarationToNgModule(options) : noop(), + addRouteDeclarationToNgModule(options, routingModulePath), mergeWith(templateSource), + isLazyLoadedModuleGen + ? schematic('component', { + ...options, + module: modulePath, + }) + : noop(), options.lintFix ? applyLintFix(options.path) : noop(), ]); }; diff --git a/packages/schematics/angular/module/index_spec.ts b/packages/schematics/angular/module/index_spec.ts index 3e5a807eb411..ae476a98dcf9 100644 --- a/packages/schematics/angular/module/index_spec.ts +++ b/packages/schematics/angular/module/index_spec.ts @@ -33,7 +33,7 @@ describe('Module Schematic', () => { name: 'bar', inlineStyle: false, inlineTemplate: false, - routing: false, + routing: true, skipTests: false, skipPackageJson: false, }; @@ -118,4 +118,95 @@ describe('Module Schematic', () => { .toPromise(); expect(appTree.files).toContain('/projects/bar/custom/app/foo/foo.module.ts'); }); + + describe('lazy route generator', () => { + const options = { + ...defaultOptions, + route: '/new-route', + module: 'app', + }; + + it('should generate a lazy loaded module with a routing module', async () => { + const tree = await schematicRunner.runSchematicAsync('module', options, appTree).toPromise(); + const files = tree.files; + + expect(files).toContain('/projects/bar/src/app/foo/foo.module.ts'); + expect(files).toContain('/projects/bar/src/app/foo/foo-routing.module.ts'); + expect(files).toContain('/projects/bar/src/app/foo/foo.component.ts'); + expect(files).toContain('/projects/bar/src/app/foo/foo.component.html'); + expect(files).toContain('/projects/bar/src/app/foo/foo.component.css'); + + const appRoutingModuleContent = tree.readContent('/projects/bar/src/app/app-routing.module.ts'); + expect(appRoutingModuleContent).toMatch( + /path: '\/new-route', loadChildren: \(\) => import\('.\/foo\/foo.module'\).then\(m => m.FooModule\)/, + ); + + const fooRoutingModuleContent = tree.readContent('/projects/bar/src/app/foo/foo-routing.module.ts'); + expect(fooRoutingModuleContent).toMatch(/RouterModule.forChild\(routes\)/); + expect(fooRoutingModuleContent) + .toMatch(/const routes: Routes = \[\r?\n?\s*{ path: '', component: FooComponent }\r?\n?\s*\];/); + }); + + it('should generate a lazy loaded module with embedded route declarations', async () => { + appTree.overwrite('/projects/bar/src/app/app.module.ts', + ` + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot([]) + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + `, + ); + appTree.delete('/projects/bar/src/app/app-routing.module.ts'); + + const tree = await schematicRunner.runSchematicAsync('module', options, appTree).toPromise(); + const files = tree.files; + + expect(files).toContain('/projects/bar/src/app/foo/foo.module.ts'); + expect(files).not.toContain('/projects/bar/src/app/foo/foo-routing.module.ts'); + expect(files).toContain('/projects/bar/src/app/foo/foo.component.ts'); + expect(files).toContain('/projects/bar/src/app/foo/foo.component.html'); + expect(files).toContain('/projects/bar/src/app/foo/foo.component.css'); + + const appModuleContent = tree.readContent('/projects/bar/src/app/app.module.ts'); + expect(appModuleContent).toMatch( + /path: '\/new-route', loadChildren: \(\) => import\('.\/foo\/foo.module'\).then\(m => m.FooModule\)/, + ); + + const fooModuleContent = tree.readContent('/projects/bar/src/app/foo/foo.module.ts'); + expect(fooModuleContent).toMatch(/RouterModule.forChild\(routes\)/); + expect(fooModuleContent) + .toMatch(/const routes: Routes = \[\r?\n?\s*{ path: '', component: FooComponent }\r?\n?\s*\];/); + }); + + it('should generate a lazy loaded module when "flat" flag is true', async () => { + const tree = await schematicRunner.runSchematicAsync( + 'module', + { ...options, flat: true }, + appTree, + ).toPromise(); + const files = tree.files; + + expect(files).toContain('/projects/bar/src/app/foo.module.ts'); + expect(files).toContain('/projects/bar/src/app/foo-routing.module.ts'); + expect(files).toContain('/projects/bar/src/app/foo.component.ts'); + expect(files).toContain('/projects/bar/src/app/foo.component.html'); + expect(files).toContain('/projects/bar/src/app/foo.component.css'); + + const appRoutingModuleContent = tree.readContent('/projects/bar/src/app/app-routing.module.ts'); + expect(appRoutingModuleContent).toMatch( + /path: '\/new-route', loadChildren: \(\) => import\('.\/foo.module'\).then\(m => m.FooModule\)/, + ); + }); + }); }); diff --git a/packages/schematics/angular/module/schema.json b/packages/schematics/angular/module/schema.json index 1e046c3bcf88..2f509c5c57fe 100644 --- a/packages/schematics/angular/module/schema.json +++ b/packages/schematics/angular/module/schema.json @@ -39,6 +39,10 @@ "description": "The scope for the new routing module.", "default": "Child" }, + "route": { + "type": "string", + "description": "Creates lazy loaded routing module. Requires --module option." + }, "flat": { "type": "boolean", "description": "When true, creates the new files at the top level of the current project root. ", diff --git a/packages/schematics/angular/utility/ast-utils.ts b/packages/schematics/angular/utility/ast-utils.ts index 461b34ba3b62..0f9a724153a7 100644 --- a/packages/schematics/angular/utility/ast-utils.ts +++ b/packages/schematics/angular/utility/ast-utils.ts @@ -344,6 +344,20 @@ export function getFirstNgModuleName(source: ts.SourceFile): string|undefined { return moduleClass.name.text; } +export function getMetadataField( + node: ts.ObjectLiteralExpression, + metadataField: string, +): ts.ObjectLiteralElement[] { + return node.properties + .filter(prop => ts.isPropertyAssignment(prop)) + // Filter out every fields that's not "metadataField". Also handles string literals + // (but not expressions). + .filter(({ name }: ts.PropertyAssignment) => { + return (ts.isIdentifier(name) || ts.isStringLiteral(name)) + && name.getText() === metadataField; + }); +} + export function addSymbolToNgModuleMetadata( source: ts.SourceFile, ngModulePath: string, @@ -360,22 +374,10 @@ export function addSymbolToNgModuleMetadata( } // Get all the children property assignment of object literals. - const matchingProperties: ts.ObjectLiteralElement[] = - (node as ts.ObjectLiteralExpression).properties - .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) - // Filter out every fields that's not "metadataField". Also handles string literals - // (but not expressions). - .filter((prop: ts.PropertyAssignment) => { - const name = prop.name; - switch (name.kind) { - case ts.SyntaxKind.Identifier: - return (name as ts.Identifier).getText(source) == metadataField; - case ts.SyntaxKind.StringLiteral: - return (name as ts.StringLiteral).text == metadataField; - } - - return false; - }); + const matchingProperties = getMetadataField( + node as ts.ObjectLiteralExpression, + metadataField, + ); // Get the last node of the array literal. if (!matchingProperties) { @@ -567,3 +569,96 @@ export function isImported(source: ts.SourceFile, return matchingNodes.length > 0; } + +/** + * Returns the RouterModule declaration from NgModule metadata, if any. + */ +export function getRouterModuleDeclaration(source: ts.SourceFile): ts.Expression | undefined { + const result = getDecoratorMetadata(source, 'NgModule', '@angular/core') as ts.Node[]; + const node = result[0] as ts.ObjectLiteralExpression; + const matchingProperties = getMetadataField(node, 'imports'); + + if (!matchingProperties) { + return; + } + + const assignment = matchingProperties[0] as ts.PropertyAssignment; + + if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + return; + } + + const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; + + return arrLiteral.elements + .filter(el => el.kind === ts.SyntaxKind.CallExpression) + .find(el => (el as ts.Identifier).getText().startsWith('RouterModule')); +} + +/** + * Adds a new route declaration to a router module (i.e. has a RouterModule declaration) + */ +export function addRouteDeclarationToModule( + source: ts.SourceFile, + fileToAdd: string, + routeLiteral: string, +): Change { + const routerModuleExpr = getRouterModuleDeclaration(source); + if (!routerModuleExpr) { + throw new Error(`Couldn't find a route declaration in ${fileToAdd}.`); + } + const scopeConfigMethodArgs = (routerModuleExpr as ts.CallExpression).arguments; + if (!scopeConfigMethodArgs.length) { + const { line } = source.getLineAndCharacterOfPosition(routerModuleExpr.getStart()); + throw new Error( + `The router module method doesn't have arguments ` + + `at line ${line} in ${fileToAdd}`, + ); + } + + let routesArr: ts.ArrayLiteralExpression | undefined; + const routesArg = scopeConfigMethodArgs[0]; + + // Check if the route declarations array is + // an inlined argument of RouterModule or a standalone variable + if (routesArg.kind === ts.SyntaxKind.ArrayLiteralExpression) { + routesArr = routesArg as ts.ArrayLiteralExpression; + } else { + const routesVarName = routesArg.getText(); + let routesVar; + if (routesArg.kind === ts.SyntaxKind.Identifier) { + routesVar = source.statements + .filter((s: ts.Statement) => s.kind === ts.SyntaxKind.VariableStatement) + .find((v: ts.VariableStatement) => { + return v.declarationList.declarations[0].name.getText() === routesVarName; + }) as ts.VariableStatement | undefined; + } + + if (!routesVar) { + const { line } = source.getLineAndCharacterOfPosition(routesArg.getStart()); + throw new Error( + `No route declaration array was found that corresponds ` + + `to router module at line ${line} in ${fileToAdd}`, + ); + } + const arrExpr = findNodes(routesVar, ts.SyntaxKind.ArrayLiteralExpression).pop(); + routesArr = arrExpr as ts.ArrayLiteralExpression; + } + + const occurencesCount = routesArr.elements.length; + const text = routesArr.getFullText(source); + + let route: string = routeLiteral; + if (occurencesCount > 0) { + const identation = text.match(/\r?\n(\r?)\s*/) || []; + route = `,${identation[0] || ' '}${routeLiteral}`; + } + + return insertAfterLastOccurrence( + routesArr.elements as unknown as ts.Node[], + route, + fileToAdd, + routesArr.elements.pos, + ts.SyntaxKind.ObjectLiteralExpression, + ); +} diff --git a/packages/schematics/angular/utility/ast-utils_spec.ts b/packages/schematics/angular/utility/ast-utils_spec.ts index 54049048a169..60a4958aa12f 100644 --- a/packages/schematics/angular/utility/ast-utils_spec.ts +++ b/packages/schematics/angular/utility/ast-utils_spec.ts @@ -15,6 +15,7 @@ import { addDeclarationToModule, addExportToModule, addProviderToModule, + addRouteDeclarationToModule, addSymbolToNgModuleMetadata, findNodes, insertAfterLastOccurrence, @@ -271,4 +272,240 @@ describe('ast utils', () => { expect(output).toMatch(/const arr = \['bar'\];/); }); }); + + // tslint:disable-next-line:no-big-function + describe('addRouteDeclarationToModule', () => { + it('should throw an error when there is no router module', () => { + const moduleContent = ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [BrowserModule], + bootstrap: [AppComponent] + }) + export class AppModule { } + `; + + const source = getTsSource(modulePath, moduleContent); + const change = () => addRouteDeclarationToModule(source, './src/app', ''); + expect(change).toThrowError(`Couldn't find a route declaration in ./src/app.`); + }); + + it(`should throw an error when router module doesn't have arguments`, () => { + const moduleContent = ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot() + ], + bootstrap: [AppComponent] + }) + export class AppModule { } + `; + + const source = getTsSource(modulePath, moduleContent); + const change = () => addRouteDeclarationToModule(source, './src/app', ''); + expect(change).toThrowError( + `The router module method doesn't have arguments at line 11 in ./src/app`, + ); + }); + + it(`should throw an error when the provided var (array) to router module doesn't exist`, () => { + const moduleContent = ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot(routes) + ], + bootstrap: [AppComponent] + }) + export class AppModule { } + `; + + const source = getTsSource(modulePath, moduleContent); + const change = () => addRouteDeclarationToModule(source, './src/app', ''); + expect(change).toThrowError( + // tslint:disable-next-line:max-line-length + `No route declaration array was found that corresponds to router module at line 11 in ./src/app`, + ); + }); + + // tslint:disable-next-line:max-line-length + it(`should throw an error, if the provided first argument of router module is not an identifier`, () => { + const moduleContent = ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot(42) + ], + bootstrap: [AppComponent] + }) + export class AppModule { } + `; + + const source = getTsSource(modulePath, moduleContent); + const change = () => addRouteDeclarationToModule(source, './src/app', ''); + expect(change).toThrowError( + // tslint:disable-next-line:max-line-length + `No route declaration array was found that corresponds to router module at line 11 in ./src/app`, + ); + }); + + it('should add a route to the routes array', () => { + const moduleContent = ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + + const routes = []; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot(routes) + ], + bootstrap: [AppComponent] + }) + export class AppModule { } + `; + + const source = getTsSource(modulePath, moduleContent); + const changes = addRouteDeclarationToModule( + source, + './src/app', `{ path: 'foo', component: FooComponent }`, + ); + const output = applyChanges(modulePath, moduleContent, [changes]); + + expect(output).toMatch(/const routes = \[{ path: 'foo', component: FooComponent }\]/); + }); + + it('should add a route to the routes array when there are multiple declarations', () => { + const moduleContent = ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + + const routes = [ + { path: 'foo', component: FooComponent } + ]; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot(routes) + ], + bootstrap: [AppComponent] + }) + export class AppModule { } + `; + + const source = getTsSource(modulePath, moduleContent); + const changes = addRouteDeclarationToModule( + source, + './src/app', `{ path: 'bar', component: BarComponent }`, + ); + const output = applyChanges(modulePath, moduleContent, [changes]); + + expect(output).toMatch( + // tslint:disable-next-line:max-line-length + /const routes = \[\r?\n?\s*{ path: 'foo', component: FooComponent },\r?\n?\s*{ path: 'bar', component: BarComponent }\r?\n?\s*\]/, + ); + }); + + it('should add a route to the routes argument of RouteModule', () => { + const moduleContent = ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot([]) + ], + bootstrap: [AppComponent] + }) + export class AppModule { } + `; + + const source = getTsSource(modulePath, moduleContent); + const changes = addRouteDeclarationToModule( + source, + './src/app', `{ path: 'foo', component: FooComponent }`, + ); + const output = applyChanges(modulePath, moduleContent, [changes]); + + expect(output).toMatch( + /RouterModule\.forRoot\(\[{ path: 'foo', component: FooComponent }\]\)/, + ); + }); + + // tslint:disable-next-line:max-line-length + it('should add a route to the routes argument of RouterModule when there are multiple declarations', () => { + const moduleContent = ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + RouterModule.forRoot([{ path: 'foo', component: FooComponent }]) + ], + bootstrap: [AppComponent] + }) + export class AppModule { } + `; + + const source = getTsSource(modulePath, moduleContent); + const changes = addRouteDeclarationToModule( + source, + './src/app', `{ path: 'bar', component: BarComponent }`, + ); + const output = applyChanges(modulePath, moduleContent, [changes]); + + expect(output).toMatch( + // tslint:disable-next-line:max-line-length + /RouterModule\.forRoot\(\[\r?\n?\s*{ path: 'foo', component: FooComponent },\r?\n?\s*{ path: 'bar', component: BarComponent }\r?\n?\s*\]\)/, + ); + }); + }); });