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'}) | ||||
|             export class SomeDirective {} | ||||
| 
 | ||||
|             @NgModule({declarations: [SomeDirective, MyComponent]}) | ||||
|             @NgModule({declarations: [SomeDirective]}) | ||||
|             export class MyModule{} | ||||
|           ` | ||||
|         } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user