Skip to content

Commit d2d9bbf

Browse files
alan-agius4alxhub
authored andcommitted
fix(platform-server): call onSerialize when state is empty (#47888)
Commit a0b2d36#diff-3975e0ee5aa3e06ecbcd76f5fa5134612f7fd2e6802ca7d370973bd410aab55cR25-R31 changed the serialization phase logic so that when the state is empty the script tag is not added to the document. As a side effect, this caused the `toJson` not called which caused the `onSerialize` callbacks also not to be called. These callbacks are used to provide the value for a key when `toJson` is called. Example: ngrx/platform#101 (comment) Closes #47172 PR Close #47888
1 parent 8707475 commit d2d9bbf

File tree

4 files changed

+96
-104
lines changed

4 files changed

+96
-104
lines changed

packages/platform-server/src/transfer_state.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,20 @@ export const TRANSFER_STATE_SERIALIZATION_PROVIDERS: Provider[] = [{
2121

2222
function serializeTransferStateFactory(doc: Document, appId: string, transferStore: TransferState) {
2323
return () => {
24+
// The `.toJSON` here causes the `onSerialize` callbacks to be called.
25+
// These callbacks can be used to provide the value for a given key.
26+
const content = transferStore.toJson();
27+
2428
if (transferStore.isEmpty) {
2529
// The state is empty, nothing to transfer,
2630
// avoid creating an extra `<script>` tag in this case.
2731
return;
2832
}
33+
2934
const script = doc.createElement('script');
3035
script.id = appId + '-state';
3136
script.setAttribute('type', 'application/json');
32-
script.textContent = escapeHtml(transferStore.toJson());
37+
script.textContent = escapeHtml(content);
3338
doc.body.appendChild(script);
3439
};
3540
}

packages/platform-server/test/integration_spec.ts

Lines changed: 3 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import {animate, AnimationBuilder, state, style, transition, trigger} from '@ang
1010
import {DOCUMENT, isPlatformServer, PlatformLocation, ɵgetDOM as getDOM} from '@angular/common';
1111
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
1212
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
13-
import {ApplicationRef, CompilerFactory, Component, destroyPlatform, getPlatform, HostBinding, HostListener, importProvidersFrom, Inject, Injectable, Input, NgModule, NgZone, OnInit, PLATFORM_ID, PlatformRef, Type, ViewEncapsulation} from '@angular/core';
13+
import {ApplicationRef, CompilerFactory, Component, destroyPlatform, getPlatform, HostListener, Inject, Injectable, Input, NgModule, NgZone, PLATFORM_ID, PlatformRef, ViewEncapsulation} from '@angular/core';
1414
import {inject, TestBed, waitForAsync} from '@angular/core/testing';
15-
import {BrowserModule, makeStateKey, Title, TransferState} from '@angular/platform-browser';
16-
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformState, renderModule, renderModuleFactory, ServerModule, ServerTransferStateModule} from '@angular/platform-server';
15+
import {BrowserModule, Title} from '@angular/platform-browser';
16+
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformState, renderModule, renderModuleFactory, ServerModule} from '@angular/platform-server';
1717
import {Observable} from 'rxjs';
1818
import {first} from 'rxjs/operators';
1919

@@ -389,19 +389,6 @@ const [MyHostComponentStandalone, _] = createFalseAttributesComponents(true);
389389
class FalseAttributesModule {
390390
}
391391

392-
@Component({selector: 'app', template: '<div [innerText]="foo"></div>'})
393-
class InnerTextComponent {
394-
foo = 'Some text';
395-
}
396-
397-
@NgModule({
398-
declarations: [InnerTextComponent],
399-
bootstrap: [InnerTextComponent],
400-
imports: [ServerModule, BrowserModule.withServerTransition({appId: 'inner-text'})]
401-
})
402-
class InnerTextModule {
403-
}
404-
405392
function createMyInputComponent(standalone: boolean) {
406393
@Component({
407394
standalone,
@@ -449,47 +436,6 @@ const HTMLTypesAppStandalone = createHTMLTypesApp(true);
449436
class HTMLTypesModule {
450437
}
451438

452-
const TEST_KEY = makeStateKey<number>('test');
453-
const STRING_KEY = makeStateKey<string>('testString');
454-
455-
@Component({selector: 'app', template: 'Works!'})
456-
class TransferComponent {
457-
constructor(private transferStore: TransferState) {}
458-
ngOnInit() {
459-
this.transferStore.set(TEST_KEY, 10);
460-
}
461-
}
462-
463-
@Component({selector: 'esc-app', template: 'Works!'})
464-
class EscapedComponent {
465-
constructor(private transferStore: TransferState) {}
466-
ngOnInit() {
467-
this.transferStore.set(STRING_KEY, '</script><script>alert(\'Hello&\' + "World");');
468-
}
469-
}
470-
471-
@NgModule({
472-
bootstrap: [TransferComponent],
473-
declarations: [TransferComponent],
474-
imports: [
475-
BrowserModule.withServerTransition({appId: 'transfer'}),
476-
ServerModule,
477-
]
478-
})
479-
class TransferStoreModule {
480-
}
481-
482-
@NgModule({
483-
bootstrap: [EscapedComponent],
484-
declarations: [EscapedComponent],
485-
imports: [
486-
BrowserModule.withServerTransition({appId: 'transfer'}),
487-
ServerModule,
488-
]
489-
})
490-
class EscapedTransferStoreModule {
491-
}
492-
493439
function createMyHiddenComponent(standalone: boolean) {
494440
@Component({
495441
standalone,
@@ -1310,50 +1256,5 @@ describe('platform-server integration', () => {
13101256
});
13111257
});
13121258
});
1313-
1314-
describe('ServerTransferStoreModule', () => {
1315-
let called = false;
1316-
const defaultExpectedOutput =
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>';
1318-
1319-
beforeEach(() => {
1320-
called = false;
1321-
});
1322-
afterEach(() => {
1323-
expect(called).toBe(true);
1324-
});
1325-
1326-
it('adds transfer script tag when using renderModule', waitForAsync(() => {
1327-
renderModule(TransferStoreModule, {document: '<app></app>'}).then(output => {
1328-
expect(output).toBe(defaultExpectedOutput);
1329-
called = true;
1330-
});
1331-
}));
1332-
1333-
it('adds transfer script tag when using renderModuleFactory',
1334-
waitForAsync(inject([PlatformRef], (defaultPlatform: PlatformRef) => {
1335-
const compilerFactory: CompilerFactory =
1336-
defaultPlatform.injector.get(CompilerFactory, null)!;
1337-
const moduleFactory =
1338-
compilerFactory.createCompiler().compileModuleSync(TransferStoreModule);
1339-
renderModuleFactory(moduleFactory, {document: '<app></app>'}).then(output => {
1340-
expect(output).toBe(defaultExpectedOutput);
1341-
called = true;
1342-
});
1343-
})));
1344-
1345-
it('cannot break out of <script> tag in serialized output', waitForAsync(() => {
1346-
renderModule(EscapedTransferStoreModule, {
1347-
document: '<esc-app></esc-app>'
1348-
}).then(output => {
1349-
expect(output).toBe(
1350-
'<html><head></head><body><esc-app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</esc-app>' +
1351-
'<script id="transfer-state" type="application/json">' +
1352-
'{&q;testString&q;:&q;&l;/script&g;&l;script&g;' +
1353-
'alert(&s;Hello&a;&s; + \\&q;World\\&q;);&q;}</script></body></html>');
1354-
called = true;
1355-
});
1356-
}));
1357-
});
13581259
});
13591260
})();
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Component, NgModule,} from '@angular/core';
10+
import {BrowserModule, makeStateKey, TransferState} from '@angular/platform-browser';
11+
import {renderModule, ServerModule} from '@angular/platform-server';
12+
13+
describe('transfer_state', () => {
14+
const defaultExpectedOutput =
15+
'<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>';
16+
17+
it('adds transfer script tag when using renderModule', async () => {
18+
const STATE_KEY = makeStateKey<number>('test');
19+
20+
@Component({selector: 'app', template: 'Works!'})
21+
class TransferComponent {
22+
constructor(private transferStore: TransferState) {
23+
this.transferStore.set(STATE_KEY, 10);
24+
}
25+
}
26+
27+
@NgModule({
28+
bootstrap: [TransferComponent],
29+
declarations: [TransferComponent],
30+
imports: [BrowserModule.withServerTransition({appId: 'transfer'}), ServerModule],
31+
})
32+
class TransferStoreModule {
33+
}
34+
35+
const output = await renderModule(TransferStoreModule, {document: '<app></app>'});
36+
expect(output).toBe(defaultExpectedOutput);
37+
});
38+
39+
it('cannot break out of <script> tag in serialized output', async () => {
40+
const STATE_KEY = makeStateKey<string>('testString');
41+
42+
@Component({selector: 'esc-app', template: 'Works!'})
43+
class EscapedComponent {
44+
constructor(private transferStore: TransferState) {
45+
this.transferStore.set(STATE_KEY, '</script><script>alert(\'Hello&\' + "World");');
46+
}
47+
}
48+
@NgModule({
49+
bootstrap: [EscapedComponent],
50+
declarations: [EscapedComponent],
51+
imports: [BrowserModule.withServerTransition({appId: 'transfer'}), ServerModule],
52+
})
53+
class EscapedTransferStoreModule {
54+
}
55+
56+
const output =
57+
await renderModule(EscapedTransferStoreModule, {document: '<esc-app></esc-app>'});
58+
expect(output).toBe(
59+
'<html><head></head><body><esc-app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</esc-app>' +
60+
'<script id="transfer-state" type="application/json">' +
61+
'{&q;testString&q;:&q;&l;/script&g;&l;script&g;' +
62+
'alert(&s;Hello&a;&s; + \\&q;World\\&q;);&q;}</script></body></html>');
63+
});
64+
65+
it('adds transfer script tag when setting state during onSerialize', async () => {
66+
const STATE_KEY = makeStateKey<number>('test');
67+
68+
@Component({selector: 'app', template: 'Works!'})
69+
class TransferComponent {
70+
constructor(private transferStore: TransferState) {
71+
this.transferStore.onSerialize(STATE_KEY, () => 10);
72+
}
73+
}
74+
75+
@NgModule({
76+
bootstrap: [TransferComponent],
77+
declarations: [TransferComponent],
78+
imports: [BrowserModule.withServerTransition({appId: 'transfer'}), ServerModule],
79+
})
80+
class TransferStoreModule {
81+
}
82+
83+
const output = await renderModule(TransferStoreModule, {document: '<app></app>'});
84+
expect(output).toBe(defaultExpectedOutput);
85+
});
86+
});

packages/platform-server/testing/src/server.ts

Lines changed: 1 addition & 1 deletion
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 {createPlatformFactory, NgModule, PlatformRef, StaticProvider} from '@angular/core';
9+
import {createPlatformFactory, NgModule} from '@angular/core';
1010
import {BrowserDynamicTestingModule, ɵplatformCoreDynamicTesting as platformCoreDynamicTesting} from '@angular/platform-browser-dynamic/testing';
1111
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
1212
import {ɵINTERNAL_SERVER_PLATFORM_PROVIDERS as INTERNAL_SERVER_PLATFORM_PROVIDERS, ɵSERVER_RENDER_PROVIDERS as SERVER_RENDER_PROVIDERS} from '@angular/platform-server';

0 commit comments

Comments
 (0)