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

Commit 14c7616

Browse files
stephenlautierCaerusKaru
authored andcommitted
fix(aspnetcore-engine): fix ServerTransferStateModule and TransferState (#889)
* fix(aspnet): fix transfer state for aspnetcore
1 parent c73d89c commit 14c7616

File tree

6 files changed

+204
-69
lines changed

6 files changed

+204
-69
lines changed

modules/aspnetcore-engine/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export { REQUEST, ORIGIN_URL } from './src/tokens';
66

77
export { IEngineOptions } from './src/interfaces/engine-options';
88
export { IRequestParams } from './src/interfaces/request-params';
9+
export { IEngineRenderResult } from './src/interfaces/engine-render-result';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export function createTransferScript(transferData: Object): string {
22
return `<script>window['TRANSFER_CACHE'] = ${JSON.stringify(transferData)};</script>`;
3-
}
3+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { NgModuleRef } from "@angular/core";
2+
3+
export interface IEngineRenderResult {
4+
html: string;
5+
moduleRef: NgModuleRef<{}>;
6+
globals: {
7+
styles: string;
8+
title: string;
9+
meta: string;
10+
transferData?: {};
11+
[key: string]: any;
12+
};
13+
}

modules/aspnetcore-engine/src/interfaces/request-params.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ export interface IRequestParams {
66
absoluteUrl: string; // e.g., 'https://example.com:1234/some/path'
77
domainTasks: Promise<any>;
88
data: any; // any custom object passed through from .NET
9-
}
9+
}

modules/aspnetcore-engine/src/main.ts

Lines changed: 85 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,92 @@
11
import { Type, NgModuleFactory, CompilerFactory, Compiler } from '@angular/core';
2-
import { platformDynamicServer, BEFORE_APP_SERIALIZED, renderModuleFactory } from '@angular/platform-server';
2+
import { platformDynamicServer } from '@angular/platform-server';
3+
import { DOCUMENT } from '@angular/platform-browser';
34
import { ResourceLoader } from '@angular/compiler';
45

56
import { REQUEST, ORIGIN_URL } from './tokens';
67
import { FileLoader } from './file-loader';
7-
88
import { IEngineOptions } from './interfaces/engine-options';
9-
import { DOCUMENT } from '@angular/platform-browser';
9+
import { IEngineRenderResult } from './interfaces/engine-render-result';
10+
import { renderModuleFactory } from './platform-server-utils';
1011

1112
/* @internal */
1213
export class UniversalData {
13-
public static appNode = '';
14-
public static title = '';
15-
public static scripts = '';
16-
public static styles = '';
17-
public static meta = '';
18-
public static links = '';
14+
public appNode = '';
15+
public title = '';
16+
public scripts = '';
17+
public styles = '';
18+
public meta = '';
19+
public links = '';
1920
}
2021

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

