From 2b4d7f6733e10e45e3ca8a448307bbc2834fc4a1 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Wed, 3 Aug 2022 14:19:40 -0700 Subject: [PATCH] feat(platform-server): support document reference in render functions (#47032) This commit updates the `renderModule` and `renderApplication` functions to also accept a document reference (in addition to the serialized document contents as a string). This should provide the necessary flexibility to NgUniversal (and other API consumers) to structure the logic to avoid serializing and parsing document multiple times during the SSR request. PR Close #47032 --- goldens/public-api/platform-server/index.md | 4 +-- packages/platform-server/src/server.ts | 11 ++++-- packages/platform-server/src/utils.ts | 13 ++++--- .../platform-server/test/integration_spec.ts | 34 ++++++++++++++++++- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/goldens/public-api/platform-server/index.md b/goldens/public-api/platform-server/index.md index 765e324ee53..77cc3c85b93 100644 --- a/goldens/public-api/platform-server/index.md +++ b/goldens/public-api/platform-server/index.md @@ -51,7 +51,7 @@ export class PlatformState { // @public export function renderApplication(rootComponent: Type, options: { appId: string; - document?: string; + document?: string | Document; url?: string; providers?: Array; platformProviders?: Provider[]; @@ -59,7 +59,7 @@ export function renderApplication(rootComponent: Type, options: { // @public export function renderModule(module: Type, options: { - document?: string; + document?: string | Document; url?: string; extraProviders?: StaticProvider[]; }): Promise; diff --git a/packages/platform-server/src/server.ts b/packages/platform-server/src/server.ts index 7e13abb810d..0e5094f123d 100644 --- a/packages/platform-server/src/server.ts +++ b/packages/platform-server/src/server.ts @@ -81,9 +81,14 @@ export class ServerModule { } function _document(injector: Injector) { - let config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null); - const document = config && config.document ? parseDocument(config.document, config.url) : - getDOM().createHtmlDocument(); + const config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null); + let document: Document; + if (config && config.document) { + document = typeof config.document === 'string' ? parseDocument(config.document, config.url) : + config.document; + } else { + document = getDOM().createHtmlDocument(); + } // Tell ivy about the global document ɵsetDocument(document); return document; diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts index aae62c238bf..411c956b104 100644 --- a/packages/platform-server/src/utils.ts +++ b/packages/platform-server/src/utils.ts @@ -16,7 +16,7 @@ import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens'; import {TRANSFER_STATE_SERIALIZATION_PROVIDERS} from './transfer_state'; interface PlatformOptions { - document?: string; + document?: string|Document; url?: string; platformProviders?: Provider[]; } @@ -95,14 +95,16 @@ the server-rendered app can be properly bootstrapped into a client app.`); /** * Renders a Module to string. * - * `document` is the full document HTML of the page to render, as a string. + * `document` is the document of the page to render, either as an HTML string or + * as a reference to the `document` instance. * `url` is the URL for the current render request. * `extraProviders` are the platform level providers for the current render request. * * @publicApi */ export function renderModule( - module: Type, options: {document?: string, url?: string, extraProviders?: StaticProvider[]}): + module: Type, + options: {document?: string|Document, url?: string, extraProviders?: StaticProvider[]}): Promise { const {document, url, extraProviders: platformProviders} = options; const platform = _getPlatform(platformDynamicServer, {document, url, platformProviders}); @@ -130,7 +132,8 @@ export function renderModule( * - `appId` - a string identifier of this application. The appId is used to prefix all * server-generated stylings and state keys of the application in TransferState * use-cases. - * - `document` - the full document HTML of the page to render, as a string. + * - `document` - the document of the page to render, either as an HTML string or + * as a reference to the `document` instance. * - `url` - the URL for the current render request. * - `providers` - set of application level providers for the current render request. * - `platformProviders` - the platform level providers for the current render request. @@ -141,7 +144,7 @@ export function renderModule( */ export function renderApplication(rootComponent: Type, options: { appId: string, - document?: string, + document?: string|Document, url?: string, providers?: Array, platformProviders?: Provider[], diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index 57e246897b4..d7c30a79481 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -11,7 +11,7 @@ import {DOCUMENT, isPlatformServer, PlatformLocation, ɵgetDOM as getDOM} from ' 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 {inject, waitForAsync} from '@angular/core/testing'; +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 {Observable} from 'rxjs'; @@ -750,6 +750,38 @@ describe('platform-server integration', () => { }); })); + it(`using ${isStandalone ? 'renderApplication' : 'renderModule'} ` + + `should allow passing a document reference`, + waitForAsync(() => { + const document = TestBed.inject(DOCUMENT); + + // Append root element based on the app selector. + const rootEl = document.createElement('app'); + document.body.appendChild(rootEl); + + // Append a special marker to verify that we use a correct instance + // of the document for rendering. + const markerEl = document.createComment('test marker'); + document.body.appendChild(markerEl); + + const options = {document}; + const bootstrap = isStandalone ? + renderApplication(MyAsyncServerAppStandalone, {document, appId: 'simple-cmp'}) : + renderModule(AsyncServerModule, options); + bootstrap + .then(output => { + expect(output).toBe( + 'fakeTitle' + + 'Works!

fine

' + + ''); + called = true; + }) + .finally(() => { + rootEl.remove(); + markerEl.remove(); + }); + })); + it('works with SVG elements', waitForAsync(() => { const options = {document: doc}; const bootstrap = isStandalone ?