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

fix(aspnetcore-engine): fix ServerTransferStateModule and TransferState #889

Merged
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions modules/aspnetcore-engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { REQUEST, ORIGIN_URL } from './src/tokens';

export { IEngineOptions } from './src/interfaces/engine-options';
export { IRequestParams } from './src/interfaces/request-params';
export { IEngineRenderResult } from './src/interfaces/engine-render-result';
2 changes: 1 addition & 1 deletion modules/aspnetcore-engine/src/create-transfer-script.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function createTransferScript(transferData: Object): string {
return `<script>window['TRANSFER_CACHE'] = ${JSON.stringify(transferData)};</script>`;
}
}
13 changes: 13 additions & 0 deletions modules/aspnetcore-engine/src/interfaces/engine-render-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NgModuleRef } from "@angular/core";

export interface IEngineRenderResult {
html: string;
moduleRef: NgModuleRef<{}>;
globals: {
styles: string;
title: string;
meta: string;
transferData?: {};
[key: string]: any;
};
}
2 changes: 1 addition & 1 deletion modules/aspnetcore-engine/src/interfaces/request-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export interface IRequestParams {
absoluteUrl: string; // e.g., 'https://example.com:1234/some/path'
domainTasks: Promise<any>;
data: any; // any custom object passed through from .NET
}
}
152 changes: 85 additions & 67 deletions modules/aspnetcore-engine/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,92 @@
import { Type, NgModuleFactory, CompilerFactory, Compiler } from '@angular/core';
import { platformDynamicServer, BEFORE_APP_SERIALIZED, renderModuleFactory } from '@angular/platform-server';
import { platformDynamicServer } from '@angular/platform-server';
import { DOCUMENT } from '@angular/platform-browser';
import { ResourceLoader } from '@angular/compiler';

import { REQUEST, ORIGIN_URL } from './tokens';
import { FileLoader } from './file-loader';

import { IEngineOptions } from './interfaces/engine-options';
import { DOCUMENT } from '@angular/platform-browser';
import { IEngineRenderResult } from './interfaces/engine-render-result';
import { renderModuleFactory } from './platform-server-utils';

/* @internal */
export class UniversalData {
public static appNode = '';
public static title = '';
public static scripts = '';
public static styles = '';
public static meta = '';
public static links = '';
public appNode = '';
public title = '';
public scripts = '';
public styles = '';
public meta = '';
public links = '';
}

/* @internal */
let appSelector = 'app-root'; // default

