Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(platform-server): call onSerialize when state is empty #47888

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/platform-server/src/transfer_state.ts
Expand Up @@ -21,15 +21,20 @@ export const TRANSFER_STATE_SERIALIZATION_PROVIDERS: Provider[] = [{

function serializeTransferStateFactory(doc: Document, appId: string, transferStore: TransferState) {
return () => {
// The `.toJSON` here causes the `onSerialize` callbacks to be called.
// These callbacks can be used to provide the value for a given key.
const content = transferStore.toJson();

if (transferStore.isEmpty) {
// The state is empty, nothing to transfer,
// avoid creating an extra `<script>` tag in this case.
return;
}

const script = doc.createElement('script');
script.id = appId + '-state';
script.setAttribute('type', 'application/json');
script.textContent = escapeHtml(transferStore.toJson());
script.textContent = escapeHtml(content);
doc.body.appendChild(script);
};
}
Expand Down
105 changes: 3 additions & 102 deletions packages/platform-server/test/integration_spec.ts
Expand Up @@ -10,10 +10,10 @@ import {animate, AnimationBuilder, state, style, transition, trigger} from '@ang
import {DOCUMENT, isPlatformServer, PlatformLocation, ɵgetDOM as getDOM} from '@angular/common';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {ApplicationRef, CompilerFactory, Component, destroyPlatform, getPlatform, HostBinding, HostListener, importProvidersFrom, Inject, Injectable, Input, NgModule, NgZone, OnInit, PLATFORM_ID, PlatformRef, Type, ViewEncapsulation} from '@angular/core';
import {ApplicationRef, CompilerFactory, Component, destroyPlatform, getPlatform, HostListener, Inject, Injectable, Input, NgModule, NgZone, PLATFORM_ID, PlatformRef, ViewEncapsulation} from '@angular/core';
import {inject, TestBed, waitForAsync} from '@angular/core/testing';
import {BrowserModule, makeStateKey, Title, TransferState} from '@angular/platform-browser';
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformState, renderModule, renderModuleFactory, ServerModule, ServerTransferStateModule} from '@angular/platform-server';
import {BrowserModule, Title} from '@angular/platform-browser';
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformState, renderModule, renderModuleFactory, ServerModule} from '@angular/platform-server';
import {Observable} from 'rxjs';
import {first} from 'rxjs/operators';

Expand Down Expand Up @@ -389,19 +389,6 @@ const [MyHostComponentStandalone, _] = createFalseAttributesComponents(true);
class FalseAttributesModule {
}

@Component({selector: 'app', template: '<div [innerText]="foo"></div>'})
class InnerTextComponent {
foo = 'Some text';
}

@NgModule({
declarations: [InnerTextComponent],
bootstrap: [InnerTextComponent],
imports: [ServerModule, BrowserModule.withServerTransition({appId: 'inner-text'})]
})
class InnerTextModule {
}

function createMyInputComponent(standalone: boolean) {
@Component({
standalone,
Expand Down Expand Up @@ -449,47 +436,6 @@ const HTMLTypesAppStandalone = createHTMLTypesApp(true);
class HTMLTypesModule {
}

const TEST_KEY = makeStateKey<number>('test');
const STRING_KEY = makeStateKey<string>('testString');

@Component({selector: 'app', template: 'Works!'})
class TransferComponent {
constructor(private transferStore: TransferState) {}
ngOnInit() {
this.transferStore.set(TEST_KEY, 10);
}
}

@Component({selector: 'esc-app', template: 'Works!'})
class EscapedComponent {
constructor(private transferStore: TransferState) {}
ngOnInit() {
this.transferStore.set(STRING_KEY, '</script><script>alert(\'Hello&\' + "World");');
}
}

@NgModule({
bootstrap: [TransferComponent],
declarations: [TransferComponent],
imports: [
BrowserModule.withServerTransition({appId: 'transfer'}),
ServerModule,
]
})
class TransferStoreModule {
}

@NgModule({
bootstrap: [EscapedComponent],
declarations: [EscapedComponent],
imports: [
BrowserModule.withServerTransition({appId: 'transfer'}),
ServerModule,
]
})
class EscapedTransferStoreModule {
}

function createMyHiddenComponent(standalone: boolean) {
@Component({
standalone,
Expand Down Expand Up @@ -1310,50 +1256,5 @@ describe('platform-server integration', () => {
});
});
});

describe('ServerTransferStoreModule', () => {
let called = false;
const defaultExpectedOutput =
'<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>';

beforeEach(() => {
called = false;
});
afterEach(() => {
expect(called).toBe(true);
});

it('adds transfer script tag when using renderModule', waitForAsync(() => {
renderModule(TransferStoreModule, {document: '<app></app>'}).then(output => {
expect(output).toBe(defaultExpectedOutput);
called = true;
});
}));

it('adds transfer script tag when using renderModuleFactory',
waitForAsync(inject([PlatformRef], (defaultPlatform: PlatformRef) => {
const compilerFactory: CompilerFactory =
defaultPlatform.injector.get(CompilerFactory, null)!;
const moduleFactory =
compilerFactory.createCompiler().compileModuleSync(TransferStoreModule);
renderModuleFactory(moduleFactory, {document: '<app></app>'}).then(output => {
expect(output).toBe(defaultExpectedOutput);
called = true;
});
})));

it('cannot break out of <script> tag in serialized output', waitForAsync(() => {
renderModule(EscapedTransferStoreModule, {
document: '<esc-app></esc-app>'
}).then(output => {
expect(output).toBe(
'<html><head></head><body><esc-app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</esc-app>' +
'<script id="transfer-state" type="application/json">' +
'{&q;testString&q;:&q;&l;/script&g;&l;script&g;' +
'alert(&s;Hello&a;&s; + \\&q;World\\&q;);&q;}</script></body></html>');
called = true;
});
}));
});
});
})();
86 changes: 86 additions & 0 deletions packages/platform-server/test/transfer_state_spec.ts
@@ -0,0 +1,86 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Component, NgModule,} from '@angular/core';
import {BrowserModule, makeStateKey, TransferState} from '@angular/platform-browser';
import {renderModule, ServerModule} from '@angular/platform-server';

