diff --git a/packages/compiler-cli/src/ngtsc/routing/BUILD.bazel b/packages/compiler-cli/src/ngtsc/routing/BUILD.bazel new file mode 100644 index 0000000000..7d96c18bd7 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/routing/BUILD.bazel @@ -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", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/routing/index.ts b/packages/compiler-cli/src/ngtsc/routing/index.ts new file mode 100644 index 0000000000..8207371d26 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/routing/index.ts @@ -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 + */ + +/// + +export {LazyRoute, NgModuleRouteAnalyzer} from './src/analyzer'; diff --git a/packages/compiler-cli/src/ngtsc/routing/src/analyzer.ts b/packages/compiler-cli/src/ngtsc/routing/src/analyzer.ts new file mode 100644 index 0000000000..5757055788 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/routing/src/analyzer.ts @@ -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(); + 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; + } +} diff --git a/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts b/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts new file mode 100644 index 0000000000..c5cd9dfe3b --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/routing/src/lazy.ts @@ -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, + args: ReadonlyArray): 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): + ref is NodeReference { + return ref instanceof NodeReference && ts.isMethodDeclaration(ref.node); +} + +function isRouteToken(ref: ResolvedValue): boolean { + return ref instanceof AbsoluteReference && ref.moduleName === '@angular/router' && + ref.symbolName === 'ROUTES'; +} diff --git a/packages/compiler-cli/src/ngtsc/routing/src/route.ts b/packages/compiler-cli/src/ngtsc/routing/src/route.ts new file mode 100644 index 0000000000..740a636d8f --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/routing/src/route.ts @@ -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(); + + 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) !; + } +} diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index 5771a21638..7a6ae03d8c 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -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{} ` }