diff --git a/angular.tsconfig.json b/angular.tsconfig.json index 49e2a2d0a..40475adaf 100644 --- a/angular.tsconfig.json +++ b/angular.tsconfig.json @@ -14,6 +14,7 @@ ], "exclude": [ "node_modules/@angular/bazel/**", + "node_modules/@angular/router/upgrade/**", "node_modules/@angular/compiler-cli/**", "node_modules/@angular/tsc-wrapped/**", "node_modules/@angular/platform-browser/testing/**", diff --git a/modules/common/BUILD.bazel b/modules/common/BUILD.bazel index 32d6852c9..ecb7bf3d2 100644 --- a/modules/common/BUILD.bazel +++ b/modules/common/BUILD.bazel @@ -13,6 +13,7 @@ ng_module( deps = [ "//modules/common/engine", "//modules/common/tokens", + "//modules/common/ls-routes", ], ) @@ -25,6 +26,7 @@ ng_package( ":common", "//modules/common/engine", "//modules/common/tokens", + "//modules/common/ls-routes", ], ) diff --git a/modules/common/ls-routes/BUILD.bazel b/modules/common/ls-routes/BUILD.bazel new file mode 100644 index 000000000..cc9b4da05 --- /dev/null +++ b/modules/common/ls-routes/BUILD.bazel @@ -0,0 +1,33 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library", "ng_module") +load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") + +ng_module( + name = "ls-routes", + srcs = glob([ + "src/*.ts", + "*.ts", + ]), + deps = [ + "//modules/module-map-ngfactory-loader", + ], + module_name = "@nguniversal/common/ls-routes", +) + +ts_library( + name = "unit_test_lib", + testonly = True, + srcs = glob([ + "spec/**/*.spec.ts", + ]), + deps = [ + "//modules/module-map-ngfactory-loader", + ":ls-routes", + ], +) + +jasmine_node_test( + name = "unit_test", + srcs = [":unit_test_lib"], +) diff --git a/modules/common/ls-routes/index.ts b/modules/common/ls-routes/index.ts new file mode 100644 index 000000000..45965af3d --- /dev/null +++ b/modules/common/ls-routes/index.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC 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 * from './public_api'; diff --git a/modules/common/ls-routes/public_api.ts b/modules/common/ls-routes/public_api.ts new file mode 100644 index 000000000..ba03aeea1 --- /dev/null +++ b/modules/common/ls-routes/public_api.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google LLC 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 { lsRoutes } from './src/ls-routes'; diff --git a/modules/common/ls-routes/spec/ls-routes.spec.ts b/modules/common/ls-routes/spec/ls-routes.spec.ts new file mode 100644 index 000000000..6e8445a96 --- /dev/null +++ b/modules/common/ls-routes/spec/ls-routes.spec.ts @@ -0,0 +1,151 @@ +import { lsRoutes } from '@nguniversal/common/ls-routes'; +import { enableProdMode, NgModule, Component, CompilerFactory, Compiler } from '@angular/core'; +import { RouterModule, Route } from '@angular/router'; +import { BrowserModule } from '@angular/platform-browser'; +import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; +import { ServerModule, platformDynamicServer } from '@angular/platform-server'; + +@Component({selector: 'lazy', template: 'lazy'}) +export class LazyComponent {} + +@NgModule({ + imports: [RouterModule.forChild([ + {path: 'lazy-a', component: LazyComponent} + ])], + declarations: [ LazyComponent ] +}) +export class LazyModule {} + +function assignComponent(route: Route, comp: any) { + route.component = comp; + if (route.children) { + route.children = route.children.map(r => assignComponent(r, comp)); + } + return route; +} + + +async function createFactoryAndGetRoutes( + routeConfig: Route[], + compiler: Compiler, + moduleMap: {[key: string]: any} = {} ) { + + @Component({ selector: 'a', template: 'a' }) + class MockComponent { } + + @NgModule({ + imports: [ + BrowserModule, + RouterModule.forRoot(routeConfig.map(r => assignComponent(r, MockComponent))), + ], + declarations: [MockComponent] + }) + class MockModule { } + @NgModule({ + imports: [ + ServerModule, + MockModule, + ModuleMapLoaderModule + ] + }) + class MockServerModule {} + const factory = await compiler.compileModuleAsync(MockServerModule); + + return lsRoutes(factory, moduleMap); +} + +describe('ls-routes', () => { + let compiler: Compiler; + beforeAll(() => { + enableProdMode(); + const compilerFactory = platformDynamicServer() + .injector.get(CompilerFactory) as CompilerFactory; + + compiler = compilerFactory.createCompiler(); + }); + + it('should resolve a single path', async(done) => { + const routes = await createFactoryAndGetRoutes([ + { path: 'a' } + ], compiler); + expect(routes).toContain('/a'); + done(); + }); + xit('should resolve a multiple paths', async(done) => { + const routes = await createFactoryAndGetRoutes([ + { path: 'a' }, + { path: 'b' }, + { path: 'c' }, + ], compiler); + expect(routes).toContain('/a'); + expect(routes).toContain('/b'); + expect(routes).toContain('/c'); + done(); + + }); + xit('should resolve nested paths', async(done) => { + const routes = await createFactoryAndGetRoutes([ + { + path: 'a', + children: [ + { path: 'a-a' }, + { path: 'a-b' } + ] + }, + ], compiler); + expect(routes).toContain('/a/a-a'); + expect(routes).toContain('/a/a-b'); + done(); + }); + xit('should resolve a string loaded loadChildren', async(done) => { + const moduleMap = { './ls-routes.spec.ts#LazyModule': LazyModule }; + const routes = await createFactoryAndGetRoutes([ + { + path: 'a', + loadChildren: './ls-routes.spec.ts#LazyModule' + } + ], compiler, moduleMap); + expect(routes).toContain('/a/lazy-a'); + done(); + }); + xit('should resolve a function loaded loadChildren', async(done) => { + const routes = await createFactoryAndGetRoutes([ + { + path: 'a', + loadChildren: () => compiler.compileModuleSync(LazyModule) + } + ], compiler); + expect(routes).toContain('/a/lazy-a'); + done(); + }); + xit('should resolve a function loaded promise loadChildren', async(done) => { + const routes = await createFactoryAndGetRoutes([ + { + path: 'a', + loadChildren: () => compiler.compileModuleAsync(LazyModule) as any + } + ], compiler); + expect(routes).toContain('/a/lazy-a'); + done(); + + }); + xit('should correctly merge nested routes with empty string ', async(done) => { + const routes = await createFactoryAndGetRoutes([ + { + path: '', + children: [ + { + path: '', + children: [ + { path: '' }, + { path: 'level3'} + ] + } + ] + } + ], compiler); + expect(routes).toContain('/'); + expect(routes).toContain('/level3'); + done(); + }); +}); diff --git a/modules/common/ls-routes/src/ls-routes.ts b/modules/common/ls-routes/src/ls-routes.ts new file mode 100644 index 000000000..076f2cc85 --- /dev/null +++ b/modules/common/ls-routes/src/ls-routes.ts @@ -0,0 +1,107 @@ +/** + * @license + * Copyright Google LLC 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 { NgModuleFactoryLoader, + NgModuleFactory, Injector, NgZone } from '@angular/core'; +import { platformServer } from '@angular/platform-server'; +import { ROUTES, Route } from '@angular/router'; +import { Observable } from 'rxjs'; +import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader'; + +export let loader: NgModuleFactoryLoader; + +export function lsRoutes( + serverFactory: NgModuleFactory, + lazyModuleMap?: any +) { + const ngZone = new NgZone({ enableLongStackTrace: false }); + const rootInjector = Injector.create( + [ + { provide: NgZone, useValue: ngZone }, + provideModuleMap(lazyModuleMap) + ], + platformServer().injector + ); + const moduleRef = serverFactory.create(rootInjector); + loader = moduleRef.injector.get(NgModuleFactoryLoader); + return Promise.all(createModule(serverFactory, rootInjector)) + .then(routes => { + return flattenArray(flattenRouteToPath(routes)); + }); +} + +function flattenArray(array: T[] | T): V[] { + return !Array.isArray(array) ? array : [].concat.apply([], array.map(r => flattenArray(r))); +} + +function flattenRouteToPath(routes: Route[]): (string[] | string)[] { + return routes.map(route => { + if (!route.children) { + return route.path ? '/' + route.path : '/'; + } else { + // extra flatten here for nested routes + return flattenArray(flattenRouteToPath(route.children)) + .map(childRoute => (!route.path ? '' : '/' + route.path) + childRoute); + } + }); +} + +function coerceIntoPromise(mightBePromise: Observable | Promise | T): Promise { + if (mightBePromise instanceof Observable) { + return mightBePromise.toPromise(); + } + return Promise.resolve(mightBePromise); +} + +function extractRoute(route: Route, injector: Injector): Promise { + if (route.loadChildren) { + return resolveLazyChildren(route, injector); + } + if (route.children) { + return Promise.all(route.children.map(r => extractRoute(r, injector))) + .then(routes => { + route.children = routes; + return route; + }); + } + return Promise.resolve(route); +} + +function resolveLazyChildren(route: Route, injector: Injector): Promise { + let nextFactory: Promise>; + if (typeof route.loadChildren === 'function') { + nextFactory = coerceIntoPromise>( + route.loadChildren() as NgModuleFactory | Promise> + ); + } else { + nextFactory = loader.load(route.loadChildren as string); + } + return nextFactory + .then(factory => Promise.all(createModule(factory, injector))) + .then(children => { + route.children = children; + delete route.loadChildren; + return route; + }); +} + +function createModule(factory: NgModuleFactory, parentInjector: Injector): Promise[] { + + const moduleRef = factory.create(parentInjector); + const routes = moduleRef.injector.get(ROUTES); + + return flattenArray(routes) + .map(route => { + if (!route.loadChildren) { + // no lazy loaded paths so we can return the routes directly + return extractRoute(route, parentInjector); + } else { + return resolveLazyChildren(route, moduleRef.injector); + } + }); +} diff --git a/modules/common/package.json b/modules/common/package.json index cea06eaa8..09f5e58fc 100644 --- a/modules/common/package.json +++ b/modules/common/package.json @@ -9,7 +9,9 @@ ], "peerDependencies": { "@angular/common": "NG_VERSION", - "@angular/core": "NG_VERSION" + "@angular/core": "NG_VERSION", + "@angular/router": "NG_VERSION", + "@nguniversal/module-map-ngfactory-loader": "0.0.0-PLACEHOLDER" }, "ng-update": { "packageGroup": "NG_UPDATE_PACKAGE_GROUP" diff --git a/package.json b/package.json index 743ac72fa..be46c6404 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,8 @@ "@angular/platform-browser": "^6.0.4", "@angular/platform-browser-dynamic": "^6.0.4", "@angular/platform-server": "^6.0.4", + "@angular/upgrade": "^6.0.4", + "@angular/router": "^6.0.4", "@bazel/ibazel": "^0.3.1", "@types/express": "^4.0.39", "@types/fs-extra": "^4.0.5", diff --git a/yarn.lock b/yarn.lock index 315326712..b6f4b4a92 100644 --- a/yarn.lock +++ b/yarn.lock @@ -71,6 +71,18 @@ tslib "^1.9.0" xhr2 "^0.1.4" +"@angular/router@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-6.0.4.tgz#81c96dfa42a8c4a218cbd450b1e8959a76ea9595" + dependencies: + tslib "^1.9.0" + +"@angular/upgrade@^6.0.4": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@angular/upgrade/-/upgrade-6.0.4.tgz#b12a016bab98db77bacbe9b1a83c4c2a47e5baf5" + dependencies: + tslib "^1.9.0" + "@bazel/ibazel@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.3.1.tgz#5f02f208f138e581bbdb1534d5c013d7a0ac9799"