describe('transfer_state', () => {
const defaultExpectedOutput =
'<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>';

it('adds transfer script tag when using renderModule', async () => {
const STATE_KEY = makeStateKey<number>('test');

@Component({selector: 'app', template: 'Works!'})
class TransferComponent {
constructor(private transferStore: TransferState) {
this.transferStore.set(STATE_KEY, 10);
}
}

@NgModule({
bootstrap: [TransferComponent],
declarations: [TransferComponent],
imports: [BrowserModule.withServerTransition({appId: 'transfer'}), ServerModule],
})
class TransferStoreModule {
}

const output = await renderModule(TransferStoreModule, {document: '<app></app>'});
expect(output).toBe(defaultExpectedOutput);
});

it('cannot break out of <script> tag in serialized output', async () => {
const STATE_KEY = makeStateKey<string>('testString');

@Component({selector: 'esc-app', template: 'Works!'})
class EscapedComponent {
constructor(private transferStore: TransferState) {
this.transferStore.set(STATE_KEY, '</script><script>alert(\'Hello&\' + "World");');
}
}
@NgModule({
bootstrap: [EscapedComponent],
declarations: [EscapedComponent],
imports: [BrowserModule.withServerTransition({appId: 'transfer'}), ServerModule],
})
class EscapedTransferStoreModule {
}

const output =
await renderModule(EscapedTransferStoreModule, {document: '<esc-app></esc-app>'});
expect(output).toBe(
'<html><head></head><body><esc-app ng-version="0.0.0-PLACEHOLDER" ng-server-context="other">Works!</esc-app>' +
'<script id="transfer-state" type="application/json">' +
'{&q;testString&q;:&q;&l;/script&g;&l;script&g;' +
'alert(&s;Hello&a;&s; + \\&q;World\\&q;);&q;}</script></body></html>');
});

it('adds transfer script tag when setting state during onSerialize', async () => {
const STATE_KEY = makeStateKey<number>('test');

@Component({selector: 'app', template: 'Works!'})
class TransferComponent {
constructor(private transferStore: TransferState) {
this.transferStore.onSerialize(STATE_KEY, () => 10);
}
}

@NgModule({
bootstrap: [TransferComponent],
declarations: [TransferComponent],
imports: [BrowserModule.withServerTransition({appId: 'transfer'}), ServerModule],
})
class TransferStoreModule {
}

const output = await renderModule(TransferStoreModule, {document: '<app></app>'});
expect(output).toBe(defaultExpectedOutput);
});
});
2 changes: 1 addition & 1 deletion packages/platform-server/testing/src/server.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {createPlatformFactory, NgModule, PlatformRef, StaticProvider} from '@angular/core';
import {createPlatformFactory, NgModule} from '@angular/core';
import {BrowserDynamicTestingModule, ɵplatformCoreDynamicTesting as platformCoreDynamicTesting} from '@angular/platform-browser-dynamic/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {ɵINTERNAL_SERVER_PLATFORM_PROVIDERS as INTERNAL_SERVER_PLATFORM_PROVIDERS, ɵSERVER_RENDER_PROVIDERS as SERVER_RENDER_PROVIDERS} from '@angular/platform-server';
Expand Down