From e6c51b3e06d9afca416f3b64b987df80444224ac Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Tue, 5 Feb 2019 16:47:35 +0200 Subject: [PATCH] feat(ivy): implement listing lazy routes for specific entry point in `ngtsc` (#28542) Related: angular/angular-cli#13532 Jira issue: FW-860 PR Close #28542 --- packages/compiler-cli/src/ngtsc/program.ts | 4 +- .../src/ngtsc/routing/src/analyzer.ts | 41 +- .../src/ngtsc/routing/src/lazy.ts | 36 +- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 566 +++++++++++++++--- 4 files changed, 561 insertions(+), 86 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 4fff26776c..54a2c6d654 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -192,9 +192,7 @@ export class NgtscProgram implements api.Program { listLazyRoutes(entryRoute?: string|undefined): api.LazyRoute[] { this.ensureAnalyzed(); - // Listing specific routes is unsupported for now, so we erroneously return - // all lazy routes instead (which should be okay for the CLI's usage). - return this.routeAnalyzer !.listLazyRoutes(); + return this.routeAnalyzer !.listLazyRoutes(entryRoute); } getLibrarySummaries(): Map { diff --git a/packages/compiler-cli/src/ngtsc/routing/src/analyzer.ts b/packages/compiler-cli/src/ngtsc/routing/src/analyzer.ts index 4443affdd2..d6ef4b4020 100644 --- a/packages/compiler-cli/src/ngtsc/routing/src/analyzer.ts +++ b/packages/compiler-cli/src/ngtsc/routing/src/analyzer.ts @@ -11,7 +11,7 @@ import * as ts from 'typescript'; import {ModuleResolver} from '../../imports'; import {PartialEvaluator} from '../../partial_evaluator'; -import {scanForRouteEntryPoints} from './lazy'; +import {scanForCandidateTransitiveModules, scanForRouteEntryPoints} from './lazy'; import {RouterEntryPointManager, entryPointKeyFor} from './route'; export interface NgModuleRawRouteData { @@ -48,18 +48,53 @@ export class NgModuleRouteAnalyzer { }); } - listLazyRoutes(): LazyRoute[] { + listLazyRoutes(entryModuleKey?: string|undefined): LazyRoute[] { + if ((entryModuleKey !== undefined) && !this.modules.has(entryModuleKey)) { + throw new Error(`Failed to list lazy routes: Unknown module '${entryModuleKey}'.`); + } + const routes: LazyRoute[] = []; - for (const key of Array.from(this.modules.keys())) { + const scannedModuleKeys = new Set(); + const pendingModuleKeys = entryModuleKey ? [entryModuleKey] : Array.from(this.modules.keys()); + + // When listing lazy routes for a specific entry module, we need to recursively extract + // "transitive" routes from imported/exported modules. This is not necessary when listing all + // lazy routes, because all analyzed modules will be scanned anyway. + const scanRecursively = entryModuleKey !== undefined; + + while (pendingModuleKeys.length > 0) { + const key = pendingModuleKeys.pop() !; + + if (scannedModuleKeys.has(key)) { + continue; + } else { + scannedModuleKeys.add(key); + } + const data = this.modules.get(key) !; const entryPoints = scanForRouteEntryPoints( data.sourceFile, data.moduleName, data, this.entryPointManager, this.evaluator); + routes.push(...entryPoints.map(entryPoint => ({ route: entryPoint.loadChildren, module: entryPoint.from, referencedModule: entryPoint.resolvedTo, }))); + + if (scanRecursively) { + pendingModuleKeys.push( + ...[ + // Scan the retrieved lazy route entry points. + ...entryPoints.map( + ({resolvedTo}) => entryPointKeyFor(resolvedTo.filePath, resolvedTo.moduleName)), + // Scan the current module's imported modules. + ...scanForCandidateTransitiveModules(data.imports, this.evaluator), + // Scan the current module's exported modules. + ...scanForCandidateTransitiveModules(data.exports, this.evaluator), + ].filter(key => this.modules.has(key))); + } } + return routes; } } diff --git a/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts b/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts index 62caeed917..3bfa9d2004 100644 --- a/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts +++ b/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts @@ -12,7 +12,7 @@ import {AbsoluteReference, NodeReference, Reference} from '../../imports'; import {ForeignFunctionResolver, PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {NgModuleRawRouteData} from './analyzer'; -import {RouterEntryPoint, RouterEntryPointManager} from './route'; +import {RouterEntryPoint, RouterEntryPointManager, entryPointKeyFor} from './route'; const ROUTES_MARKER = '__ngRoutesMarker__'; @@ -22,6 +22,35 @@ export interface LazyRouteEntry { resolvedTo: RouterEntryPoint; } +export function scanForCandidateTransitiveModules( + expr: ts.Expression | null, evaluator: PartialEvaluator): string[] { + if (expr === null) { + return []; + } + + const candidateModuleKeys: string[] = []; + const entries = evaluator.evaluate(expr); + + function recursivelyAddModules(entry: ResolvedValue) { + if (Array.isArray(entry)) { + for (const e of entry) { + recursivelyAddModules(e); + } + } else if (entry instanceof Map) { + if (entry.has('ngModule')) { + recursivelyAddModules(entry.get('ngModule') !); + } + } else if ((entry instanceof Reference) && hasIdentifier(entry.node)) { + const filePath = entry.node.getSourceFile().fileName; + const moduleName = entry.node.name.text; + candidateModuleKeys.push(entryPointKeyFor(filePath, moduleName)); + } + } + + recursivelyAddModules(entries); + return candidateModuleKeys; +} + export function scanForRouteEntryPoints( ngModule: ts.SourceFile, moduleName: string, data: NgModuleRawRouteData, entryPointManager: RouterEntryPointManager, evaluator: PartialEvaluator): LazyRouteEntry[] { @@ -152,6 +181,11 @@ const routerModuleFFR: ForeignFunctionResolver = ]); }; +function hasIdentifier(node: ts.Node): node is ts.Node&{name: ts.Identifier} { + const node_ = node as ts.NamedDeclaration; + return (node_.name !== undefined) && ts.isIdentifier(node_.name); +} + function isMethodNodeReference( ref: Reference): ref is NodeReference { diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 6d727ce9fb..ccf8d727c5 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -6,6 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ +import {LazyRoute} from '@angular/compiler-cli/src/ngtsc/routing'; +import * as path from 'path'; import * as ts from 'typescript'; import {NgtscTestEnvironment} from './env'; @@ -1439,13 +1441,13 @@ describe('ngtsc behavioral tests', () => { env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; import {NormalComponent} from './cyclic'; - + @Component({ selector: 'cyclic-component', template: 'Importing this causes a cycle', }) export class CyclicComponent {} - + @NgModule({ declarations: [NormalComponent, CyclicComponent], }) @@ -1454,7 +1456,7 @@ describe('ngtsc behavioral tests', () => { env.write('cyclic.ts', ` import {Component} from '@angular/core'; - + @Component({ selector: 'normal-component', template: '', @@ -1992,91 +1994,497 @@ describe('ngtsc behavioral tests', () => { }); }); - it('should detect all lazy routes', () => { - env.tsconfig(); - env.write('test.ts', ` - import {NgModule} from '@angular/core'; - import {RouterModule} from '@angular/router'; + describe('listLazyRoutes()', () => { + // clang-format off + const lazyRouteMatching = ( + route: string, fromModulePath: RegExp, fromModuleName: string, toModulePath: RegExp, + toModuleName: string) => { + return { + route, + module: jasmine.objectContaining({ + name: fromModuleName, + filePath: jasmine.stringMatching(fromModulePath), + }), + referencedModule: jasmine.objectContaining({ + name: toModuleName, + filePath: jasmine.stringMatching(toModulePath), + }), + } as unknown as LazyRoute; + }; + // clang-format on - @NgModule({ - imports: [ - RouterModule.forChild([ - {path: '', loadChildren: './lazy#LazyModule'}, - ]), - ], - }) - export class TestModule {} - `); - env.write('lazy.ts', ` - import {NgModule} from '@angular/core'; - import {RouterModule} from '@angular/router'; + beforeEach(() => { + env.tsconfig(); + env.write('node_modules/@angular/router/index.d.ts', ` + import {ModuleWithProviders} from '@angular/core'; - @NgModule({}) - export class LazyModule {} - `); - env.write('node_modules/@angular/router/index.d.ts', ` - import {ModuleWithProviders} from '@angular/core'; + export declare var ROUTES; + export declare class RouterModule { + static forRoot(arg1: any, arg2: any): ModuleWithProviders; + static forChild(arg1: any): ModuleWithProviders; + } + `); + }); - export declare var ROUTES; - export declare class RouterModule { - static forRoot(arg1: any, arg2: any): ModuleWithProviders; - static forChild(arg1: any): ModuleWithProviders; - } - `); + describe('when called without arguments', () => { + it('should list all routes', () => { + env.write('test.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; - const routes = env.driveRoutes(); - expect(routes.length).toBe(1); - expect(routes[0].route).toEqual('./lazy#LazyModule'); - expect(routes[0].module.filePath.endsWith('/test.ts')).toBe(true); - expect(routes[0].referencedModule.filePath.endsWith('/lazy.ts')).toBe(true); - }); + @NgModule({ + imports: [ + RouterModule.forRoot([ + {path: '1', loadChildren: './lazy/lazy-1#Lazy1Module'}, + {path: '2', loadChildren: './lazy/lazy-2#Lazy2Module'}, + ]), + ], + }) + export class TestModule {} + `); + env.write('lazy/lazy-1.ts', ` + import {NgModule} from '@angular/core'; - it('should detect lazy routes in simple children routes', () => { - env.tsconfig(); - env.write('test.ts', ` - import {NgModule} from '@angular/core'; - import {RouterModule} from '@angular/router'; - - @Component({ - selector: 'foo', - template: '
Foo
' - }) - class FooCmp {} + @NgModule({}) + export class Lazy1Module {} + `); + env.write('lazy/lazy-2.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; - @NgModule({ - imports: [ - RouterModule.forRoot([ - {path: '', children: [ - {path: 'foo', component: FooCmp}, - {path: 'lazy', loadChildren: './lazy#LazyModule'} - ]}, - ]), - ], - }) - export class TestModule {} - `); - env.write('lazy.ts', ` - import {NgModule} from '@angular/core'; - import {RouterModule} from '@angular/router'; + @NgModule({ + imports: [ + RouterModule.forChild([ + {path: '3', loadChildren: './lazy-3#Lazy3Module'}, + ]), + ], + }) + export class Lazy2Module {} + `); + env.write('lazy/lazy-3.ts', ` + import {NgModule} from '@angular/core'; - @NgModule({}) - export class LazyModule {} - `); - env.write('node_modules/@angular/router/index.d.ts', ` - import {ModuleWithProviders} from '@angular/core'; + @NgModule({}) + export class Lazy3Module {} + `); - export declare var ROUTES; - export declare class RouterModule { - static forRoot(arg1: any, arg2: any): ModuleWithProviders; - static forChild(arg1: any): ModuleWithProviders; - } - `); + const routes = env.driveRoutes(); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy-3#Lazy3Module', /\/lazy\/lazy-2\.ts$/, 'Lazy2Module', /\/lazy\/lazy-3\.ts$/, + 'Lazy3Module'), + lazyRouteMatching( + './lazy/lazy-1#Lazy1Module', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy-1\.ts$/, + 'Lazy1Module'), + lazyRouteMatching( + './lazy/lazy-2#Lazy2Module', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy-2\.ts$/, + 'Lazy2Module'), + ]); + }); - const routes = env.driveRoutes(); - expect(routes.length).toBe(1); - expect(routes[0].route).toEqual('./lazy#LazyModule'); - expect(routes[0].module.filePath.endsWith('/test.ts')).toBe(true); - expect(routes[0].referencedModule.filePath.endsWith('/lazy.ts')).toBe(true); + it('should detect lazy routes in simple children routes', () => { + env.write('test.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @Component({ + selector: 'foo', + template: '
Foo
' + }) + class FooCmp {} + + @NgModule({ + imports: [ + RouterModule.forRoot([ + {path: '', children: [ + {path: 'foo', component: FooCmp}, + {path: 'lazy', loadChildren: './lazy#LazyModule'} + ]}, + ]), + ], + }) + export class TestModule {} + `); + env.write('lazy.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @NgModule({}) + export class LazyModule {} + `); + + const routes = env.driveRoutes(); + expect(routes).toEqual([ + lazyRouteMatching( + './lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\.ts$/, 'LazyModule'), + ]); + }); + }); + + describe('when called with entry module', () => { + it('should throw if the entry module hasn\'t been analyzed', () => { + env.write('test.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', loadChildren: './lazy#LazyModule'}, + ]), + ], + }) + export class TestModule {} + `); + + const entryModule1 = path.join(env.basePath, 'test#TestModule'); + const entryModule2 = path.join(env.basePath, 'not-test#TestModule'); + const entryModule3 = path.join(env.basePath, 'test#NotTestModule'); + + expect(() => env.driveRoutes(entryModule1)).not.toThrow(); + expect(() => env.driveRoutes(entryModule2)) + .toThrowError(`Failed to list lazy routes: Unknown module '${entryModule2}'.`); + expect(() => env.driveRoutes(entryModule3)) + .toThrowError(`Failed to list lazy routes: Unknown module '${entryModule3}'.`); + }); + + it('should list all transitive lazy routes', () => { + env.write('test.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + import {Test1Module as Test1ModuleRenamed} from './test-1'; + import {Test2Module} from './test-2'; + + @NgModule({ + exports: [ + Test1ModuleRenamed, + ], + imports: [ + Test2Module, + RouterModule.forRoot([ + {path: '', loadChildren: './lazy/lazy#LazyModule'}, + ]), + ], + }) + export class TestModule {} + `); + env.write('test-1.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @NgModule({ + imports: [ + RouterModule.forChild([ + {path: 'one', loadChildren: './lazy-1/lazy-1#Lazy1Module'}, + ]), + ], + }) + export class Test1Module {} + `); + env.write('test-2.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @NgModule({ + exports: [ + RouterModule.forChild([ + {path: 'two', loadChildren: './lazy-2/lazy-2#Lazy2Module'}, + ]), + ], + }) + export class Test2Module {} + `); + env.write('lazy/lazy.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class LazyModule {} + `); + env.write('lazy-1/lazy-1.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class Lazy1Module {} + `); + env.write('lazy-2/lazy-2.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class Lazy2Module {} + `); + + const routes = env.driveRoutes(path.join(env.basePath, 'test#TestModule')); + + expect(routes).toEqual([ + lazyRouteMatching( + './lazy/lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy\.ts$/, + 'LazyModule'), + lazyRouteMatching( + './lazy-1/lazy-1#Lazy1Module', /\/test-1\.ts$/, 'Test1Module', + /\/lazy-1\/lazy-1\.ts$/, 'Lazy1Module'), + lazyRouteMatching( + './lazy-2/lazy-2#Lazy2Module', /\/test-2\.ts$/, 'Test2Module', + /\/lazy-2\/lazy-2\.ts$/, 'Lazy2Module'), + ]); + }); + + it('should ignore exports that do not refer to an `NgModule`', () => { + env.write('test-1.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + import {Test2Component, Test2Module} from './test-2'; + + @NgModule({ + exports: [ + Test2Component, + Test2Module, + ], + imports: [ + RouterModule.forRoot([ + {path: '', loadChildren: './lazy-1/lazy-1#Lazy1Module'}, + ]), + ], + }) + export class Test1Module {} + `); + env.write('test-2.ts', ` + import {Component, NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @Component({ + selector: 'test-2', + template: '', + }) + export class Test2Component {} + + @NgModule({ + declarations: [ + Test2Component, + ], + exports: [ + Test2Component, + RouterModule.forChild([ + {path: 'two', loadChildren: './lazy-2/lazy-2#Lazy2Module'}, + ]), + ], + }) + export class Test2Module {} + `); + env.write('lazy-1/lazy-1.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class Lazy1Module {} + `); + env.write('lazy-2/lazy-2.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class Lazy2Module {} + `); + + const routes = env.driveRoutes(path.join(env.basePath, 'test-1#Test1Module')); + + expect(routes).toEqual([ + lazyRouteMatching( + './lazy-1/lazy-1#Lazy1Module', /\/test-1\.ts$/, 'Test1Module', + /\/lazy-1\/lazy-1\.ts$/, 'Lazy1Module'), + lazyRouteMatching( + './lazy-2/lazy-2#Lazy2Module', /\/test-2\.ts$/, 'Test2Module', + /\/lazy-2\/lazy-2\.ts$/, 'Lazy2Module'), + ]); + }); + + it('should support `ModuleWithProviders`', () => { + env.write('test.ts', ` + import {ModuleWithProviders, NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', loadChildren: './lazy-2/lazy-2#Lazy2Module'}, + ]), + ], + }) + export class TestRoutingModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: TestRoutingModule, + providers: [], + }; + } + } + + @NgModule({ + imports: [ + TestRoutingModule.forRoot(), + RouterModule.forRoot([ + {path: '', loadChildren: './lazy-1/lazy-1#Lazy1Module'}, + ]), + ], + }) + export class TestModule {} + `); + env.write('lazy-1/lazy-1.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class Lazy1Module {} + `); + env.write('lazy-2/lazy-2.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class Lazy2Module {} + `); + + const routes = env.driveRoutes(path.join(env.basePath, 'test#TestModule')); + + expect(routes).toEqual([ + lazyRouteMatching( + './lazy-1/lazy-1#Lazy1Module', /\/test\.ts$/, 'TestModule', /\/lazy-1\/lazy-1\.ts$/, + 'Lazy1Module'), + lazyRouteMatching( + './lazy-2/lazy-2#Lazy2Module', /\/test\.ts$/, 'TestRoutingModule', + /\/lazy-2\/lazy-2\.ts$/, 'Lazy2Module'), + ]); + }); + + it('should only process each module once', () => { + env.write('test.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', loadChildren: './lazy/lazy#LazyModule'}, + ]), + ], + }) + export class SharedModule {} + + @NgModule({ + imports: [ + SharedModule, + RouterModule.forRoot([ + {path: '', loadChildren: './lazy/lazy#LazyModule'}, + ]), + ], + }) + export class TestModule {} + `); + env.write('lazy/lazy.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', loadChildren: '../lazier/lazier#LazierModule'}, + ]), + ], + }) + export class LazyModule {} + `); + env.write('lazier/lazier.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class LazierModule {} + `); + + const routes = env.driveRoutes(path.join(env.basePath, 'test#TestModule')); + + // `LazyModule` is referenced in both `SharedModule` and `TestModule`, + // but it is only processed once (hence one `LazierModule` entry). + expect(routes).toEqual([ + lazyRouteMatching( + './lazy/lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy\.ts$/, + 'LazyModule'), + lazyRouteMatching( + './lazy/lazy#LazyModule', /\/test\.ts$/, 'SharedModule', /\/lazy\/lazy\.ts$/, + 'LazyModule'), + lazyRouteMatching( + '../lazier/lazier#LazierModule', /\/lazy\/lazy\.ts$/, 'LazyModule', + /\/lazier\/lazier\.ts$/, 'LazierModule'), + ]); + }); + + it('should ignore modules not (transitively) referenced by the entry module', () => { + env.write('test.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @NgModule({ + imports: [ + RouterModule.forRoot([ + {path: '', loadChildren: './lazy/lazy#Lazy1Module'}, + ]), + ], + }) + export class Test1Module {} + + @NgModule({ + imports: [ + RouterModule.forRoot([ + {path: '', loadChildren: './lazy/lazy#Lazy2Module'}, + ]), + ], + }) + export class Test2Module {} + `); + env.write('lazy/lazy.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class Lazy1Module {} + + @NgModule({}) + export class Lazy2Module {} + `); + + const routes = env.driveRoutes(path.join(env.basePath, 'test#Test1Module')); + + expect(routes).toEqual([ + lazyRouteMatching( + './lazy/lazy#Lazy1Module', /\/test\.ts$/, 'Test1Module', /\/lazy\/lazy\.ts$/, + 'Lazy1Module'), + ]); + }); + + it('should ignore routes to unknown modules', () => { + env.write('test.ts', ` + import {NgModule} from '@angular/core'; + import {RouterModule} from '@angular/router'; + + @NgModule({ + imports: [ + RouterModule.forRoot([ + {path: '', loadChildren: './unknown/unknown#UnknownModule'}, + {path: '', loadChildren: './lazy/lazy#LazyModule'}, + ]), + ], + }) + export class TestModule {} + `); + env.write('lazy/lazy.ts', ` + import {NgModule} from '@angular/core'; + + @NgModule({}) + export class LazyModule {} + `); + + const routes = env.driveRoutes(path.join(env.basePath, 'test#TestModule')); + + expect(routes).toEqual([ + lazyRouteMatching( + './lazy/lazy#LazyModule', /\/test\.ts$/, 'TestModule', /\/lazy\/lazy\.ts$/, + 'LazyModule'), + ]); + }); + }); }); });