From e3551cfb74356a30391b2792a04e7071807c4e50 Mon Sep 17 00:00:00 2001 From: Stephen Lautier Date: Wed, 7 Feb 2018 11:57:19 +0100 Subject: [PATCH 1/2] style(*): fix lint errors --- .../aspnetcore-engine/src/create-transfer-script.ts | 2 +- .../src/interfaces/engine-render-result.ts | 13 +++++++++++++ .../src/interfaces/request-params.ts | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 modules/aspnetcore-engine/src/interfaces/engine-render-result.ts diff --git a/modules/aspnetcore-engine/src/create-transfer-script.ts b/modules/aspnetcore-engine/src/create-transfer-script.ts index 15b299d07..cb78ab2f5 100644 --- a/modules/aspnetcore-engine/src/create-transfer-script.ts +++ b/modules/aspnetcore-engine/src/create-transfer-script.ts @@ -1,3 +1,3 @@ export function createTransferScript(transferData: Object): string { return ``; -} \ No newline at end of file +} diff --git a/modules/aspnetcore-engine/src/interfaces/engine-render-result.ts b/modules/aspnetcore-engine/src/interfaces/engine-render-result.ts new file mode 100644 index 000000000..c80aad4f8 --- /dev/null +++ b/modules/aspnetcore-engine/src/interfaces/engine-render-result.ts @@ -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; + }; +} diff --git a/modules/aspnetcore-engine/src/interfaces/request-params.ts b/modules/aspnetcore-engine/src/interfaces/request-params.ts index 2d989eca0..7b8c74176 100644 --- a/modules/aspnetcore-engine/src/interfaces/request-params.ts +++ b/modules/aspnetcore-engine/src/interfaces/request-params.ts @@ -6,4 +6,4 @@ export interface IRequestParams { absoluteUrl: string; // e.g., 'https://example.com:1234/some/path' domainTasks: Promise; data: any; // any custom object passed through from .NET -} \ No newline at end of file +} From bbab06cbbead3980ac463995c0290cff99ce9b33 Mon Sep 17 00:00:00 2001 From: Stephen Lautier Date: Wed, 7 Feb 2018 12:17:32 +0100 Subject: [PATCH 2/2] fix(aspnet): fix transfer state for aspnetcore --- modules/aspnetcore-engine/index.ts | 1 + modules/aspnetcore-engine/src/main.ts | 152 ++++++++++-------- .../src/platform-server-utils.ts | 103 ++++++++++++ 3 files changed, 189 insertions(+), 67 deletions(-) create mode 100644 modules/aspnetcore-engine/src/platform-server-utils.ts diff --git a/modules/aspnetcore-engine/index.ts b/modules/aspnetcore-engine/index.ts index 0a23d586a..8ca333d11 100644 --- a/modules/aspnetcore-engine/index.ts +++ b/modules/aspnetcore-engine/index.ts @@ -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'; diff --git a/modules/aspnetcore-engine/src/main.ts b/modules/aspnetcore-engine/src/main.ts index 0aa73ad50..1096826a5 100644 --- a/modules/aspnetcore-engine/src/main.ts +++ b/modules/aspnetcore-engine/src/main.ts @@ -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 { if (!options.appSelector) { throw new Error(`appSelector is required! Pass in " appSelector: '' ", for your root App component.`); @@ -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 + } ] ); @@ -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); diff --git a/modules/aspnetcore-engine/src/platform-server-utils.ts b/modules/aspnetcore-engine/src/platform-server-utils.ts new file mode 100644 index 000000000..2f1d5bd1c --- /dev/null +++ b/modules/aspnetcore-engine/src/platform-server-utils.ts @@ -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 { + html: string; + moduleRef: NgModuleRef; +} + +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( + platform: PlatformRef, moduleRefPromise: Promise>): Promise> { + 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( + module: Type, options: { document?: string, url?: string, extraProviders?: StaticProvider[] }): + Promise> { + 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( + moduleFactory: NgModuleFactory, + options: { document?: string, url?: string, extraProviders?: StaticProvider[] }): + Promise> { + const platform = _getPlatform(platformServer, options); + return _render(platform, platform.bootstrapModuleFactory(moduleFactory)); +}