/* @internal */
function beforeAppSerialized(
function _getUniversalData(
doc: any /* TODO: type definition for Domino - DomAPI Spec (similar to "Document") */
) {

return () => {
const STYLES = [];
const SCRIPTS = [];
const META = [];
const LINKS = [];

for (let i = 0; i < doc.head.children.length; i++) {
const element = doc.head.children[i];
const tagName = element.tagName.toUpperCase();

switch (tagName) {
case 'SCRIPT':
SCRIPTS.push(element.outerHTML);
break;
case 'STYLE':
STYLES.push(element.outerHTML);
break;
case 'LINK':
LINKS.push(element.outerHTML);
break;
case 'META':
META.push(element.outerHTML);
break;
default:
break;
}
): UniversalData {

const STYLES = [];
const SCRIPTS = [];
const META = [];
const LINKS = [];

for (let i = 0; i < doc.head.children.length; i++) {
const element = doc.head.children[i];
const tagName = element.tagName.toUpperCase();

switch (tagName) {
case 'SCRIPT':
SCRIPTS.push(element.outerHTML);
break;
case 'STYLE':
STYLES.push(element.outerHTML);
break;
case 'LINK':
LINKS.push(element.outerHTML);
break;
case 'META':
META.push(element.outerHTML);
break;
default:
break;
}
}

for (let i = 0; i < doc.body.children.length; i++) {
const element: Element = doc.body.children[i];
const tagName = element.tagName.toUpperCase();

switch (tagName) {
case 'SCRIPT':
SCRIPTS.push(element.outerHTML);
break;
case 'STYLE':
STYLES.push(element.outerHTML);
break;
case 'LINK':
LINKS.push(element.outerHTML);
break;
case 'META':
META.push(element.outerHTML);
break;
default:
break;
}
}

UniversalData.title = doc.title;
UniversalData.appNode = doc.querySelector(appSelector).outerHTML;
UniversalData.scripts = SCRIPTS.join(' ');
UniversalData.styles = STYLES.join(' ');
UniversalData.meta = META.join(' ');
UniversalData.links = LINKS.join(' ');
return {
title: doc.title,
appNode: doc.querySelector(appSelector).outerHTML,
scripts: SCRIPTS.join('\n'),
styles: STYLES.join('\n'),
meta: META.join('\n'),
links: LINKS.join('\n')
};
}


export function ngAspnetCoreEngine(
options: IEngineOptions
): Promise<{ html: string, globals: { styles: string, title: string, meta: string, transferData?: {}, [key: string]: any } }> {
export function ngAspnetCoreEngine(options: IEngineOptions): Promise<IEngineRenderResult> {

if (!options.appSelector) {
throw new Error(`appSelector is required! Pass in " appSelector: '<app-root></app-root>' ", for your root App component.`);
Expand Down Expand Up @@ -95,17 +115,13 @@ export function ngAspnetCoreEngine(
options.providers = options.providers || [];

const extraProviders = options.providers.concat(
...options.providers,
[{
provide: ORIGIN_URL,
useValue: options.request.origin
}, {
provide: REQUEST,
useValue: options.request.data.request
}, {
provide: BEFORE_APP_SERIALIZED,
useFactory: beforeAppSerialized, multi: true, deps: [ DOCUMENT ]
}
[{
provide: ORIGIN_URL,
useValue: options.request.origin
}, {
provide: REQUEST,
useValue: options.request.data.request
}
]
);

Expand All @@ -117,18 +133,20 @@ export function ngAspnetCoreEngine(
extraProviders: extraProviders
});
})
.then(() => {
.then(result => {
const doc = result.moduleRef.injector.get(DOCUMENT);
const universalData = _getUniversalData(doc);

resolve({
html: UniversalData.appNode,
html: universalData.appNode,
moduleRef: result.moduleRef,
globals: {
styles: UniversalData.styles,
title: UniversalData.title,
scripts: UniversalData.scripts,
meta: UniversalData.meta,
links: UniversalData.links
styles: universalData.styles,
title: universalData.title,
scripts: universalData.scripts,
meta: universalData.meta,
links: universalData.links
}

});
}, (err) => {
reject(err);
Expand Down
103 changes: 103 additions & 0 deletions modules/aspnetcore-engine/src/platform-server-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Copied from @angular/platform-server utils. https://github.com/angular/angular/blob/master/packages/platform-server/src/utils.ts
Github issue to avoid copy/paste: https://github.com/angular/angular/issues/22049#issuecomment-363638743
*/

import { ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider, Type } from '@angular/core';
import { ɵTRANSITION_ID } from '@angular/platform-browser';
import { filter } from 'rxjs/operator/filter';
import { first } from 'rxjs/operator/first';
import { toPromise } from 'rxjs/operator/toPromise';
import { platformDynamicServer, platformServer, BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformState } from '@angular/platform-server';

interface PlatformOptions {
document?: string;
url?: string;
extraProviders?: StaticProvider[];
}

export interface ModuleRenderResult<T> {
html: string;
moduleRef: NgModuleRef<T>;
}

function _getPlatform(
platformFactory: (extraProviders: StaticProvider[]) => PlatformRef,
options: PlatformOptions): PlatformRef {
const extraProviders = options.extraProviders ? options.extraProviders : [];
return platformFactory([
{ provide: INITIAL_CONFIG, useValue: { document: options.document, url: options.url } },
extraProviders
]);
}

function _render<T>(
platform: PlatformRef, moduleRefPromise: Promise<NgModuleRef<T>>): Promise<ModuleRenderResult<T>> {
return moduleRefPromise.then(moduleRef => {
const transitionId = moduleRef.injector.get(ɵTRANSITION_ID, null);
if (!transitionId) {
throw new Error(
`renderModule[Factory]() requires the use of BrowserModule.withServerTransition() to ensure
the server-rendered app can be properly bootstrapped into a client app.`);
}
const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
return toPromise
.call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable)))
.then(() => {
const platformState = platform.injector.get(PlatformState);

// Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
const callbacks = moduleRef.injector.get(BEFORE_APP_SERIALIZED, null);
if (callbacks) {
for (const callback of callbacks) {
try {
callback();
} catch (e) {
// Ignore exceptions.
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
}
}
}

const output = platformState.renderToString();
platform.destroy();
return { html: output, moduleRef };
});
});
}

/**
* Renders a Module to string.
*
* `document` is the full document HTML of the page to render, as a string.
* `url` is the URL for the current render request.
* `extraProviders` are the platform level providers for the current render request.
*
* Do not use this in a production server environment. Use pre-compiled {@link NgModuleFactory} with
* {@link renderModuleFactory} instead.
*
* @experimental
*/
export function renderModule<T>(
module: Type<T>, options: { document?: string, url?: string, extraProviders?: StaticProvider[] }):
Promise<ModuleRenderResult<T>> {
const platform = _getPlatform(platformDynamicServer, options);
return _render(platform, platform.bootstrapModule(module));
}

/**
* Renders a {@link NgModuleFactory} to string.
*
* `document` is the full document HTML of the page to render, as a string.
* `url` is the URL for the current render request.
* `extraProviders` are the platform level providers for the current render request.
*
* @experimental
*/
export function renderModuleFactory<T>(
moduleFactory: NgModuleFactory<T>,
options: { document?: string, url?: string, extraProviders?: StaticProvider[] }):
Promise<ModuleRenderResult<T>> {
const platform = _getPlatform(platformServer, options);
return _render(platform, platform.bootstrapModuleFactory(moduleFactory));
}