From b237c203921ff9f04e8e11060b90c8a2f6f0b365 Mon Sep 17 00:00:00 2001 From: Fabian Wiles Date: Sun, 6 Aug 2017 19:07:19 +1200 Subject: [PATCH] feat(ls-routes): introduce ls-routes --- modules/common/BUILD.bazel | 8 +- modules/common/ls-routes/BUILD.bazel | 31 ++++ modules/common/ls-routes/README.md | 24 +++ modules/common/ls-routes/index.ts | 8 + modules/common/ls-routes/package.json | 3 + modules/common/ls-routes/public_api.ts | 8 + .../common/ls-routes/spec/ls-routes.spec.ts | 161 ++++++++++++++++++ modules/common/ls-routes/src/index.ts | 8 + modules/common/ls-routes/src/ls-routes.ts | 118 +++++++++++++ modules/common/ls-routes/tsconfig.json | 12 ++ modules/common/ls-routes/tsconfig.lib.json | 40 +++++ modules/common/ls-routes/tsconfig.spec.json | 19 +++ .../module-map-ngfactory-loader/BUILD.bazel | 3 +- package.json | 8 +- tools/defaults.bzl | 1 + tools/package-tools/rollup-globals.ts | 4 + tsconfig.json | 5 +- yarn.lock | 12 ++ 18 files changed, 467 insertions(+), 6 deletions(-) create mode 100644 modules/common/ls-routes/BUILD.bazel create mode 100644 modules/common/ls-routes/README.md create mode 100644 modules/common/ls-routes/index.ts create mode 100644 modules/common/ls-routes/package.json create mode 100644 modules/common/ls-routes/public_api.ts create mode 100644 modules/common/ls-routes/spec/ls-routes.spec.ts create mode 100644 modules/common/ls-routes/src/index.ts create mode 100644 modules/common/ls-routes/src/ls-routes.ts create mode 100644 modules/common/ls-routes/tsconfig.json create mode 100644 modules/common/ls-routes/tsconfig.lib.json create mode 100644 modules/common/ls-routes/tsconfig.spec.json diff --git a/modules/common/BUILD.bazel b/modules/common/BUILD.bazel index 5e3568278..585e9b026 100644 --- a/modules/common/BUILD.bazel +++ b/modules/common/BUILD.bazel @@ -12,9 +12,13 @@ ng_module( ng_package( name = "@nguniversal/common", - srcs = [":package.json"], + srcs = [ + ":package.json", + "//modules/common/ls-routes:package.json", + ], entry_point = "index.js", - deps = [":common_module"], + deps = [":common_module", "//modules/common/ls-routes"], + secondary_entry_points = ["ls-routes"], ) ts_library( name = "unit_test_lib", diff --git a/modules/common/ls-routes/BUILD.bazel b/modules/common/ls-routes/BUILD.bazel new file mode 100644 index 000000000..76c2f389c --- /dev/null +++ b/modules/common/ls-routes/BUILD.bazel @@ -0,0 +1,31 @@ +package(default_visibility = ["//modules/common:__subpackages__"]) + +load("//tools:defaults.bzl", "ts_library", "ng_module", "ng_package") +load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") + +exports_files(["package.json"]) + +ng_module( + name = "ls-routes", + srcs = glob([ + "*.ts", + "src/**/*.ts", + ]), + # deps = ["//modules/module-map-ngfactory-loader:module-map-ngfactory-loader_module"], +) + +# ts_library( +# name = "unit_test_lib", +# srcs = glob([ +# "spec/**/*.spec.ts" +# ]), +# testonly=True, +# deps = [ +# ":common_module" +# ], +# ) + +# jasmine_node_test( +# name = "unit_test", +# srcs = [":unit_test_lib"], +# ) diff --git a/modules/common/ls-routes/README.md b/modules/common/ls-routes/README.md new file mode 100644 index 000000000..bf95c1905 --- /dev/null +++ b/modules/common/ls-routes/README.md @@ -0,0 +1,24 @@ +This is a tool which will gather the routes from a build factory bundle and return them ready to be used with stamping out prerendered index.html files + +```js +import { lsRoutes } from '@nguniversal/ls-routes'; + +const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./main.a5d2e81ce51e0c3ba3c8.bundle.js') + +lsRoutes( + 'flatPaths', + AppServerModuleNgFactory, + LAZY_MODULE_MAP +).then(paths => { + paths.filter(path => !path.includes(':')) + .forEach(path => { + renderModuleFactory(AppServerModuleNgFactory, { + document: index, + url: path, + extraProviders: [ + provideModuleMap(LAZY_MODULE_MAP) + ] + }) + .then(html => fs.writeFileSync(`dist/${path.replace(/\//g, '-')}.index.html`, html)) + }) +}) \ No newline at end of file diff --git a/modules/common/ls-routes/index.ts b/modules/common/ls-routes/index.ts new file mode 100644 index 000000000..61908996f --- /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 './src/index'; diff --git a/modules/common/ls-routes/package.json b/modules/common/ls-routes/package.json new file mode 100644 index 000000000..6450c5b5c --- /dev/null +++ b/modules/common/ls-routes/package.json @@ -0,0 +1,3 @@ +{ + "name": "@nguniversal/common/ls-routes" +} diff --git a/modules/common/ls-routes/public_api.ts b/modules/common/ls-routes/public_api.ts new file mode 100644 index 000000000..1babee663 --- /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 * from './index'; 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..41621a605 --- /dev/null +++ b/modules/common/ls-routes/spec/ls-routes.spec.ts @@ -0,0 +1,161 @@ +import { lsRoutes } from '../src/ls-routes'; +import { enableProdMode, NgModule, Component, CompilerFactory, Compiler } from '@angular/core'; +import { async } from '@angular/core/testing'; +import { ResourceLoader } from '@angular/compiler'; +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'; + +import * as fs from 'fs'; + +class FileLoader implements ResourceLoader { + get(url: string): string { + return fs.readFileSync(url).toString(); + } +} + +@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; +} + +function createTestingFactory(routeConfig: Route[], compiler: Compiler) { + @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 {} + return compiler.compileModuleAsync(MockServerModule); +} +function createFactoryAndGetRoutes(routeConfig: Route[], compiler: Compiler, moduleMap: {[key: string]: any} = {} ) { + // make it as easy as possible + return createTestingFactory(routeConfig, compiler) + .then(factory => lsRoutes('flatPaths', factory, moduleMap)); +} + +describe('ls-routes', () => { + let compiler: Compiler; + beforeAll(() => { + enableProdMode(); + const compilerFactory = platformDynamicServer().injector.get(CompilerFactory); + compiler = compilerFactory.createCompiler([ + { + providers: [ + { provide: ResourceLoader, useClass: FileLoader, deps: [] } + ] + } + ]); + }) + + it('should resolve a single path', async(() => { + createFactoryAndGetRoutes([ + { path: 'a' } + ], compiler).then(routes => { + expect(routes).toContain('/a'); + }); + })); + it('should resolve a multiple paths', async(() => { + createFactoryAndGetRoutes([ + { path: 'a' }, + { path: 'b' }, + { path: 'c' }, + ], compiler).then(routes => { + expect(routes).toContain('/a'); + expect(routes).toContain('/b'); + expect(routes).toContain('/c'); + }); + })); + it('should resolve nested paths', async(() => { + createFactoryAndGetRoutes([ + { + path: 'a', + children: [ + { path: 'a-a' }, + { path: 'a-b' } + ] + }, + ], compiler).then(routes => { + expect(routes).toContain('/a/a-a'); + expect(routes).toContain('/a/a-b'); + }); + })); + it('should resolve a string loaded loadChildren', async(() => { + const moduleMap = { './ls-routes.spec.ts#LazyModule': LazyModule }; + createFactoryAndGetRoutes([ + { + path: 'a', + loadChildren: './ls-routes.spec.ts#LazyModule' + } + ], compiler, moduleMap).then(routes => { + expect(routes).toContain('/a/lazy-a'); + }); + })); + it('should resolve a function loaded loadChildren', async(() => { + createFactoryAndGetRoutes([ + { + path: 'a', + loadChildren: () => compiler.compileModuleSync(LazyModule) + } + ], compiler).then(routes => { + expect(routes).toContain('/a/lazy-a'); + }); + })); + it('should resolve a function loaded promise loadChildren', async(() => { + createFactoryAndGetRoutes([ + { + path: 'a', + loadChildren: () => compiler.compileModuleAsync(LazyModule) as any + } + ], compiler).then(routes => { + expect(routes).toContain('/a/lazy-a'); + }); + })); + it('should correctly merge nested routes with empty string ', async(() => { + createFactoryAndGetRoutes([ + { + path: '', + children: [ + { + path: '', + children: [ + { path: '' }, + { path: 'level3'} + ] + } + ] + } + ], compiler).then(routes => { + expect(routes).toContain('/'); + expect(routes).toContain('/level3'); + }); + })); +}); diff --git a/modules/common/ls-routes/src/index.ts b/modules/common/ls-routes/src/index.ts new file mode 100644 index 000000000..5faeb9c95 --- /dev/null +++ b/modules/common/ls-routes/src/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 './ls-routes'; 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..44f8704ed --- /dev/null +++ b/modules/common/ls-routes/src/ls-routes.ts @@ -0,0 +1,118 @@ +/** + * @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 'zone.js/dist/zone-node'; +import { ReflectiveInjector, NgModuleFactoryLoader, + NgModuleFactory, Injector, NgZone } from '@angular/core'; +import { platformServer } from '@angular/platform-server'; +import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader'; +import { ROUTES, Route } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; + +let loader: NgModuleFactoryLoader; + +export function lsRoutes( + returnType: 'flatPaths' | 'nestedPaths' | 'full', + serverFactory: NgModuleFactory, + // lazyModuleMap?: any +) { + const ngZone = new NgZone({ enableLongStackTrace: false }); + const rootInjector = ReflectiveInjector.resolveAndCreate( + [ + { 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 => { + if (returnType === 'full') { + return routes; + } + if (returnType === 'nestedPaths') { + return flattenRouteToPath(routes); + } + if (returnType === 'flatPaths') { + return flattenArray(flattenRouteToPath(routes)); + } + throw new Error('you must provide a supported returnType'); + }); +} + +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/ls-routes/tsconfig.json b/modules/common/ls-routes/tsconfig.json new file mode 100644 index 000000000..7f50f20bb --- /dev/null +++ b/modules/common/ls-routes/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/ls-routes" + }, + "angularCompilerOptions": { + "genDir": "ngfactory" + }, + "files": [ + "index.ts" + ] +} diff --git a/modules/common/ls-routes/tsconfig.lib.json b/modules/common/ls-routes/tsconfig.lib.json new file mode 100644 index 000000000..e82c38b6c --- /dev/null +++ b/modules/common/ls-routes/tsconfig.lib.json @@ -0,0 +1,40 @@ + +{ + "compilerOptions": { + "declaration": true, + "stripInternal": false, + "experimentalDecorators": true, + "noUnusedParameters": true, + "strictNullChecks": true, + "importHelpers": true, + "newLine": "lf", + "module": "es2015", + "moduleResolution": "node", + "outDir": "../../dist/packages/ls-routes", + "rootDir": ".", + "rootDirs": [ + ".", + "../../dist/packages/ls-routes" + ], + "paths": { + "@nguniversal/module-map-ngfactory-loader": ["../../../dist/bin/modules/module-map-ngfactory-loader"] + }, + "sourceMap": true, + "inlineSources": true, + "target": "es2015", + "lib": ["es2015", "dom"], + "skipLibCheck": true, + "types": [], + "baseUrl": "." + }, + "files": [ + "./public-api.ts" + ], + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "strictMetadataEmit": true, + "skipTemplateCodegen": true, + "flatModuleOutFile": "index.js", + "flatModuleId": "@nguniversal/ls-routes" + } +} \ No newline at end of file diff --git a/modules/common/ls-routes/tsconfig.spec.json b/modules/common/ls-routes/tsconfig.spec.json new file mode 100644 index 000000000..9cd71d3d7 --- /dev/null +++ b/modules/common/ls-routes/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.lib", + "compilerOptions": { + "importHelpers": false, + "module": "commonjs", + "target": "es5", + "types": ["jasmine"] + }, + "angularCompilerOptions": { + "strictMetadataEmit": true, + "skipTemplateCodegen": true, + "emitDecoratorMetadata": true, + "fullTemplateTypeCheck": true + }, + "include": [ + "**/*.spec.ts", + "index.ts" + ] +} \ No newline at end of file diff --git a/modules/module-map-ngfactory-loader/BUILD.bazel b/modules/module-map-ngfactory-loader/BUILD.bazel index 042f7c893..1ddfb4d95 100644 --- a/modules/module-map-ngfactory-loader/BUILD.bazel +++ b/modules/module-map-ngfactory-loader/BUILD.bazel @@ -1,12 +1,13 @@ load("//tools:defaults.bzl", "ts_library", "ng_module", "ng_package") load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") - +package(default_visibility = ["//modules:__subpackages__"]) ng_module( name = "module-map-ngfactory-loader_module", srcs = glob([ "*.ts", "src/**/*.ts", ]), + # module_name = "@nguniversal/module-map-ngfactory-loader", deps = [], ) diff --git a/package.json b/package.json index f2037656f..0fecfcaec 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "karma-typescript": "^3.0.12", "minimatch": "^3.0.4", "protractor": "^5.2.0", + "replace-in-file": "^3.1.1", "rimraf": "^2.6.1", "rollup": "~0.41.6", "rollup-plugin-alias": "^1.4.0", @@ -109,14 +110,17 @@ "rollup-plugin-sourcemaps": "^0.4.2", "rollup-plugin-uglify": "^2.0.1", "rxjs": "^5.5.6", + "standard-version": "^4.3.0", "systemjs": "0.19.43", "ts-node": "^3.0.4", "tsconfig-paths": "^2.3.0", "tslint": "^5.9.1", "tsutils": "^2.21.2", "typescript": "~2.6.2", - "standard-version": "^4.3.0", - "replace-in-file": "^3.1.1", "zone.js": "^0.8.12" + }, + "dependencies": { + "@angular/router": "^5.2.7", + "@angular/upgrade": "^5.2.8" } } diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 53b878b78..9e14eb046 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -21,5 +21,6 @@ def ng_package(globals = None, **kwargs): "rxjs/operators/filter": "Rx.Observable.prototype", "rxjs/operators/take": "Rx.Observable.prototype", "rxjs/operators/tap": "Rx.Observable.prototype", + "@nguniversal/module-map-ngfactory-loader": "nguniversal.module-map-ngfactory-loader" } _ng_package(globals = globals, **kwargs) diff --git a/tools/package-tools/rollup-globals.ts b/tools/package-tools/rollup-globals.ts index f168c16df..f609087a1 100644 --- a/tools/package-tools/rollup-globals.ts +++ b/tools/package-tools/rollup-globals.ts @@ -5,6 +5,8 @@ export const rollupGlobals = { // See rollup/rollup/wiki/JavaScript-API#globals for more. '@angular/animations': 'ng.animations', '@angular/core': 'ng.core', + '@angular/core/testing': 'ng.core.testing', + '@angular/router': 'ng.router', '@angular/common': 'ng.common', '@angular/common/http': 'ng.common.http', '@angular/compiler': 'ng.compiler', @@ -12,12 +14,14 @@ export const rollupGlobals = { '@angular/platform-browser': 'ng.platformBrowser', '@angular/platform-server': 'ng.platformServer', '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic', + '@nguniversal/module-map-ngfactory-loader': 'nguniversal.moduleMapNgfactoryLoader', 'rxjs/Observable': 'Rx', 'rxjs/operators/filter': 'Rx.operators', 'rxjs/operators/map': 'Rx.operators', 'rxjs/operators/take': 'Rx.operators', 'rxjs/operators/tap': 'Rx.operators', 'rxjs/observable/of': 'Rx.Observable', + 'zone.js/dist/zone-node': 'zone.js.zone-node', 'fs': 'fs', 'express': 'express', 'hapi': 'hapi' diff --git a/tsconfig.json b/tsconfig.json index f6713a73a..d5983376b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,9 @@ "typeRoots": [ "node_modules/@types" ], + "paths": { + "@nguniversal/*": ["dist/bin/modules/*"] + }, "lib": [ "dom", "es6", @@ -37,6 +40,6 @@ "rewriteTsconfig": false }, "bazelOptions": { - "suppressTsconfigOverrideWarnings": true + "suppressTsconfigOverrideWarnings": false } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 03761ef2b..6419e081c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -71,6 +71,18 @@ tslib "^1.7.1" xhr2 "^0.1.4" +"@angular/router@^5.2.7": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-5.2.8.tgz#10e906803e5e71d42d157b0ccd78e773d1e316fc" + dependencies: + tslib "^1.7.1" + +"@angular/upgrade@^5.2.8": + version "5.2.8" + resolved "https://registry.yarnpkg.com/@angular/upgrade/-/upgrade-5.2.8.tgz#f66aa0f9a40551e98089493d9e2444d83b56f096" + dependencies: + tslib "^1.7.1" + "@bazel/ibazel@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.3.1.tgz#5f02f208f138e581bbdb1534d5c013d7a0ac9799"