2425
/* @internal */
25-
function beforeAppSerialized(
26+
function _getUniversalData(
2627
doc: any /* TODO: type definition for Domino - DomAPI Spec (similar to "Document") */
27-
) {
28-
29-
return () => {
30-
const STYLES = [];
31-
const SCRIPTS = [];
32-
const META = [];
33-
const LINKS = [];
34-
35-
for (let i = 0; i < doc.head.children.length; i++) {
36-
const element = doc.head.children[i];
37-
const tagName = element.tagName.toUpperCase();
38-
39-
switch (tagName) {
40-
case 'SCRIPT':
41-
SCRIPTS.push(element.outerHTML);
42-
break;
43-
case 'STYLE':
44-
STYLES.push(element.outerHTML);
45-
break;
46-
case 'LINK':
47-
LINKS.push(element.outerHTML);
48-
break;
49-
case 'META':
50-
META.push(element.outerHTML);
51-
break;
52-
default:
53-
break;
54-
}
28+
): UniversalData {
29+
30+
const STYLES = [];
31+
const SCRIPTS = [];
32+
const META = [];
33+
const LINKS = [];
34+
35+
for (let i = 0; i < doc.head.children.length; i++) {
36+
const element = doc.head.children[i];
37+
const tagName = element.tagName.toUpperCase();
38+
39+
switch (tagName) {
40+
case 'SCRIPT':
41+
SCRIPTS.push(element.outerHTML);
42+
break;
43+
case 'STYLE':
44+
STYLES.push(element.outerHTML);
45+
break;
46+
case 'LINK':
47+
LINKS.push(element.outerHTML);
48+
break;
49+
case 'META':
50+
META.push(element.outerHTML);
51+
break;
52+
default:
53+
break;
54+
}
55+
}
56+
57+
for (let i = 0; i < doc.body.children.length; i++) {
58+
const element: Element = doc.body.children[i];
59+
const tagName = element.tagName.toUpperCase();
60+
61+
switch (tagName) {
62+
case 'SCRIPT':
63+
SCRIPTS.push(element.outerHTML);
64+
break;
65+
case 'STYLE':
66+
STYLES.push(element.outerHTML);
67+
break;
68+
case 'LINK':
69+
LINKS.push(element.outerHTML);
70+
break;
71+
case 'META':
72+
META.push(element.outerHTML);
73+
break;
74+
default:
75+
break;
5576
}
77+
}
5678

57-
UniversalData.title = doc.title;
58-
UniversalData.appNode = doc.querySelector(appSelector).outerHTML;
59-
UniversalData.scripts = SCRIPTS.join(' ');
60-
UniversalData.styles = STYLES.join(' ');
61-
UniversalData.meta = META.join(' ');
62-
UniversalData.links = LINKS.join(' ');
79+
return {
80+
title: doc.title,
81+
appNode: doc.querySelector(appSelector).outerHTML,
82+
scripts: SCRIPTS.join('\n'),
83+
styles: STYLES.join('\n'),
84+
meta: META.join('\n'),
85+
links: LINKS.join('\n')
6386
};
6487
}
6588

66-
67-
export function ngAspnetCoreEngine(
68-
options: IEngineOptions
69-
): Promise<{ html: string, globals: { styles: string, title: string, meta: string, transferData?: {}, [key: string]: any } }> {
89+
export function ngAspnetCoreEngine(options: IEngineOptions): Promise<IEngineRenderResult> {
7090

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

97117
const extraProviders = options.providers.concat(
98-
...options.providers,
99-
[{
100-
provide: ORIGIN_URL,
101-
useValue: options.request.origin
102-
}, {
103-
provide: REQUEST,
104-
useValue: options.request.data.request
105-
}, {
106-
provide: BEFORE_APP_SERIALIZED,
107-
useFactory: beforeAppSerialized, multi: true, deps: [ DOCUMENT ]
108-
}
118+
[{
119+
provide: ORIGIN_URL,
120+
useValue: options.request.origin
121+
}, {
122+
provide: REQUEST,
123+
useValue: options.request.data.request
124+
}
109125
]
110126
);
111127

@@ -117,18 +133,20 @@ export function ngAspnetCoreEngine(
117133
extraProviders: extraProviders
118134
});
119135
})
120-
.then(() => {
136+
.then(result => {
137+
const doc = result.moduleRef.injector.get(DOCUMENT);
138+
const universalData = _getUniversalData(doc);
121139

122140
resolve({
123-
html: UniversalData.appNode,
141+
html: universalData.appNode,
142+
moduleRef: result.moduleRef,
124143
globals: {
125-
styles: UniversalData.styles,
126-
title: UniversalData.title,
127-
scripts: UniversalData.scripts,
128-
meta: UniversalData.meta,
129-
links: UniversalData.links
144+
styles: universalData.styles,
145+
title: universalData.title,
146+
scripts: universalData.scripts,
147+
meta: universalData.meta,
148+
links: universalData.links
130149
}
131-
132150
});
133151
}, (err) => {
134152
reject(err);
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Copied from @angular/platform-server utils. https://github.com/angular/angular/blob/master/packages/platform-server/src/utils.ts
3+
Github issue to avoid copy/paste: https://github.com/angular/angular/issues/22049#issuecomment-363638743
4+
*/
5+
6+
import { ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider, Type } from '@angular/core';
7+
import { ɵTRANSITION_ID } from '@angular/platform-browser';
8+
import { filter } from 'rxjs/operator/filter';
9+
import { first } from 'rxjs/operator/first';
10+
import { toPromise } from 'rxjs/operator/toPromise';
11+
import { platformDynamicServer, platformServer, BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformState } from '@angular/platform-server';
12+
13+
interface PlatformOptions {
14+
document?: string;
15+
url?: string;
16+
extraProviders?: StaticProvider[];
17+
}
18+
19+
export interface ModuleRenderResult<T> {
20+
html: string;
21+
moduleRef: NgModuleRef<T>;
22+
}
23+
24+
function _getPlatform(
25+
platformFactory: (extraProviders: StaticProvider[]) => PlatformRef,
26+
options: PlatformOptions): PlatformRef {
27+
const extraProviders = options.extraProviders ? options.extraProviders : [];
28+
return platformFactory([
29+
{ provide: INITIAL_CONFIG, useValue: { document: options.document, url: options.url } },
30+
extraProviders
31+
]);
32+
}
33+
34+
function _render<T>(
35+
platform: PlatformRef, moduleRefPromise: Promise<NgModuleRef<T>>): Promise<ModuleRenderResult<T>> {
36+
return moduleRefPromise.then(moduleRef => {
37+
const transitionId = moduleRef.injector.get(ɵTRANSITION_ID, null);
38+
if (!transitionId) {
39+
throw new Error(
40+
`renderModule[Factory]() requires the use of BrowserModule.withServerTransition() to ensure
41+
the server-rendered app can be properly bootstrapped into a client app.`);
42+
}
43+
const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
44+
return toPromise
45+
.call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable)))
46+
.then(() => {
47+
const platformState = platform.injector.get(PlatformState);
48+
49+
// Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
50+
const callbacks = moduleRef.injector.get(BEFORE_APP_SERIALIZED, null);
51+
if (callbacks) {
52+
for (const callback of callbacks) {
53+
try {
54+
callback();
55+
} catch (e) {
56+
// Ignore exceptions.
57+
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
58+
}
59+
}
60+
}
61+
62+
const output = platformState.renderToString();
63+
platform.destroy();
64+
return { html: output, moduleRef };
65+
});
66+
});
67+
}
68+
69+
/**
70+
* Renders a Module to string.
71+
*
72+
* `document` is the full document HTML of the page to render, as a string.
73+
* `url` is the URL for the current render request.
74+
* `extraProviders` are the platform level providers for the current render request.
75+
*
76+
* Do not use this in a production server environment. Use pre-compiled {@link NgModuleFactory} with
77+
* {@link renderModuleFactory} instead.
78+
*
79+
* @experimental
80+
*/
81+
export function renderModule<T>(
82+
module: Type<T>, options: { document?: string, url?: string, extraProviders?: StaticProvider[] }):
83+
Promise<ModuleRenderResult<T>> {
84+
const platform = _getPlatform(platformDynamicServer, options);
85+
return _render(platform, platform.bootstrapModule(module));
86+
}
87+
88+
/**
89+
* Renders a {@link NgModuleFactory} to string.
90+
*
91+
* `document` is the full document HTML of the page to render, as a string.
92+
* `url` is the URL for the current render request.
93+
* `extraProviders` are the platform level providers for the current render request.
94+
*
95+
* @experimental
96+
*/
97+
export function renderModuleFactory<T>(
98+
moduleFactory: NgModuleFactory<T>,
99+
options: { document?: string, url?: string, extraProviders?: StaticProvider[] }):
100+
Promise<ModuleRenderResult<T>> {
101+
const platform = _getPlatform(platformServer, options);
102+
return _render(platform, platform.bootstrapModuleFactory(moduleFactory));
103+
}

0 commit comments

Comments
 (0)