Skip to content

Commit

Permalink
Tighten up some of the main application APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
clbond committed Mar 30, 2017
1 parent 2009f31 commit 9874115
Show file tree
Hide file tree
Showing 19 changed files with 185 additions and 148 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -316,7 +316,7 @@ export class LocaleTransition implements StateTransition<string> {
// values from the Set we created when we first described the locale variant below).
// Note that this class can use the ng dependency injection system to retrieve any
// services that it needs in order to execute the state transition.
execute(value: string) {
transition(value: string) {
this.localeService.locale(value);
}
}
Expand Down Expand Up @@ -380,7 +380,7 @@ export class ServerStateReader implements StateReader<MyState> {
constructor(private store: Store<AppState>) {}

getState(): Promise<MyState> {
return this.store.select(s => s.someState).take(1).toPromise();
return this.store.select().toPromise();
}
}
```
Expand Down
9 changes: 7 additions & 2 deletions examples/demand-express/server/index.ts
Expand Up @@ -10,7 +10,7 @@ import {AppModule} from '../app/app.module';

import {absoluteUri, configure, listen} from './http';

import {Variants, variants} from './variants';
import {TransitionLocale, Variants} from './variants';

import {index} from './paths';

Expand All @@ -22,7 +22,12 @@ enableProdMode();

const builder = new ApplicationBuilderFromModule<Variants, AppModule>(AppModule, index);

builder.variants(variants);
builder.variants({
locale: {
values: ['en-US', 'fr-FR'],
transition: TransitionLocale
}
});

const application = builder.build();

Expand Down
11 changes: 2 additions & 9 deletions examples/demand-express/server/variants.ts
@@ -1,25 +1,18 @@
import {Injectable} from '@angular/core';

import {StateTransition, VariantsMap} from 'angular-ssr';
import {StateTransition} from 'angular-ssr';

import {LocaleService} from '../app/locale.service';

@Injectable()
export class TransitionLocale implements StateTransition<string> {
constructor(private service: LocaleService) {}

execute(locale: string) {
transition(locale: string) {
this.service.locale(locale);
}
}

export interface Variants {
locale: string;
}

export const variants: VariantsMap = {
locale: {
values: ['en-US', 'fr-FR'],
transition: TransitionLocale
}
};
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "angular-ssr",
"version": "0.0.99",
"version": "0.1.0",
"description": "Angular server-side rendering implementation",
"main": "build/index.js",
"typings": "build/index.d.ts",
Expand Down
107 changes: 107 additions & 0 deletions source/application/builder/application-base.ts
@@ -0,0 +1,107 @@
import {NgModuleFactory} from '@angular/core';

import {Observable, Subject} from 'rxjs';

import chalk = require('chalk');

import uri = require('url');

import {Application} from './application';
import {PlatformImpl, bootstrapWithExecute, forkZone} from '../../platform';
import {RenderOperation, RenderVariantOperation} from '../operation';
import {Route, applicationRoutes, renderableRoutes} from '../../route';
import {Snapshot, snapshot} from '../../snapshot';
import {baseUri} from '../../static';
import {composeTransitions} from '../../variants';
import {forkRender} from './fork';

export abstract class ApplicationBase<V, M> implements Application<V> {
constructor(
private platformImpl: PlatformImpl,
private render: RenderOperation,
private moduleFactory: () => Promise<NgModuleFactory<M>>
) {}

abstract dispose(): void;

async prerender(): Promise<Observable<Snapshot<V>>> {
if (this.render.routes == null || this.render.routes.length === 0) {
this.render.routes = renderableRoutes(await this.discoverRoutes());

if (this.render.routes.length === 0) {
return Observable.of();
}
}

return this.renderToStream(this.render);
}

renderUri(uri: string, variant?: V): Promise<Snapshot<V>> {
uri = resolveToAbsoluteUri(uri);

const transition = composeTransitions(this.render.variants, variant);

const vop: RenderVariantOperation<V> = {scope: this.render, uri, variant, transition};

return this.renderVariant(vop);
}

async discoverRoutes(): Promise<Array<Route>> {
const moduleFactory = await this.moduleFactory();

return await applicationRoutes(this.platformImpl, moduleFactory, this.render.templateDocument);
}

private renderToStream(operation: RenderOperation): Observable<Snapshot<V>> {
const subject = new Subject<Snapshot<V>>();

const bind = async (suboperation: RenderVariantOperation<V>) => {
try {
subject.next(await this.renderVariant(suboperation));
}
catch (exception) {
subject.error(exception);
}
};

const promises = forkRender<V>(operation).map(suboperation => bind(suboperation));

Promise.all(promises).then(() => subject.complete());

return subject.asObservable();
}

private async renderVariant(operation: RenderVariantOperation<V>): Promise<Snapshot<V>> {
const {uri, scope: {templateDocument}} = operation;

const moduleFactory = await this.moduleFactory();

const instantiate = async () =>
await bootstrapWithExecute<M, Snapshot<V>>(this.platformImpl, moduleFactory, ref => snapshot(ref, operation));

return await forkZone(templateDocument, uri, instantiate);
}
}

let relativeUriWarning = false;

const resolveToAbsoluteUri = (relativeUri: string): string => {
if (relativeUri == null ||
relativeUri.length === 0 ||
relativeUri === '/') {
return baseUri;
}

const resolved = uri.resolve(baseUri, relativeUri);

if (resolved !== relativeUri) {
if (relativeUriWarning === false) {
console.warn(chalk.yellow(`It is best to avoid using relative URIs like ${relativeUri} when requesting render results`));
console.warn(chalk.yellow('The reason is that your application may key its service URIs from "window.location" in some manner'));
console.warn(chalk.yellow(`I have resolved this relative URI to ${resolved} and this may impact your application`));
relativeUriWarning = true;
}
}

return resolved;
};
117 changes: 14 additions & 103 deletions source/application/builder/application.ts
@@ -1,107 +1,18 @@
import {NgModuleFactory} from '@angular/core';
import {Observable} from 'rxjs/Observable';

import {Observable, Subject} from 'rxjs';
import {Disposable} from '../../disposable';
import {Route} from '../../route';
import {Snapshot} from '../../snapshot';

import {Disposable} from './../../disposable';
import {PlatformImpl, bootstrapWithExecute, forkZone} from '../../platform';
import {RenderOperation, RenderVariantOperation} from '../operation';
import {Snapshot, snapshot} from '../../snapshot';
import {Route, applicationRoutes, renderableRoutes} from '../../route';
import {baseUri} from '../../static';
import {composeTransitions} from '../../variants';
import {fork} from './fork';
export interface Application<V> extends Disposable {
// Render the specified absolute URI with optional variant
renderUri(uri: string, variant?: V): Promise<Snapshot<V>>;

import uri = require('url');
// Prerender all of the routes provided from the ApplicationBuilder. If no routes were
// provided, they will be discovered using discoverRoutes() and filtered down to the
// routes that do not require parameters (eg /blog/:id will be excluded, / will not)
prerender(): Promise<Observable<Snapshot<V>>>;

import chalk = require('chalk');

export abstract class Application<V, M> implements Disposable {
constructor(
private platformImpl: PlatformImpl,
private render: RenderOperation,
private moduleFactory: () => Promise<NgModuleFactory<M>>
) {}

abstract dispose(): void;

async prerender(): Promise<Observable<Snapshot<V>>> {
if (this.render.routes == null || this.render.routes.length === 0) {
this.render.routes = renderableRoutes(await this.discoverRoutes());

if (this.render.routes.length === 0) {
return Observable.of();
}
}

return this.renderToStream(this.render);
}

renderUri(uri: string, variant?: V): Promise<Snapshot<V>> {
uri = resolveToAbsoluteUri(uri);

const transition = composeTransitions(this.render.variants, variant);

const vop: RenderVariantOperation<V> = {scope: this.render, uri, variant, transition};

return this.renderVariant(vop);
}

async discoverRoutes(): Promise<Array<Route>> {
const moduleFactory = await this.moduleFactory();

return await applicationRoutes(this.platformImpl, moduleFactory, this.render.templateDocument);
}

private renderToStream(operation: RenderOperation): Observable<Snapshot<V>> {
const subject = new Subject<Snapshot<V>>();

const bind = async (suboperation: RenderVariantOperation<V>) => {
try {
subject.next(await this.renderVariant(suboperation));
}
catch (exception) {
subject.error(exception);
}
};

const promises = fork<V>(operation).map(suboperation => bind(suboperation));

Promise.all(promises).then(() => subject.complete());

return subject.asObservable();
}

private async renderVariant(operation: RenderVariantOperation<V>): Promise<Snapshot<V>> {
const {uri, scope: {templateDocument}} = operation;

const moduleFactory = await this.moduleFactory();

const instantiate = async () =>
await bootstrapWithExecute<M, Snapshot<V>>(this.platformImpl, moduleFactory, ref => snapshot(ref, operation));

return await forkZone(templateDocument, uri, instantiate);
}
}

let relativeUriWarning = false;

const resolveToAbsoluteUri = (relativeUri: string): string => {
if (relativeUri == null ||
relativeUri.length === 0 ||
relativeUri === '/') {
return baseUri;
}

const resolved = uri.resolve(baseUri, relativeUri);

if (resolved !== relativeUri) {
if (relativeUriWarning === false) {
console.warn(chalk.yellow(`It is best to avoid using relative URIs like ${relativeUri} when requesting render results`));
console.warn(chalk.yellow('The reason is that your application may key its service URIs from "window.location" in some manner'));
console.warn(chalk.yellow(`I have resolved this relative URI to ${resolved} and this may impact your application`));
relativeUriWarning = true;
}
}

return resolved;
};
// Discover all of the routes defined in all the NgModules of this application
discoverRoutes(): Promise<Array<Route>>;
}
6 changes: 3 additions & 3 deletions source/application/builder/builder-base.ts
@@ -1,11 +1,11 @@
import {Application} from './application';
import {ApplicationBuilder} from './builder';
import {ApplicationBootstrapper, ApplicationStateReader, Postprocessor, PrebootConfiguration, VariantsMap} from '../contracts';
import {FileReference, fileFromString} from '../../filesystem';
import {RenderOperation} from '../operation';
import {Route} from '../../route';
import {FileReference, fileFromString} from '../../filesystem';

export abstract class ApplicationBuilderBase<M> implements ApplicationBuilder {
export abstract class ApplicationBuilderBase<V> implements ApplicationBuilder<V> {
constructor(templateDocument?: FileReference | string) {
if (templateDocument) {
this.templateDocument(templateDocument.toString());
Expand All @@ -14,7 +14,7 @@ export abstract class ApplicationBuilderBase<M> implements ApplicationBuilder {

protected operation: Partial<RenderOperation> = {};

abstract build(): Application<any, M>;
abstract build(): Application<V>;

preboot(config?: PrebootConfiguration) {
if (config != null) {
Expand Down
4 changes: 2 additions & 2 deletions source/application/builder/builder.ts
Expand Up @@ -10,9 +10,9 @@ import {Application} from './application';

import {Route} from './../../route/route';

export interface ApplicationBuilder {
export interface ApplicationBuilder<V> {
// Construct an application from this builder after configuring it
build(): Application<any, any>;
build(): Application<V>;

// Provide a template HTML document that will be used when rendering this application.
// In almost all cases this will be the build output file `dist/index.html`, not the
Expand Down
2 changes: 1 addition & 1 deletion source/application/builder/fork.ts
Expand Up @@ -4,7 +4,7 @@ import {permutations} from '../../variants/permutations';

import {routeToUri} from '../../route/transform';

export const fork = <V>(operation: RenderOperation): Array<RenderVariantOperation<V>> => {
export const forkRender = <V>(operation: RenderOperation): Array<RenderVariantOperation<V>> => {
const operations = new Array<RenderVariantOperation<V>>();

for (const route of operation.routes) {
Expand Down
5 changes: 3 additions & 2 deletions source/application/builder/from-module-factory.ts
Expand Up @@ -3,6 +3,7 @@ import {NgModuleFactory} from '@angular/core';
import {FileReference} from '../../filesystem';

import {Application} from './application';
import {ApplicationBase} from './application-base';
import {ApplicationBuilderBase} from './builder-base';
import {RenderOperation} from '../operation';
import {PlatformImpl, createJitPlatform} from '../../platform';
Expand All @@ -16,10 +17,10 @@ export class ApplicationBuilderFromModuleFactory<V> extends ApplicationBuilderBa
}
}

build(): Application<V, any> {
build(): Application<V> {
const platform = createJitPlatform([]) as PlatformImpl;

class ApplicationFromModuleFactoryImpl extends Application<V, any> {
class ApplicationFromModuleFactoryImpl extends ApplicationBase<V, any> {
dispose() {
platform.destroy();
}
Expand Down

0 comments on commit 9874115

Please sign in to comment.