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:
Alex Rickabaugh 2018-11-16 17:52:55 +01:00
parent 2fc5f002e0
commit da85cee07c
6 changed files with 318 additions and 1 deletions

View 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",
],
)

View 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';

View 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;
}
}

View 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';
}

View 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) !;
}
}

View File

@ -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{}
`
}