feat(ivy): implement ngtsc's route analysis (#27697)
This commit introduces the NgModuleRouteAnalyzer & friends, which given metadata about the NgModules in a program can extract the list of lazy routes in the same format that the ngtools API uses. PR Close #27697
This commit is contained in:
		
							parent
							
								
									2fc5f002e0
								
							
						
					
					
						commit
						da85cee07c
					
				
							
								
								
									
										19
									
								
								packages/compiler-cli/src/ngtsc/routing/BUILD.bazel
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								packages/compiler-cli/src/ngtsc/routing/BUILD.bazel
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | package(default_visibility = ["//visibility:public"]) | ||||||
|  | 
 | ||||||
|  | load("//tools:defaults.bzl", "ts_library") | ||||||
|  | 
 | ||||||
|  | ts_library( | ||||||
|  |     name = "routing", | ||||||
|  |     srcs = glob([ | ||||||
|  |         "index.ts", | ||||||
|  |         "src/**/*.ts", | ||||||
|  |     ]), | ||||||
|  |     module_name = "@angular/compiler-cli/src/ngtsc/routing", | ||||||
|  |     deps = [ | ||||||
|  |         "//packages/compiler", | ||||||
|  |         "//packages/compiler-cli/src/ngtsc/imports", | ||||||
|  |         "//packages/compiler-cli/src/ngtsc/partial_evaluator", | ||||||
|  |         "@ngdeps//@types/node", | ||||||
|  |         "@ngdeps//typescript", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
							
								
								
									
										11
									
								
								packages/compiler-cli/src/ngtsc/routing/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								packages/compiler-cli/src/ngtsc/routing/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * 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
 | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /// <reference types="node" />
 | ||||||
|  | 
 | ||||||
|  | export {LazyRoute, NgModuleRouteAnalyzer} from './src/analyzer'; | ||||||
							
								
								
									
										65
									
								
								packages/compiler-cli/src/ngtsc/routing/src/analyzer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								packages/compiler-cli/src/ngtsc/routing/src/analyzer.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * 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 * as ts from 'typescript'; | ||||||
|  | 
 | ||||||
|  | import {ModuleResolver} from '../../imports'; | ||||||
|  | import {PartialEvaluator} from '../../partial_evaluator'; | ||||||
|  | 
 | ||||||
|  | import {scanForRouteEntryPoints} from './lazy'; | ||||||
|  | import {RouterEntryPointManager} from './route'; | ||||||
|  | 
 | ||||||
|  | export interface NgModuleRawRouteData { | ||||||
|  |   sourceFile: ts.SourceFile; | ||||||
|  |   moduleName: string; | ||||||
|  |   imports: ts.Expression|null; | ||||||
|  |   exports: ts.Expression|null; | ||||||
|  |   providers: ts.Expression|null; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export interface LazyRoute { | ||||||
|  |   route: string; | ||||||
|  |   module: {name: string, filePath: string}; | ||||||
|  |   referencedModule: {name: string, filePath: string}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class NgModuleRouteAnalyzer { | ||||||
|  |   private modules = new Map<string, NgModuleRawRouteData>(); | ||||||
|  |   private entryPointManager: RouterEntryPointManager; | ||||||
|  | 
 | ||||||
|  |   constructor(moduleResolver: ModuleResolver, private evaluator: PartialEvaluator) { | ||||||
|  |     this.entryPointManager = new RouterEntryPointManager(moduleResolver); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   add(sourceFile: ts.SourceFile, moduleName: string, imports: ts.Expression|null, | ||||||
|  |       exports: ts.Expression|null, providers: ts.Expression|null): void { | ||||||
|  |     const key = `${sourceFile.fileName}#${moduleName}`; | ||||||
|  |     if (this.modules.has(key)) { | ||||||
|  |       throw new Error(`Double route analyzing ${key}`); | ||||||
|  |     } | ||||||
|  |     this.modules.set( | ||||||
|  |         key, { | ||||||
|  |                  sourceFile, moduleName, imports, exports, providers, | ||||||
|  |              }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   listLazyRoutes(): LazyRoute[] { | ||||||
|  |     const routes: LazyRoute[] = []; | ||||||
|  |     for (const key of Array.from(this.modules.keys())) { | ||||||
|  |       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, | ||||||
|  |                                      }))); | ||||||
|  |     } | ||||||
|  |     return routes; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										164
									
								
								packages/compiler-cli/src/ngtsc/routing/src/lazy.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								packages/compiler-cli/src/ngtsc/routing/src/lazy.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,164 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * 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 * as ts from 'typescript'; | ||||||
|  | 
 | ||||||
|  | import {AbsoluteReference, NodeReference, Reference} from '../../imports'; | ||||||
|  | import {ForeignFunctionResolver, PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; | ||||||
|  | 
 | ||||||
|  | import {NgModuleRawRouteData} from './analyzer'; | ||||||
|  | import {RouterEntryPoint, RouterEntryPointManager} from './route'; | ||||||
|  | 
 | ||||||
|  | const ROUTES_MARKER = '__ngRoutesMarker__'; | ||||||
|  | 
 | ||||||
|  | export interface LazyRouteEntry { | ||||||
|  |   loadChildren: string; | ||||||
|  |   from: RouterEntryPoint; | ||||||
|  |   resolvedTo: RouterEntryPoint; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function scanForRouteEntryPoints( | ||||||
|  |     ngModule: ts.SourceFile, moduleName: string, data: NgModuleRawRouteData, | ||||||
|  |     entryPointManager: RouterEntryPointManager, evaluator: PartialEvaluator): LazyRouteEntry[] { | ||||||
|  |   const loadChildrenIdentifiers: string[] = []; | ||||||
|  |   const from = entryPointManager.fromNgModule(ngModule, moduleName); | ||||||
|  |   if (data.providers !== null) { | ||||||
|  |     loadChildrenIdentifiers.push(...scanForProviders(data.providers, evaluator)); | ||||||
|  |   } | ||||||
|  |   if (data.imports !== null) { | ||||||
|  |     loadChildrenIdentifiers.push(...scanForRouterModuleUsage(data.imports, evaluator)); | ||||||
|  |   } | ||||||
|  |   if (data.exports !== null) { | ||||||
|  |     loadChildrenIdentifiers.push(...scanForRouterModuleUsage(data.exports, evaluator)); | ||||||
|  |   } | ||||||
|  |   const routes: LazyRouteEntry[] = []; | ||||||
|  |   for (const loadChildren of loadChildrenIdentifiers) { | ||||||
|  |     const resolvedTo = entryPointManager.resolveLoadChildrenIdentifier(loadChildren, ngModule); | ||||||
|  |     if (resolvedTo !== null) { | ||||||
|  |       routes.push({ | ||||||
|  |           loadChildren, from, resolvedTo, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return routes; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function scanForProviders(expr: ts.Expression, evaluator: PartialEvaluator): string[] { | ||||||
|  |   const loadChildrenIdentifiers: string[] = []; | ||||||
|  |   const providers = evaluator.evaluate(expr); | ||||||
|  | 
 | ||||||
|  |   function recursivelyAddProviders(provider: ResolvedValue): void { | ||||||
|  |     if (Array.isArray(provider)) { | ||||||
|  |       for (const entry of provider) { | ||||||
|  |         recursivelyAddProviders(entry); | ||||||
|  |       } | ||||||
|  |     } else if (provider instanceof Map) { | ||||||
|  |       if (provider.has('provide') && provider.has('useValue')) { | ||||||
|  |         const provide = provider.get('provide'); | ||||||
|  |         const useValue = provider.get('useValue'); | ||||||
|  |         if (isRouteToken(provide) && Array.isArray(useValue)) { | ||||||
|  |           loadChildrenIdentifiers.push(...scanForLazyRoutes(useValue)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   recursivelyAddProviders(providers); | ||||||
|  |   return loadChildrenIdentifiers; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function scanForRouterModuleUsage(expr: ts.Expression, evaluator: PartialEvaluator): string[] { | ||||||
|  |   const loadChildrenIdentifiers: string[] = []; | ||||||
|  |   const imports = evaluator.evaluate(expr, routerModuleFFR); | ||||||
|  | 
 | ||||||
|  |   function recursivelyAddRoutes(imp: ResolvedValue) { | ||||||
|  |     if (Array.isArray(imp)) { | ||||||
|  |       for (const entry of imp) { | ||||||
|  |         recursivelyAddRoutes(entry); | ||||||
|  |       } | ||||||
|  |     } else if (imp instanceof Map) { | ||||||
|  |       if (imp.has(ROUTES_MARKER) && imp.has('routes')) { | ||||||
|  |         const routes = imp.get('routes'); | ||||||
|  |         if (Array.isArray(routes)) { | ||||||
|  |           loadChildrenIdentifiers.push(...scanForLazyRoutes(routes)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   recursivelyAddRoutes(imports); | ||||||
|  |   return loadChildrenIdentifiers; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function scanForLazyRoutes(routes: ResolvedValue[]): string[] { | ||||||
|  |   const loadChildrenIdentifiers: string[] = []; | ||||||
|  | 
 | ||||||
|  |   function recursivelyScanRoutes(routes: ResolvedValue[]): void { | ||||||
|  |     for (let route of routes) { | ||||||
|  |       if (!(route instanceof Map)) { | ||||||
|  |         continue; | ||||||
|  |       } | ||||||
|  |       if (route.has('loadChildren')) { | ||||||
|  |         const loadChildren = route.get('loadChildren'); | ||||||
|  |         if (typeof loadChildren === 'string') { | ||||||
|  |           loadChildrenIdentifiers.push(loadChildren); | ||||||
|  |         } | ||||||
|  |       } else if (route.has('children')) { | ||||||
|  |         const children = route.get('children'); | ||||||
|  |         if (Array.isArray(children)) { | ||||||
|  |           recursivelyScanRoutes(routes); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   recursivelyScanRoutes(routes); | ||||||
|  |   return loadChildrenIdentifiers; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A foreign function resolver that converts `RouterModule.forRoot/forChild(X)` to a special object | ||||||
|  |  * of the form `{__ngRoutesMarker__: true, routes: X}`. | ||||||
|  |  * | ||||||
|  |  * These objects are then recognizable inside the larger set of imports/exports. | ||||||
|  |  */ | ||||||
|  | const routerModuleFFR: ForeignFunctionResolver = | ||||||
|  |     function routerModuleFFR( | ||||||
|  |         ref: Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>, | ||||||
|  |         args: ReadonlyArray<ts.Expression>): ts.Expression | | ||||||
|  |     null { | ||||||
|  |       if (!isMethodNodeReference(ref) || !ts.isClassDeclaration(ref.node.parent)) { | ||||||
|  |         return null; | ||||||
|  |       } else if (ref.moduleName !== '@angular/router') { | ||||||
|  |         return null; | ||||||
|  |       } else if ( | ||||||
|  |           ref.node.parent.name === undefined || ref.node.parent.name.text !== 'RouterModule') { | ||||||
|  |         return null; | ||||||
|  |       } else if ( | ||||||
|  |           !ts.isIdentifier(ref.node.name) || | ||||||
|  |           (ref.node.name.text !== 'forRoot' && ref.node.name.text !== 'forChild')) { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const routes = args[0]; | ||||||
|  |       return ts.createObjectLiteral([ | ||||||
|  |         ts.createPropertyAssignment(ROUTES_MARKER, ts.createTrue()), | ||||||
|  |         ts.createPropertyAssignment('routes', routes), | ||||||
|  |       ]); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  | function isMethodNodeReference( | ||||||
|  |     ref: Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>): | ||||||
|  |     ref is NodeReference<ts.MethodDeclaration> { | ||||||
|  |   return ref instanceof NodeReference && ts.isMethodDeclaration(ref.node); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function isRouteToken(ref: ResolvedValue): boolean { | ||||||
|  |   return ref instanceof AbsoluteReference && ref.moduleName === '@angular/router' && | ||||||
|  |       ref.symbolName === 'ROUTES'; | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								packages/compiler-cli/src/ngtsc/routing/src/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								packages/compiler-cli/src/ngtsc/routing/src/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. All Rights Reserved. | ||||||
|  |  * | ||||||
|  |  * 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 * as ts from 'typescript'; | ||||||
|  | 
 | ||||||
|  | import {ModuleResolver} from '../../imports'; | ||||||
|  | 
 | ||||||
|  | export abstract class RouterEntryPoint { | ||||||
|  |   abstract readonly filePath: string; | ||||||
|  | 
 | ||||||
|  |   abstract readonly moduleName: string; | ||||||
|  | 
 | ||||||
|  |   // Alias of moduleName.
 | ||||||
|  |   abstract readonly name: string; | ||||||
|  | 
 | ||||||
|  |   abstract toString(): string; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class RouterEntryPointImpl implements RouterEntryPoint { | ||||||
|  |   constructor(readonly filePath: string, readonly moduleName: string) {} | ||||||
|  | 
 | ||||||
|  |   get name(): string { return this.moduleName; } | ||||||
|  | 
 | ||||||
|  |   toString(): string { return `${this.filePath}#${this.moduleName}`; } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export class RouterEntryPointManager { | ||||||
|  |   private map = new Map<string, RouterEntryPoint>(); | ||||||
|  | 
 | ||||||
|  |   constructor(private moduleResolver: ModuleResolver) {} | ||||||
|  | 
 | ||||||
|  |   resolveLoadChildrenIdentifier(loadChildrenIdentifier: string, context: ts.SourceFile): | ||||||
|  |       RouterEntryPoint|null { | ||||||
|  |     const [relativeFile, moduleName] = loadChildrenIdentifier.split('#'); | ||||||
|  |     if (moduleName === undefined) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     const resolvedSf = this.moduleResolver.resolveModuleName(relativeFile, context); | ||||||
|  |     if (resolvedSf === null) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |     return this.fromNgModule(resolvedSf, moduleName); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   fromNgModule(sf: ts.SourceFile, moduleName: string): RouterEntryPoint { | ||||||
|  |     const absoluteFile = sf.fileName; | ||||||
|  |     const key = `${absoluteFile}#${moduleName}`; | ||||||
|  |     if (!this.map.has(key)) { | ||||||
|  |       this.map.set(key, new RouterEntryPointImpl(absoluteFile, moduleName)); | ||||||
|  |     } | ||||||
|  |     return this.map.get(key) !; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -2543,7 +2543,7 @@ describe('compiler compliance', () => { | |||||||
|             @Directive({selector: '[some-directive]', exportAs: 'someDir, otherDir'}) |             @Directive({selector: '[some-directive]', exportAs: 'someDir, otherDir'}) | ||||||
|             export class SomeDirective {} |             export class SomeDirective {} | ||||||
| 
 | 
 | ||||||
|             @NgModule({declarations: [SomeDirective, MyComponent]}) |             @NgModule({declarations: [SomeDirective]}) | ||||||
|             export class MyModule{} |             export class MyModule{} | ||||||
|           ` |           ` | ||||||
|         } |         } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user