diff --git a/build.sh b/build.sh index d0b126e010..476979eb82 100755 --- a/build.sh +++ b/build.sh @@ -16,3 +16,8 @@ npm run build:ng-module-map-ngfactory-loader cp modules/ng-module-map-ngfactory-loader/package.json dist/ng-module-map-ngfactory-loader/package.json cp modules/ng-module-map-ngfactory-loader/README.md dist/ng-module-map-ngfactory-loader/README.md + +npm run build:ng-ls-routes + +cp modules/ng-ls-routes/package.json dist/ng-ls-routes/package.json +cp modules/ng-ls-routes/README.md dist/ng-ls-routes/README.md diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000000..3caddde804 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,41 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', 'karma-typescript'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('karma-typescript'), + ], + client:{ + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + files: [ + 'test.ts', + './modules/**/*.spec.ts', + './modules/**/*.ts', + ], + preprocessors: { + '**/*.ts': ['karma-typescript'], + }, + karmaTypescriptConfig: { + tsconfig: './tsconfig.spec.json', + }, + coverageIstanbulReporter: { + reports: [ 'html', 'lcovonly' ], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; diff --git a/modules/ng-ls-routes/README.md b/modules/ng-ls-routes/README.md new file mode 100644 index 0000000000..bf95c19054 --- /dev/null +++ b/modules/ng-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/ng-ls-routes/index.ts b/modules/ng-ls-routes/index.ts new file mode 100644 index 0000000000..8420b1093f --- /dev/null +++ b/modules/ng-ls-routes/index.ts @@ -0,0 +1 @@ +export * from './src'; diff --git a/modules/ng-ls-routes/package.json b/modules/ng-ls-routes/package.json new file mode 100644 index 0000000000..aafd890684 --- /dev/null +++ b/modules/ng-ls-routes/package.json @@ -0,0 +1,27 @@ +{ + "name": "@nguniversal/ls-routes", + "main": "index.js", + "types": "index.d.ts", + "version": "1.0.0-beta.0", + "description": "A tool for retrieving routes from a factory", + "homepage": "https://github.com/angular/universal", + "license": "MIT", + "contributors": [ + "Toxicable" + ], + "repository": { + "type": "git", + "url": "https://github.com/angular/universal" + }, + "bugs": { + "url": "https://github.com/angular/universal/issues" + }, + "peerDependencies": { + "@angular/core": "^4.0.0", + "@angular/platform-server": "^4.0.0", + "@angular/router": "^4.0.0", + "@nguniversal/module-map-ngfactory-loader": "^1.0.0", + "rxjs": "^5.0.0", + "zone.js": "^0.8.4" + } +} diff --git a/modules/ng-ls-routes/src/index.ts b/modules/ng-ls-routes/src/index.ts new file mode 100644 index 0000000000..1134a4e28b --- /dev/null +++ b/modules/ng-ls-routes/src/index.ts @@ -0,0 +1 @@ +export * from './ls-routes'; diff --git a/modules/ng-ls-routes/src/ls-routes.spec.ts b/modules/ng-ls-routes/src/ls-routes.spec.ts new file mode 100644 index 0000000000..a8995a0fdb --- /dev/null +++ b/modules/ng-ls-routes/src/ls-routes.spec.ts @@ -0,0 +1,92 @@ +import { lsRoutes } from './ls-routes'; +import * as fs from 'fs'; +import { ReflectiveInjector, enableProdMode, NgModule, Component } from '@angular/core'; +import { COMPILER_PROVIDERS, JitCompiler, ResourceLoader } from '@angular/compiler'; +import { RouterModule, Route } from '@angular/router'; +import { BrowserModule } from "@angular/platform-browser"; +class FileLoader implements ResourceLoader { + get(url: string): Promise { + return new Promise((resolve) => { + resolve(fs.readFileSync(url).toString()); + }); + } +} +let _jitCompiler: JitCompiler; +export function jitCompiler(): JitCompiler { + if (!_jitCompiler) { + enableProdMode(); + const injector = ReflectiveInjector.resolveAndCreate([ + COMPILER_PROVIDERS, + { provide: ResourceLoader, useValue: new FileLoader() } + ]); + _jitCompiler = injector.get(JitCompiler); + } + return _jitCompiler; +} + +function assignComponent(route: Route, comp: any){ + route.component = comp; + if(route.children){ + route.children = route.children.map(route => assignComponent(route, comp)); + } + return route; +} + +function createTestingFactory(routeConfig: Route[]) { + @Component({selector: 'a', template: 'a' }) + class MockComponent { } + + @NgModule({ + imports: [ + BrowserModule, + RouterModule.forRoot(routeConfig.map(r => assignComponent(r, MockComponent))) + ], + declarations: [MockComponent] + }) + class TestModule { } + return jitCompiler().compileModuleAsync(TestModule); +} +function createFactoryAndGetRotues(routeConfig: Route[]) { + //make it as easy as possible + return createTestingFactory(routeConfig) + .then(factory => lsRoutes('flatPaths', factory, {})) +} + +describe('ls-routes', () => { + it('should resolve a single path', (done) => { + createFactoryAndGetRotues([ + { path: 'a' } + ]) + .then(routes => { + expect(routes).toContain('/a') + done() + }) + }) + it('should resolve a multiple paths', (done) => { + createFactoryAndGetRotues([ + { path: 'a' }, + { path: 'b' }, + { path: 'c' }, + ]) + .then(routes => { + expect(routes).toContain('/a') + expect(routes).toContain('/b') + expect(routes).toContain('/c') + done() + }) + }) + it('should resolve nested paths', (done) => { + createFactoryAndGetRotues([ + { + path: 'a', + children: [ + { path: 'a-a' } + ] + }, + ]) + .then(routes => { + expect(routes).toContain('/a/a-a') + done() + }) + }) +}) diff --git a/modules/ng-ls-routes/src/ls-routes.ts b/modules/ng-ls-routes/src/ls-routes.ts new file mode 100644 index 0000000000..11b7296108 --- /dev/null +++ b/modules/ng-ls-routes/src/ls-routes.ts @@ -0,0 +1,109 @@ +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"; +import 'rxjs/add/operator/toPromise'; + +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 { + return flattenRouteToPath(route.children) + .map((childRoute: string) => (!route.path ? '' : '/' + route.path) + childRoute); + } + }); +} + +function cocerceIntoPromise(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 = cocerceIntoPromise>( + < NgModuleFactory | Promise> >route.loadChildren() + ) + } else { + nextFactory = loader.load(route.loadChildren) + } + 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/ng-ls-routes/tsconfig.json b/modules/ng-ls-routes/tsconfig.json new file mode 100644 index 0000000000..6cc5b010c3 --- /dev/null +++ b/modules/ng-ls-routes/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/ng-ls-routes" + }, + "angularCompilerOptions": { + "genDir": "ngfactory" + }, + "files": [ + "index.ts" + ] +} diff --git a/package.json b/package.json index c54e0e1043..2f2753cf14 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,9 @@ "build:ng-express-engine": "ngc -p modules/ng-express-engine/tsconfig.json", "build:ng-aspnetcore-engine": "ngc -p modules/ng-aspnetcore-engine/tsconfig.json", "build:ng-module-map-ngfactory-loader": "ngc -p modules/ng-module-map-ngfactory-loader/tsconfig.json", + "build:ng-ls-routes": "ngc -p modules/ng-ls-routes/tsconfig.json", "build": "./build.sh", - "test": "exit 0" + "test": "karma start ./karma.conf.js" }, "devDependencies": { "@angular/animations": "^4.0.0", @@ -75,8 +76,17 @@ "@angular/http": "^4.0.0", "@angular/platform-browser": "^4.0.0", "@angular/platform-server": "^4.0.0", + "@angular/router": "^4.0.0", "@types/express": "^4.0.35", + "@types/jasmine": "^2.5.53", "express": "^4.15.2", + "jasmine": "^2.7.0", + "karma": "^1.7.0", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage-istanbul-reporter": "^1.3.0", + "karma-jasmine": "^1.1.0", + "karma-jasmine-html-reporter": "^0.2.2", + "karma-typescript": "^3.0.5", "rimraf": "^2.6.1", "rxjs": "^5.2.0", "typescript": "^2.2.1", diff --git a/test.ts b/test.ts new file mode 100644 index 0000000000..41d11f17d5 --- /dev/null +++ b/test.ts @@ -0,0 +1,10 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files +import 'zone.js/dist/zone-node'; +import 'zone.js/dist/long-stack-trace-zone'; +import 'zone.js/dist/proxy.js'; +import 'zone.js/dist/sync-test'; +import 'zone.js/dist/jasmine-patch'; +import 'zone.js/dist/async-test'; +import 'zone.js/dist/fake-async-test'; +import 'reflect-metadata'; + diff --git a/tsconfig.json b/tsconfig.json index 1e334c2604..136c284bc1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,12 +12,15 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "sourceMap": true, + "skipLibCheck": true, "lib": [ "dom", "es6" ], "types": [ - "node" + "node", + "jasmine", + "karma" ] }, "compileOnSave": false, diff --git a/tsconfig.spec.json b/tsconfig.spec.json new file mode 100644 index 0000000000..5b0ae3b3e6 --- /dev/null +++ b/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine", + "node" + ], + "skipLibCheck": true + }, + "include": [ + "test.ts", + "./modules/**/*.spec.ts", + "./modules/**/*.ts" + ] +}