Skip to content
This repository has been archived by the owner on Nov 22, 2024. It is now read-only.

Commit

Permalink
feat(ls-routes): introduce ls-routes
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabian Wiles committed Aug 29, 2017
1 parent 3090533 commit ea09dbb
Show file tree
Hide file tree
Showing 15 changed files with 3,303 additions and 48 deletions.
12 changes: 11 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ addons:
before_install:
# Updating NPM to relevant version >= 3 on Node.JS LTS
- npm i -g npm@^3
- export CHROME_BIN=/usr/bin/google-chrome
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- sudo apt-get update
- sudo apt-get install -y libappindicator1 fonts-liberation
- wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- sudo dpkg -i google-chrome*.deb

before_script:
- npm install

script:
- npm run test:ci
- npm run build
5 changes: 5 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
47 changes: 47 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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'],
customLaunchers: {
Chrome_travis_ci: {
base: 'Chrome',
flags: ['--no-sandbox']
}
},
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: [process.env.TRAVIS ? 'Chrome_travis_ci' : 'Chrome'],
singleRun: false
});
};
24 changes: 24 additions & 0 deletions modules/ng-ls-routes/README.md
Original file line number Diff line number Diff line change
@@ -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))
})
})
1 change: 1 addition & 0 deletions modules/ng-ls-routes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src';
27 changes: 27 additions & 0 deletions modules/ng-ls-routes/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
1 change: 1 addition & 0 deletions modules/ng-ls-routes/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ls-routes';
155 changes: 155 additions & 0 deletions modules/ng-ls-routes/src/ls-routes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { lsRoutes } from './ls-routes';
import { ReflectiveInjector, enableProdMode, NgModule, Component } from '@angular/core';
import { async } from '@angular/core/testing';
import { COMPILER_PROVIDERS, JitCompiler, 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 } from '@angular/platform-server';

import * as fs from 'fs';

class FileLoader implements ResourceLoader {
get(url: string): string {
return fs.readFileSync(url).toString();
}
}

enableProdMode();
const injector = ReflectiveInjector.resolveAndCreate([
COMPILER_PROVIDERS,
{ provide: ResourceLoader, useValue: new FileLoader() }
]);
const jitCompiler: JitCompiler = injector.get(JitCompiler);

@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(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 MockModule { }
@NgModule({
imports: [
ServerModule,
MockModule,
ModuleMapLoaderModule
]
})
class MockServerModule{}
return jitCompiler.compileModuleAsync(MockServerModule);
}
function createFactoryAndGetRoutes(routeConfig: Route[], moduleMap: {[key: string]: any} = {}) {
//make it as easy as possible
return createTestingFactory(routeConfig)
.then(factory => lsRoutes('flatPaths', factory, moduleMap))
}

describe('ls-routes', () => {
it('should resolve a single path', async(() => {
createFactoryAndGetRoutes([
{ path: 'a' }
]).then(routes => {
expect(routes).toContain('/a')
})
}))
it('should resolve a multiple paths', async(() => {
createFactoryAndGetRoutes([
{ path: 'a' },
{ path: 'b' },
{ path: 'c' },
]).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' }
]
},
]).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'
}
], moduleMap).then(routes => {
expect(routes).toContain('/a/lazy-a')
})
}))
it('should resolve a function loaded loadChildren', async(() => {
createFactoryAndGetRoutes([
{
path: 'a',
loadChildren: () => jitCompiler.compileModuleSync(LazyModule)
}
]).then(routes => {
expect(routes).toContain('/a/lazy-a')
})
}))
it('should resolve a function loaded promise loadChildren', async(() => {
createFactoryAndGetRoutes([
{
path: 'a',
loadChildren: () => <any>jitCompiler.compileModuleAsync(LazyModule)
}
]).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'}
]
}
]
}
]).then(routes => {
expect(routes).toContain('/')
expect(routes).toContain('/level3')
})
}))
})
110 changes: 110 additions & 0 deletions modules/ng-ls-routes/src/ls-routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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<T>(
returnType: 'flatPaths' | 'nestedPaths' | 'full',
serverFactory: NgModuleFactory<T>,
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<T, V>(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: string) => (!route.path ? '' : '/' + route.path) + childRoute);
}
});
}

function coerceIntoPromise<T>(mightBePromise: Observable<T> | Promise<T> | T): Promise<T> {
if (mightBePromise instanceof Observable) {
return mightBePromise.toPromise();
}
return Promise.resolve(mightBePromise);
}

function extractRoute(route: Route, injector: Injector): Promise<Route> {
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<Route> {
let nextFactory: Promise<NgModuleFactory<any>>;
if (typeof route.loadChildren === 'function') {
nextFactory = coerceIntoPromise<NgModuleFactory<any>>(
< NgModuleFactory<any> | Promise<NgModuleFactory<any>> >route.loadChildren()
)
} else {
nextFactory = loader.load(<string>route.loadChildren)
}
return nextFactory
.then(factory => Promise.all(createModule(factory, injector)))
.then(children => {
route.children = children;
delete route.loadChildren;
return route;
});
}

function createModule<T>(factory: NgModuleFactory<T>, parentInjector: Injector): Promise<Route>[] {

const moduleRef = factory.create(parentInjector);
const routes = moduleRef.injector.get(ROUTES);

return flattenArray<Route[][], Route>(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);
}
});
}
Loading

0 comments on commit ea09dbb

Please sign in to comment.