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

Commit

Permalink
feat(common): introduce ls-routes
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabian Wiles committed May 20, 2018
1 parent 5ca79bb commit 3cb0384
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 1 deletion.
4 changes: 3 additions & 1 deletion modules/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions modules/common/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
*/
export { TransferHttpCacheModule } from './src/transfer_http';
export { StateTransferInitializerModule } from './src/state-transfer-initializer/module';
export { lsRoutes } from './src/ls-routes';
export * from './private_api';
154 changes: 154 additions & 0 deletions modules/common/spec/ls-routes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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 { ɵFileLoader as FileLoader } from '@nguniversal/common';

@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('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');
});
}));
});
8 changes: 8 additions & 0 deletions modules/common/src/ls-routes/index.ts
Original file line number Diff line number Diff line change
@@ -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';
118 changes: 118 additions & 0 deletions modules/common/src/ls-routes/ls-routes.ts
Original file line number Diff line number Diff line change
@@ -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 { ROUTES, Route } from '@angular/router';
import { Observable } from 'rxjs';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

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 => (!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>>(
route.loadChildren() as NgModuleFactory<any> | Promise<NgModuleFactory<any>>
);
} 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<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);
}
});
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"@angular/common": "^6.0.0",
"@angular/compiler": "^6.0.0",
"@angular/compiler-cli": "^6.0.0",
"@angular/router": "^6.0.0",
"@angular/core": "^6.0.0",
"@angular/http": "^6.0.0",
"@angular/platform-browser": "^6.0.0",
Expand Down
6 changes: 6 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@
tslib "^1.9.0"
xhr2 "^0.1.4"

"@angular/router@^6.0.2":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@angular/router/-/router-6.0.2.tgz#a7c925751accede6c5b4a8369170a53f3a48b940"
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"
Expand Down

0 comments on commit 3cb0384

Please sign in to comment.