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