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

feat(ls-routes): introduce ls-routes #778

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions modules/common/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 31 additions & 0 deletions modules/common/ls-routes/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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"],
# )
24 changes: 24 additions & 0 deletions modules/common/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))
})
})
8 changes: 8 additions & 0 deletions modules/common/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 './src/index';
3 changes: 3 additions & 0 deletions modules/common/ls-routes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "@nguniversal/common/ls-routes"
}
8 changes: 8 additions & 0 deletions modules/common/ls-routes/public_api.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 './index';
161 changes: 161 additions & 0 deletions modules/common/ls-routes/spec/ls-routes.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
}));
});
8 changes: 8 additions & 0 deletions modules/common/ls-routes/src/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/ls-routes/src/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 { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
import { ROUTES, Route } from '@angular/router';
import { Observable } from 'rxjs/Observable';

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);
}
});
}
12 changes: 12 additions & 0 deletions modules/common/ls-routes/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/ls-routes"
},
"angularCompilerOptions": {
"genDir": "ngfactory"
},
"files": [
"index.ts"
]
}
Loading