From 7256d0ede5fbfda5d22cdf09efac5c1a22007da7 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 13 Dec 2016 17:35:06 -0800 Subject: [PATCH] chore(internal API): introduce an internal API for ngtools. (#13415) --- modules/@angular/compiler-cli/index.ts | 4 + .../ngtools_src/app.component.css | 3 + .../ngtools_src/app.component.html | 5 + .../ngtools_src/app.component.ts | 10 + .../integrationtest/ngtools_src/app.module.ts | 28 +++ .../ngtools_src/feature/feature.module.ts | 20 ++ .../feature/lazy-feature.module.ts | 20 ++ .../ngtools_src/feature2/default.module.ts | 20 ++ .../ngtools_src/feature2/feature2.module.ts | 22 ++ .../ngtools_src/lazy.module.ts | 23 +++ .../ngtools_src/tsconfig-build.json | 19 ++ .../integrationtest/test/test_ngtools_api.ts | 162 +++++++++++++++ .../integrationtest/tsconfig-build.json | 1 + .../@angular/compiler-cli/src/ngtools_api.ts | 124 +++++++++++ .../@angular/compiler-cli/src/ngtools_impl.ts | 195 ++++++++++++++++++ scripts/ci-lite/offline_compiler_test.sh | 3 +- 16 files changed, 658 insertions(+), 1 deletion(-) create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.css create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.html create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.ts create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/app.module.ts create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/feature/feature.module.ts create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/feature/lazy-feature.module.ts create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/feature2/default.module.ts create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/feature2/feature2.module.ts create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/lazy.module.ts create mode 100644 modules/@angular/compiler-cli/integrationtest/ngtools_src/tsconfig-build.json create mode 100644 modules/@angular/compiler-cli/integrationtest/test/test_ngtools_api.ts create mode 100644 modules/@angular/compiler-cli/src/ngtools_api.ts create mode 100644 modules/@angular/compiler-cli/src/ngtools_impl.ts diff --git a/modules/@angular/compiler-cli/index.ts b/modules/@angular/compiler-cli/index.ts index 3418eed737..edcf54702e 100644 --- a/modules/@angular/compiler-cli/index.ts +++ b/modules/@angular/compiler-cli/index.ts @@ -11,3 +11,7 @@ export {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter, NodeComp export {Extractor} from './src/extractor'; export * from '@angular/tsc-wrapped'; export {VERSION} from './src/version'; + + +// TODO(hansl): moving to Angular 4 need to update this API. +export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api' diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.css b/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.css new file mode 100644 index 0000000000..5cde7b9223 --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.css @@ -0,0 +1,3 @@ +:host { + background-color: blue; +} diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.html b/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.html new file mode 100644 index 0000000000..5a532db930 --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.html @@ -0,0 +1,5 @@ +
+

hello world

+ lazy + +
diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.ts b/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.ts new file mode 100644 index 0000000000..505501c47a --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.component.ts @@ -0,0 +1,10 @@ +import {Component, ViewEncapsulation} from '@angular/core'; + + +@Component({ + selector: 'app-root', + templateUrl: 'app.component.html', + styleUrls: ['app.component.css'], + encapsulation: ViewEncapsulation.None +}) +export class AppComponent { } diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.module.ts b/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.module.ts new file mode 100644 index 0000000000..b4f15399ea --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/app.module.ts @@ -0,0 +1,28 @@ +import { NgModule, Component } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { RouterModule } from '@angular/router'; +import { AppComponent } from './app.component'; + +@Component({ + selector: 'home-view', + template: 'home!' +}) +export class HomeView {} + + +@NgModule({ + declarations: [ + AppComponent, + HomeView + ], + imports: [ + BrowserModule, + RouterModule.forRoot([ + {path: 'lazy', loadChildren: './lazy.module#LazyModule'}, + {path: 'feature2', loadChildren: 'feature2/feature2.module#Feature2Module'}, + {path: '', component: HomeView} + ]) + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature/feature.module.ts b/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature/feature.module.ts new file mode 100644 index 0000000000..f464ca028b --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature/feature.module.ts @@ -0,0 +1,20 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({ + selector: 'feature-component', + template: 'foo.html' +}) +export class FeatureComponent {} + +@NgModule({ + declarations: [ + FeatureComponent + ], + imports: [ + RouterModule.forChild([ + { path: '', component: FeatureComponent} + ]) + ] +}) +export class FeatureModule {} diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature/lazy-feature.module.ts b/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature/lazy-feature.module.ts new file mode 100644 index 0000000000..40a80ed122 --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature/lazy-feature.module.ts @@ -0,0 +1,20 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({ + selector: 'lazy-feature-comp', + template: 'lazy feature!' +}) +export class LazyFeatureComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyFeatureComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature.module#FeatureModule'} + ]) + ], + declarations: [LazyFeatureComponent] +}) +export class LazyFeatureModule { +} diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature2/default.module.ts b/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature2/default.module.ts new file mode 100644 index 0000000000..cb60e490c5 --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature2/default.module.ts @@ -0,0 +1,20 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({ + selector: 'feature-component', + template: 'foo.html' +}) +export class FeatureComponent {} + +@NgModule({ + declarations: [ + FeatureComponent + ], + imports: [ + RouterModule.forChild([ + { path: '', component: FeatureComponent }, + ]) + ] +}) +export default class DefaultModule {} diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature2/feature2.module.ts b/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature2/feature2.module.ts new file mode 100644 index 0000000000..f22d5ee857 --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/feature2/feature2.module.ts @@ -0,0 +1,22 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({ + selector: 'feature-component', + template: 'foo.html' +}) +export class FeatureComponent {} + +@NgModule({ + declarations: [ + FeatureComponent + ], + imports: [ + RouterModule.forChild([ + { path: '', component: FeatureComponent }, + { path: 'd', loadChildren: './default.module' } + { path: 'e', loadChildren: 'feature/feature.module#FeatureModule' } + ]) + ] +}) +export class Feature2Module {} diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/lazy.module.ts b/modules/@angular/compiler-cli/integrationtest/ngtools_src/lazy.module.ts new file mode 100644 index 0000000000..803b4ad549 --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/lazy.module.ts @@ -0,0 +1,23 @@ +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({ + selector: 'lazy-comp', + template: 'lazy!' +}) +export class LazyComponent {} + +@NgModule({ + imports: [ + RouterModule.forChild([ + {path: '', component: LazyComponent, pathMatch: 'full'}, + {path: 'feature', loadChildren: './feature/feature.module#FeatureModule'}, + {path: 'lazy-feature', loadChildren: './feature/lazy-feature.module#LazyFeatureModule'} + ]) + ], + declarations: [LazyComponent] +}) +export class LazyModule { +} + +export class SecondModule {} diff --git a/modules/@angular/compiler-cli/integrationtest/ngtools_src/tsconfig-build.json b/modules/@angular/compiler-cli/integrationtest/ngtools_src/tsconfig-build.json new file mode 100644 index 0000000000..f5a1f98c21 --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/ngtools_src/tsconfig-build.json @@ -0,0 +1,19 @@ +{ + "angularCompilerOptions": { + // For TypeScript 1.8, we have to lay out generated files + // in the same source directory with your code. + "genDir": ".", + "debug": true + }, + + "compilerOptions": { + "target": "es5", + "experimentalDecorators": true, + "noImplicitAny": true, + "moduleResolution": "node", + "rootDir": "", + "declaration": true, + "lib": ["es6", "dom"], + "baseUrl": "." + } +} \ No newline at end of file diff --git a/modules/@angular/compiler-cli/integrationtest/test/test_ngtools_api.ts b/modules/@angular/compiler-cli/integrationtest/test/test_ngtools_api.ts new file mode 100644 index 0000000000..2dc94c166b --- /dev/null +++ b/modules/@angular/compiler-cli/integrationtest/test/test_ngtools_api.ts @@ -0,0 +1,162 @@ +#!/usr/bin/env node +/** + * @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 + */ +/* tslint:disable:no-console */ + +// Must be imported first, because angular2 decorators throws on load. +import 'reflect-metadata'; + +import * as path from 'path'; +import * as ts from 'typescript'; +import * as assert from 'assert'; +import {tsc} from '@angular/tsc-wrapped/src/tsc'; +import { + AngularCompilerOptions, + CodeGenerator, + CompilerHostContext, + NodeCompilerHostContext, + __NGTOOLS_PRIVATE_API_2 +} from '@angular/compiler-cli'; + +const glob = require('glob'); + + +/** + * Main method. + * Standalone program that executes codegen using the ngtools API and tests that files were + * properly read and wrote. + */ +function main() { + console.log(`testing ngtools API...`); + + Promise.resolve() + .then(() => codeGenTest()) + .then(() => lazyRoutesTest()) + .then(() => { + console.log('All done!'); + process.exit(0); + }) + .catch((err) => { + console.error(err.stack); + console.error('Test failed'); + process.exit(1); + }); +} + + +function codeGenTest() { + const basePath = path.join(__dirname, '../ngtools_src'); + const project = path.join(basePath, 'tsconfig-build.json'); + const readResources: string[] = []; + const wroteFiles: string[] = []; + + const config = tsc.readConfiguration(project, basePath); + const hostContext = new NodeCompilerHostContext(); + const delegateHost = ts.createCompilerHost(config.parsed.options, true); + const host: ts.CompilerHost = Object.assign({}, delegateHost, { + writeFile: (fileName: string, ...rest: any[]) => { + wroteFiles.push(fileName); + return delegateHost.writeFile.call(delegateHost, fileName, ...rest); + } + }); + const program = ts.createProgram(config.parsed.fileNames, config.parsed.options, host); + + config.ngOptions.basePath = basePath; + + console.log(`>>> running codegen for ${project}`); + return __NGTOOLS_PRIVATE_API_2.codeGen({ + basePath, + compilerOptions: config.parsed.options, + program, + host, + + angularCompilerOptions: config.ngOptions, + + // i18n options. + i18nFormat: null, + i18nFile: null, + locale: null, + + readResource: (fileName: string) => { + readResources.push(fileName); + return hostContext.readResource(fileName); + } + }) + .then(() => { + console.log(`>>> codegen done, asserting read and wrote files`); + + // Assert for each file that it has been read and each `ts` has a written file associated. + const allFiles = glob.sync(path.join(basePath, '**/*'), { nodir: true }); + + allFiles.forEach((fileName: string) => { + // Skip tsconfig. + if (fileName.match(/tsconfig-build.json$/)) { + return; + } + + // Assert that file was read. + if (fileName.match(/\.module\.ts$/)) { + const factory = fileName.replace(/\.module\.ts$/, '.module.ngfactory.ts'); + assert(wroteFiles.indexOf(factory) != -1, `Expected file "${factory}" to be written.`); + } else if (fileName.match(/\.css$/) || fileName.match(/\.html$/)) { + assert(readResources.indexOf(fileName) != -1, `Expected resource "${fileName}" to be read.`); + } + }); + + console.log(`done, no errors.`); + }) + .catch((e: any) => { + console.error(e.stack); + console.error('Compilation failed'); + throw e; + }); +} + + +function lazyRoutesTest() { + const basePath = path.join(__dirname, '../ngtools_src'); + const project = path.join(basePath, 'tsconfig-build.json'); + + const config = tsc.readConfiguration(project, basePath); + const host = ts.createCompilerHost(config.parsed.options, true); + const program = ts.createProgram(config.parsed.fileNames, config.parsed.options, host); + + config.ngOptions.basePath = basePath; + + const lazyRoutes = __NGTOOLS_PRIVATE_API_2.listLazyRoutes({ + program, + host, + angularCompilerOptions: config.ngOptions, + entryModule: 'app.module#AppModule' + }); + + const expectations: {[route: string]: string} = { + './lazy.module#LazyModule': 'lazy.module.ts', + './feature/feature.module#FeatureModule': 'feature/feature.module.ts', + './feature/lazy-feature.module#LazyFeatureModule': 'feature/lazy-feature.module.ts', + 'feature2/feature2.module#Feature2Module': 'feature2/feature2.module.ts', + './default.module': 'feature2/default.module.ts', + 'feature/feature.module#FeatureModule': 'feature/feature.module.ts' + }; + + Object.keys(lazyRoutes).forEach((route: string) => { + assert(route in expectations, `Found a route that was not expected: "${route}".`); + assert(lazyRoutes[route] == path.join(basePath, expectations[route]), + `Route "${route}" does not point to the expected absolute path ` + + `"${path.join(basePath, expectations[route])}". It points to "${lazyRoutes[route]}"`); + }); + + // Verify that all expectations were met. + assert.deepEqual(Object.keys(lazyRoutes), Object.keys(expectations), + `Expected routes listed to be: \n` + + ` ${JSON.stringify(Object.keys(expectations))}\n` + + `Actual:\n` + + ` ${JSON.stringify(Object.keys(lazyRoutes))}\n`); +} + +main(); diff --git a/modules/@angular/compiler-cli/integrationtest/tsconfig-build.json b/modules/@angular/compiler-cli/integrationtest/tsconfig-build.json index 260f3090b3..c1125b2044 100644 --- a/modules/@angular/compiler-cli/integrationtest/tsconfig-build.json +++ b/modules/@angular/compiler-cli/integrationtest/tsconfig-build.json @@ -21,6 +21,7 @@ "src/module", "src/bootstrap", "test/all_spec", + "test/test_ngtools_api", "test/test_summaries", "benchmarks/src/tree/ng2/index_aot.ts", "benchmarks/src/tree/ng2_switch/index_aot.ts", diff --git a/modules/@angular/compiler-cli/src/ngtools_api.ts b/modules/@angular/compiler-cli/src/ngtools_api.ts new file mode 100644 index 0000000000..4e7730c9b7 --- /dev/null +++ b/modules/@angular/compiler-cli/src/ngtools_api.ts @@ -0,0 +1,124 @@ +/** + * This is a private API for the ngtools toolkit. + * + * This API should be stable for NG 2. It can be removed in NG 4..., but should be replaced by + * something else. + */ + +import * as ts from 'typescript'; +import {StaticReflector, AotCompilerHost} from '@angular/compiler'; +import {AngularCompilerOptions, NgcCliOptions} from '@angular/tsc-wrapped'; + +import {CodeGenerator} from './codegen'; +import {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter} from './compiler_host'; +import {listLazyRoutesOfModule} from './ngtools_impl'; +import {PathMappedCompilerHost} from './path_mapped_compiler_host'; + + +export interface NgTools_InternalApi_NG2_CodeGen_Options { + basePath: string; + compilerOptions: ts.CompilerOptions; + program: ts.Program; + host: ts.CompilerHost; + + angularCompilerOptions: AngularCompilerOptions; + + // i18n options. + i18nFormat: string; + i18nFile: string; + locale: string; + + readResource: (fileName: string) => Promise; + + // Every new property under this line should be optional. +} + +export interface NgTools_InternalApi_NG2_ListLazyRoutes_Options { + program: ts.Program; + host: ts.CompilerHost; + angularCompilerOptions: AngularCompilerOptions; + entryModule: string; + + // Every new property under this line should be optional. +} + + +export interface NgTools_InternalApi_NG_2_LazyRouteMap { + [route: string]: string; +} + + +/** + * A ModuleResolutionHostAdapter that overrides the readResource() method with the one + * passed in the interface. + */ +class CustomLoaderModuleResolutionHostAdapter extends ModuleResolutionHostAdapter { + constructor(private _readResource: (path: string) => Promise, + host: ts.ModuleResolutionHost) { + super(host); + } + + readResource(path: string) { + return this._readResource(path); + } +} + + +/** + * @internal + * @private + */ +export class NgTools_InternalApi_NG_2 { + /** + * @internal + * @private + */ + static codeGen(options: NgTools_InternalApi_NG2_CodeGen_Options): Promise { + const hostContext: CompilerHostContext = new CustomLoaderModuleResolutionHostAdapter( + options.readResource, options.host + ); + const cliOptions: NgcCliOptions = { + i18nFormat: options.i18nFormat, + i18nFile: options.i18nFile, + locale: options.locale, + basePath: options.basePath + }; + + // Create the Code Generator. + const codeGenerator = CodeGenerator.create( + options.angularCompilerOptions, + cliOptions, + options.program, + options.host, + hostContext + ); + + return codeGenerator.codegen(); + } + + + /** + * @internal + * @private + */ + static listLazyRoutes(options: NgTools_InternalApi_NG2_ListLazyRoutes_Options) + : NgTools_InternalApi_NG_2_LazyRouteMap { + const angularCompilerOptions = options.angularCompilerOptions; + const program = options.program; + + const moduleResolutionHost = new ModuleResolutionHostAdapter(options.host); + const usePathMapping = !!angularCompilerOptions.rootDirs && angularCompilerOptions.rootDirs.length > 0; + const ngCompilerHost: AotCompilerHost = usePathMapping + ? new PathMappedCompilerHost(program, angularCompilerOptions, moduleResolutionHost) + : new CompilerHost(program, angularCompilerOptions, moduleResolutionHost); + + const staticReflector = new StaticReflector(ngCompilerHost); + const routeMap = listLazyRoutesOfModule(options.entryModule, ngCompilerHost, staticReflector); + + return Object.keys(routeMap) + .reduce((acc: NgTools_InternalApi_NG_2_LazyRouteMap, route: string) => { + acc[route] = routeMap[route].absoluteFilePath; + return acc; + }, {}); + } +} diff --git a/modules/@angular/compiler-cli/src/ngtools_impl.ts b/modules/@angular/compiler-cli/src/ngtools_impl.ts new file mode 100644 index 0000000000..3f29b8d03b --- /dev/null +++ b/modules/@angular/compiler-cli/src/ngtools_impl.ts @@ -0,0 +1,195 @@ +/** + * This is a private API for the ngtools toolkit. + * + * This API should be stable for NG 2. It can be removed in NG 4..., but should be replaced by + * something else. + */ +import {NgModule} from '@angular/core'; +import {StaticSymbol, StaticReflector, AotCompilerHost} from '@angular/compiler'; + + +// We cannot depend directly to @angular/router. +type Route = any; +const ROUTER_MODULE_PATH = '@angular/router/src/router_config_loader'; +const ROUTER_ROUTES_SYMBOL_NAME = 'ROUTES'; + + +// LazyRoute information between the extractors. +export interface LazyRoute { + routeDef: RouteDef; + absoluteFilePath: string; +} +export type LazyRouteMap = {[route: string]: LazyRoute}; + +// A route definition. Normally the short form 'path/to/module#ModuleClassName' is used by +// the user, and this is a helper class to extract information from it. +export class RouteDef { + private constructor(public readonly path: string, public readonly className: string = null) {} + + toString() { + return (this.className === null || this.className == 'default') + ? this.path + : `${this.path}#${this.className}`; + } + + static fromString(entry: string): RouteDef { + const split = entry.split('#'); + return new RouteDef(split[0], split[1] || null); + } +} + + +/** + * + * @returns {LazyRouteMap} + * @private + */ +export function listLazyRoutesOfModule(entryModule: string, + host: AotCompilerHost, + reflector: StaticReflector): LazyRouteMap { + const entryRouteDef = RouteDef.fromString(entryModule); + const containingFile = _resolveModule(entryRouteDef.path, entryRouteDef.path, host); + const modulePath = `./${containingFile.replace(/^(.*)\//, '')}`; + const className = entryRouteDef.className; + + // List loadChildren of this single module. + const staticSymbol = reflector.findDeclaration(modulePath, className, containingFile); + const ROUTES = reflector.findDeclaration(ROUTER_MODULE_PATH, ROUTER_ROUTES_SYMBOL_NAME); + const lazyRoutes: LazyRoute[] = _extractLazyRoutesFromStaticModule( + staticSymbol, reflector, host, ROUTES); + const routes: LazyRouteMap = {}; + + lazyRoutes + .forEach((lazyRoute: LazyRoute) => { + const route: string = lazyRoute.routeDef.toString(); + _assertRoute(routes, lazyRoute); + routes[route] = lazyRoute; + + const lazyModuleSymbol = reflector.findDeclaration( + lazyRoute.absoluteFilePath, lazyRoute.routeDef.className || 'default'); + const subRoutes = _extractLazyRoutesFromStaticModule( + lazyModuleSymbol, reflector, host, ROUTES); + + // Populate the map using the routes we just found. + subRoutes.forEach(subRoute => { + _assertRoute(routes, subRoute); + routes[subRoute.routeDef.toString()] = subRoute; + }); + }); + + return routes; +} + + +/** + * Try to resolve a module, and returns its absolute path. + * @private + */ +function _resolveModule(modulePath: string, containingFile: string, host: AotCompilerHost) { + const result = host.moduleNameToFileName(modulePath, containingFile); + if (!result) { + throw new Error(`Could not resolve "${modulePath}" from "${containingFile}".`); + } + return result; +} + + +/** + * Throw an exception if a route is in a route map, but does not point to the same module. + * @private + */ +function _assertRoute(map: LazyRouteMap, route: LazyRoute) { + const r = route.routeDef.toString(); + if (map[r] && map[r].absoluteFilePath != route.absoluteFilePath) { + throw new Error(`Duplicated path in loadChildren detected: "${r}" is used in 2 loadChildren, ` + + `but they point to different modules "(${map[r].absoluteFilePath} and ` + + `"${route.absoluteFilePath}"). Webpack cannot distinguish on context and would fail to ` + + 'load the proper one.'); + } +} + + +/** + * Extract all the LazyRoutes from a module. This extracts all `loadChildren` keys from this + * module and all statically referred modules. + * @private + */ +function _extractLazyRoutesFromStaticModule(staticSymbol: StaticSymbol, + reflector: StaticReflector, + host: AotCompilerHost, + ROUTES: StaticSymbol): LazyRoute[] { + const moduleMetadata = _getNgModuleMetadata(staticSymbol, reflector); + const allRoutes: any = (moduleMetadata.imports || []) + .filter(i => 'providers' in i) + .reduce((mem: Route[], m: any) => { + return mem.concat(_collectRoutes(m.providers || [], reflector, ROUTES)); + }, _collectRoutes(moduleMetadata.providers || [], reflector, ROUTES)); + + const lazyRoutes: LazyRoute[] = _collectLoadChildren(allRoutes) + .reduce((acc: LazyRoute[], route: string) => { + const routeDef = RouteDef.fromString(route); + const absoluteFilePath = _resolveModule(routeDef.path, staticSymbol.filePath, host); + acc.push({ routeDef, absoluteFilePath }); + return acc; + }, []); + + const importedSymbols = ((moduleMetadata.imports || []) as any[]) + .filter(i => i instanceof StaticSymbol) as StaticSymbol[]; + + return importedSymbols + .reduce((acc: LazyRoute[], i: StaticSymbol) => { + return acc.concat(_extractLazyRoutesFromStaticModule( + i, reflector, host, ROUTES)); + }, []) + .concat(lazyRoutes); +} + + +/** + * Get the NgModule Metadata of a symbol. + * @private + */ +function _getNgModuleMetadata(staticSymbol: StaticSymbol, reflector: StaticReflector): NgModule { + const ngModules = reflector.annotations(staticSymbol).filter((s: any) => s instanceof NgModule); + if (ngModules.length === 0) { + throw new Error(`${staticSymbol.name} is not an NgModule`); + } + return ngModules[0]; +} + + +/** + * Return the routes from the provider list. + * @private + */ +function _collectRoutes(providers: any[], reflector: StaticReflector, + ROUTES: StaticSymbol): Route[] { + return providers.reduce((routeList: Route[], p: any) => { + if (p.provide === ROUTES) { + return routeList.concat(p.useValue); + } else if (Array.isArray(p)) { + return routeList.concat(_collectRoutes(p, reflector, ROUTES)); + } else { + return routeList; + } + }, []); +} + + +/** + * Return the loadChildren values of a list of Route. + * @private + */ +function _collectLoadChildren(routes: Route[]): string[] { + return routes.reduce((m, r) => { + if (r.loadChildren) { + return m.concat(r.loadChildren); + } else if (Array.isArray(r)) { + return m.concat(_collectLoadChildren(r)); + } else if (r.children) { + return m.concat(_collectLoadChildren(r.children)); + } else { + return m; + } + }, []); +} diff --git a/scripts/ci-lite/offline_compiler_test.sh b/scripts/ci-lite/offline_compiler_test.sh index dcb056b15d..32d9bdc352 100755 --- a/scripts/ci-lite/offline_compiler_test.sh +++ b/scripts/ci-lite/offline_compiler_test.sh @@ -3,7 +3,7 @@ set -ex -o pipefail # These ones can be `npm link`ed for fast development LINKABLE_PKGS=( - $(pwd)/dist/packages-dist/{common,forms,core,compiler,compiler-cli,platform-{browser,server},platform-browser-dynamic} + $(pwd)/dist/packages-dist/{common,forms,core,compiler,compiler-cli,platform-{browser,server},platform-browser-dynamic,router} $(pwd)/dist/tools/@angular/tsc-wrapped ) PKGS=( @@ -51,6 +51,7 @@ cp -v package.json $TMP ./node_modules/.bin/ng-xi18n -p tsconfig-build.json --i18nFormat=xmb node test/test_summaries.js + node test/test_ngtools_api.js ./node_modules/.bin/jasmine init # Run compiler-cli integration tests in node