Skip to content

Commit 6c7cd6b

Browse files
AndrewKushnirdylhunn
authored andcommitted
refactor(platform-server): add a marker to specify how a page was rendered (angular#47103)
This commit updates the `renderApplication`, `renderModule` and `renderModuleFactory` functions to append a special marker (in a form of an attribute, called `ng-server-context`) to the component host elements. This marker is needed to analyze how a page was rendered. PR Close angular#47103
1 parent ec9ee8e commit 6c7cd6b

File tree

4 files changed

+156
-36
lines changed

4 files changed

+156
-36
lines changed

goldens/public-api/platform-server/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function renderApplication<T>(rootComponent: Type<T>, options: {
5858
}): Promise<string>;
5959

6060
// @public
61-
export function renderModule<T>(module: Type<T>, options: {
61+
export function renderModule<T>(moduleType: Type<T>, options: {
6262
document?: string | Document;
6363
url?: string;
6464
extraProviders?: StaticProvider[];

packages/platform-server/src/private_export.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99
export {setDomTypes as ɵsetDomTypes} from './domino_adapter';
1010
export {INTERNAL_SERVER_PLATFORM_PROVIDERS as ɵINTERNAL_SERVER_PLATFORM_PROVIDERS, SERVER_RENDER_PROVIDERS as ɵSERVER_RENDER_PROVIDERS} from './server';
1111
export {ServerRendererFactory2 as ɵServerRendererFactory2} from './server_renderer';
12+
export {SERVER_CONTEXT as ɵSERVER_CONTEXT} from './utils';

packages/platform-server/src/utils.ts

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ApplicationRef, ImportedNgModuleProviders, importProvidersFrom, NgModuleFactory, NgModuleRef, PlatformRef, Provider, StaticProvider, Type, ɵinternalCreateApplication as internalCreateApplication, ɵisPromise} from '@angular/core';
9+
import {ApplicationRef, ImportedNgModuleProviders, importProvidersFrom, InjectionToken, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵinternalCreateApplication as internalCreateApplication, ɵisPromise} from '@angular/core';
1010
import {BrowserModule, ɵTRANSITION_ID} from '@angular/platform-browser';
1111
import {first} from 'rxjs/operators';
1212

@@ -31,6 +31,20 @@ function _getPlatform(
3131
]);
3232
}
3333

34+
/**
35+
* Adds the `ng-server-context` attribute to host elements of all bootstrapped components
36+
* within a given application.
37+
*/
38+
function appendServerContextInfo(serverContext: string, applicationRef: ApplicationRef) {
39+
applicationRef.components.forEach(componentRef => {
40+
const renderer = componentRef.injector.get(Renderer2);
41+
const element = componentRef.location.nativeElement;
42+
if (element) {
43+
renderer.setAttribute(element, 'ng-server-context', serverContext);
44+
}
45+
});
46+
}
47+
3448
function _render<T>(
3549
platform: PlatformRef,
3650
bootstrapPromise: Promise<NgModuleRef<T>|ApplicationRef>): Promise<string> {
@@ -45,9 +59,13 @@ the server-rendered app can be properly bootstrapped into a client app.`);
4559
const applicationRef: ApplicationRef = moduleOrApplicationRef instanceof ApplicationRef ?
4660
moduleOrApplicationRef :
4761
environmentInjector.get(ApplicationRef);
62+
const serverContext =
63+
sanitizeServerContext(environmentInjector.get(SERVER_CONTEXT, DEFAULT_SERVER_CONTEXT));
4864
return applicationRef.isStable.pipe((first((isStable: boolean) => isStable)))
4965
.toPromise()
5066
.then(() => {
67+
appendServerContextInfo(serverContext, applicationRef);
68+
5169
const platformState = platform.injector.get(PlatformState);
5270

5371
const asyncPromises: Promise<any>[] = [];
@@ -93,22 +111,47 @@ the server-rendered app can be properly bootstrapped into a client app.`);
93111
}
94112

95113
/**
96-
* Renders a Module to string.
114+
* Specifies the value that should be used if no server context value has been provided.
115+
*/
116+
const DEFAULT_SERVER_CONTEXT = 'other';
117+
118+
/**
119+
* An internal token that allows providing extra information about the server context
120+
* (e.g. whether SSR or SSG was used). The value is a string and characters other
121+
* than [a-zA-Z0-9\-] are removed. See the default value in `DEFAULT_SERVER_CONTEXT` const.
122+
*/
123+
export const SERVER_CONTEXT = new InjectionToken<string>('SERVER_CONTEXT');
124+
125+
/**
126+
* Sanitizes provided server context:
127+
* - removes all characters other than a-z, A-Z, 0-9 and `-`
128+
* - returns `other` if nothing is provided or the string is empty after sanitization
129+
*/
130+
function sanitizeServerContext(serverContext: string): string {
131+
const context = serverContext.replace(/[^a-zA-Z0-9\-]/g, '');
132+
return context.length > 0 ? context : DEFAULT_SERVER_CONTEXT;
133+
}
134+
135+
/**
136+
* Bootstraps an application using provided NgModule and serializes the page content to string.
97137
*
98-
* `document` is the document of the page to render, either as an HTML string or
99-
* as a reference to the `document` instance.
100-
* `url` is the URL for the current render request.
101-
* `extraProviders` are the platform level providers for the current render request.
138+
* @param moduleType A reference to an NgModule that should be used for bootstrap.
139+
* @param options Additional configuration for the render operation:
140+
* - `document` - the document of the page to render, either as an HTML string or
141+
* as a reference to the `document` instance.
142+
* - `url` - the URL for the current render request.
143+
* - `extraProviders` - set of platform level providers for the current render request.
102144
*
103145
* @publicApi
104146
*/
105-
export function renderModule<T>(
106-
module: Type<T>,
107-
options: {document?: string|Document, url?: string, extraProviders?: StaticProvider[]}):
108-
Promise<string> {
147+
export function renderModule<T>(moduleType: Type<T>, options: {
148+
document?: string|Document,
149+
url?: string,
150+
extraProviders?: StaticProvider[],
151+
}): Promise<string> {
109152
const {document, url, extraProviders: platformProviders} = options;
110153
const platform = _getPlatform(platformDynamicServer, {document, url, platformProviders});
111-
return _render(platform, platform.bootstrapModule(module));
154+
return _render(platform, platform.bootstrapModule(moduleType));
112155
}
113156

114157
/**
@@ -137,6 +180,7 @@ export function renderModule<T>(
137180
* - `url` - the URL for the current render request.
138181
* - `providers` - set of application level providers for the current render request.
139182
* - `platformProviders` - the platform level providers for the current render request.
183+
*
140184
* @returns A Promise, that returns serialized (to a string) rendered page, once resolved.
141185
*
142186
* @publicApi
@@ -161,22 +205,28 @@ export function renderApplication<T>(rootComponent: Type<T>, options: {
161205
}
162206

163207
/**
164-
* Renders a {@link NgModuleFactory} to string.
208+
* Bootstraps an application using provided {@link NgModuleFactory} and serializes the page content
209+
* to string.
165210
*
166-
* `document` is the full document HTML of the page to render, as a string.
167-
* `url` is the URL for the current render request.
168-
* `extraProviders` are the platform level providers for the current render request.
211+
* @param moduleFactory An instance of the {@link NgModuleFactory} that should be used for
212+
* bootstrap.
213+
* @param options Additional configuration for the render operation:
214+
* - `document` - the document of the page to render, either as an HTML string or
215+
* as a reference to the `document` instance.
216+
* - `url` - the URL for the current render request.
217+
* - `extraProviders` - set of platform level providers for the current render request.
169218
*
170219
* @publicApi
171220
*
172221
* @deprecated
173222
* This symbol is no longer necessary as of Angular v13.
174223
* Use {@link renderModule} API instead.
175224
*/
176-
export function renderModuleFactory<T>(
177-
moduleFactory: NgModuleFactory<T>,
178-
options: {document?: string, url?: string, extraProviders?: StaticProvider[]}):
179-
Promise<string> {
225+
export function renderModuleFactory<T>(moduleFactory: NgModuleFactory<T>, options: {
226+
document?: string,
227+
url?: string,
228+
extraProviders?: StaticProvider[],
229+
}): Promise<string> {
180230
const {document, url, extraProviders: platformProviders} = options;
181231
const platform = _getPlatform(platformServer, {document, url, platformProviders});
182232
return _render(platform, platform.bootstrapModuleFactory(moduleFactory));

packages/platform-server/test/integration_spec.ts

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformSt
1717
import {Observable} from 'rxjs';
1818
import {first} from 'rxjs/operators';
1919

20-
import {renderApplication} from '../src/utils';
20+
import {renderApplication, SERVER_CONTEXT} from '../src/utils';
2121

2222
function createMyServerApp(standalone: boolean) {
2323
@Component({
@@ -696,7 +696,7 @@ describe('platform-server integration', () => {
696696
let doc: string;
697697
let called: boolean;
698698
let expectedOutput =
699-
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">Works!<h1 textcontent="fine">fine</h1></app></body></html>';
699+
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!<h1 textcontent="fine">fine</h1></app></body></html>';
700700

701701
beforeEach(() => {
702702
// PlatformConfig takes in a parsed document so that it can be cached across requests.
@@ -713,11 +713,17 @@ describe('platform-server integration', () => {
713713

714714
platform.bootstrapModule(AsyncServerModule)
715715
.then((moduleRef) => {
716-
const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
716+
const applicationRef = moduleRef.injector.get(ApplicationRef);
717717
return applicationRef.isStable.pipe(first((isStable: boolean) => isStable))
718718
.toPromise();
719719
})
720720
.then((b) => {
721+
// Note: the `ng-server-context` is not present in this output, since
722+
// `renderModule` or `renderApplication` functions are not used here.
723+
const expectedOutput =
724+
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">' +
725+
'Works!<h1 textcontent="fine">fine</h1></app></body></html>';
726+
721727
expect(platform.injector.get(PlatformState).renderToString()).toBe(expectedOutput);
722728
platform.destroy();
723729
called = true;
@@ -772,7 +778,8 @@ describe('platform-server integration', () => {
772778
.then(output => {
773779
expect(output).toBe(
774780
'<html><head><title>fakeTitle</title></head>' +
775-
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!<h1 textcontent="fine">fine</h1></app>' +
781+
'<body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">' +
782+
'Works!<h1 textcontent="fine">fine</h1></app>' +
776783
'<!--test marker--></body></html>');
777784
called = true;
778785
})
@@ -789,7 +796,7 @@ describe('platform-server integration', () => {
789796
renderModule(SVGServerModule, options);
790797
bootstrap.then(output => {
791798
expect(output).toBe(
792-
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">' +
799+
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">' +
793800
'<svg><use xlink:href="#clear"></use></svg></app></body></html>');
794801
called = true;
795802
});
@@ -830,7 +837,69 @@ describe('platform-server integration', () => {
830837
renderModule(ExampleStylesModule, options);
831838
bootstrap.then(output => {
832839
expect(output).toMatch(
833-
/<html><head><style ng-transition="example-styles">div\[_ngcontent-sc\d+\] {color: blue; } \[_nghost-sc\d+\] { color: red; }<\/style><\/head><body><app _nghost-sc\d+="" ng-version="0.0.0-PLACEHOLDER"><div _ngcontent-sc\d+="">Works!<\/div><\/app><\/body><\/html>/);
840+
/<html><head><style ng-transition="example-styles">div\[_ngcontent-sc\d+\] {color: blue; } \[_nghost-sc\d+\] { color: red; }<\/style><\/head><body><app _nghost-sc\d+="" ng-version="0.0.0-PLACEHOLDER" ng-server-context="other"><div _ngcontent-sc\d+="">Works!<\/div><\/app><\/body><\/html>/);
841+
called = true;
842+
});
843+
}));
844+
845+
it('adds the `ng-server-context` attribute to host elements', waitForAsync(() => {
846+
const options = {
847+
document: doc,
848+
};
849+
const providers = [{
850+
provide: SERVER_CONTEXT,
851+
useValue: 'ssg',
852+
}];
853+
const bootstrap = isStandalone ?
854+
renderApplication(
855+
MyStylesAppStandalone,
856+
{...options, platformProviders: providers, appId: 'example-styles'}) :
857+
renderModule(ExampleStylesModule, {...options, extraProviders: providers});
858+
bootstrap.then(output => {
859+
expect(output).toMatch(
860+
/<html><head><style ng-transition="example-styles">div\[_ngcontent-sc\d+\] {color: blue; } \[_nghost-sc\d+\] { color: red; }<\/style><\/head><body><app _nghost-sc\d+="" ng-version="0.0.0-PLACEHOLDER" ng-server-context="ssg"><div _ngcontent-sc\d+="">Works!<\/div><\/app><\/body><\/html>/);
861+
called = true;
862+
});
863+
}));
864+
865+
it('sanitizes the `serverContext` value', waitForAsync(() => {
866+
const options = {
867+
document: doc,
868+
};
869+
const providers = [{
870+
provide: SERVER_CONTEXT,
871+
useValue: '!!!Some extra chars&& --><!--',
872+
}];
873+
const bootstrap = isStandalone ?
874+
renderApplication(
875+
MyStylesAppStandalone,
876+
{...options, platformProviders: providers, appId: 'example-styles'}) :
877+
renderModule(ExampleStylesModule, {...options, extraProviders: providers});
878+
bootstrap.then(output => {
879+
// All symbols other than [a-zA-Z0-9\-] are removed
880+
expect(output).toMatch(/ng-server-context="Someextrachars----"/);
881+
called = true;
882+
});
883+
}));
884+
885+
it('uses `other` as the `serverContext` value when all symbols are removed after sanitization',
886+
waitForAsync(() => {
887+
const options = {
888+
document: doc,
889+
};
890+
const providers = [{
891+
provide: SERVER_CONTEXT,
892+
useValue: '!!! &&<>',
893+
}];
894+
const bootstrap = isStandalone ?
895+
renderApplication(
896+
MyStylesAppStandalone,
897+
{...options, platformProviders: providers, appId: 'example-styles'}) :
898+
renderModule(ExampleStylesModule, {...options, extraProviders: providers});
899+
bootstrap.then(output => {
900+
// All symbols other than [a-zA-Z0-9\-] are removed,
901+
// the `other` is used as the default.
902+
expect(output).toMatch(/ng-server-context="other"/);
834903
called = true;
835904
});
836905
}));
@@ -842,7 +911,7 @@ describe('platform-server integration', () => {
842911
renderModule(FalseAttributesModule, options);
843912
bootstrap.then(output => {
844913
expect(output).toBe(
845-
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">' +
914+
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">' +
846915
'<my-child ng-reflect-attr="false">Works!</my-child></app></body></html>');
847916
called = true;
848917
});
@@ -855,7 +924,7 @@ describe('platform-server integration', () => {
855924
renderModule(NameModule, options);
856925
bootstrap.then(output => {
857926
expect(output).toBe(
858-
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">' +
927+
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">' +
859928
'<input name=""></app></body></html>');
860929
called = true;
861930
});
@@ -872,7 +941,7 @@ describe('platform-server integration', () => {
872941
renderModule(HTMLTypesModule, options);
873942
bootstrap.then(output => {
874943
expect(output).toBe(
875-
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">' +
944+
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">' +
876945
'<div><b>foo</b> bar</div></app></body></html>');
877946
called = true;
878947
});
@@ -885,7 +954,7 @@ describe('platform-server integration', () => {
885954
renderModule(HiddenModule, options);
886955
bootstrap.then(output => {
887956
expect(output).toBe(
888-
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">' +
957+
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">' +
889958
'<input hidden=""><input></app></body></html>');
890959
called = true;
891960
});
@@ -902,7 +971,7 @@ describe('platform-server integration', () => {
902971
// title should be added by the render hook.
903972
expect(output).toBe(
904973
'<html><head><title>RenderHook</title></head><body>' +
905-
'<app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
974+
'<app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</app></body></html>');
906975
called = true;
907976
});
908977
}));
@@ -919,7 +988,7 @@ describe('platform-server integration', () => {
919988
// title should be added by the render hook.
920989
expect(output).toBe(
921990
'<html><head><title>RenderHook</title><meta name="description"></head>' +
922-
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
991+
'<body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</app></body></html>');
923992
expect(consoleSpy).toHaveBeenCalled();
924993
called = true;
925994
});
@@ -936,7 +1005,7 @@ describe('platform-server integration', () => {
9361005
// title should be added by the render hook.
9371006
expect(output).toBe(
9381007
'<html><head><title>AsyncRenderHook</title></head><body>' +
939-
'<app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
1008+
'<app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</app></body></html>');
9401009
called = true;
9411010
});
9421011
}));
@@ -954,7 +1023,7 @@ describe('platform-server integration', () => {
9541023
// title should be added by the render hook.
9551024
expect(output).toBe(
9561025
'<html><head><meta name="description"><title>AsyncRenderHook</title></head>' +
957-
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
1026+
'<body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</app></body></html>');
9581027
expect(consoleSpy).toHaveBeenCalled();
9591028
called = true;
9601029
});
@@ -1245,7 +1314,7 @@ describe('platform-server integration', () => {
12451314
describe('ServerTransferStoreModule', () => {
12461315
let called = false;
12471316
const defaultExpectedOutput =
1248-
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER">Works!</app><script id="transfer-state" type="application/json">{&q;test&q;:10}</script></body></html>';
1317+
'<html><head></head><body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</app><script id="transfer-state" type="application/json">{&q;test&q;:10}</script></body></html>';
12491318

12501319
beforeEach(() => {
12511320
called = false;
@@ -1278,7 +1347,7 @@ describe('platform-server integration', () => {
12781347
document: '<esc-app></esc-app>'
12791348
}).then(output => {
12801349
expect(output).toBe(
1281-
'<html><head></head><body><esc-app ng-version="0.0.0-PLACEHOLDER">Works!</esc-app>' +
1350+
'<html><head></head><body><esc-app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</esc-app>' +
12821351
'<script id="transfer-state" type="application/json">' +
12831352
'{&q;testString&q;:&q;&l;/script&g;&l;script&g;' +
12841353
'alert(&s;Hello&a;&s; + \\&q;World\\&q;);&q;}</script></body></html>');

0 commit comments

Comments
 (0)