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
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
|
@ -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…
Reference in New Issue