Skip to content

Commit b5278cc

Browse files
alan-agius4thePunderWoman
authored andcommitted
feat(platform-server): renderApplication now accepts a bootstrapping method (#49248)
The `renderApplication` now also accepts a bootstrapping function call with return `Promise<ApplicationRef>`as the first parameter. Example: ```ts const bootstrap = () => bootstrapApplication(RootComponent, appConfig); const output: string = await renderApplication(bootstrap); ``` PR Close #49248
1 parent 2e568b8 commit b5278cc

File tree

4 files changed

+108
-8
lines changed

4 files changed

+108
-8
lines changed

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
```ts
66

7+
import { ApplicationRef } from '@angular/core';
78
import { EnvironmentProviders } from '@angular/core';
89
import * as i0 from '@angular/core';
910
import * as i1 from '@angular/common/http';
@@ -47,6 +48,13 @@ export class PlatformState {
4748
static ɵprov: i0.ɵɵInjectableDeclaration<PlatformState>;
4849
}
4950

51+
// @public
52+
export function renderApplication<T>(bootstrap: () => Promise<ApplicationRef>, options: {
53+
document?: string | Document;
54+
url?: string;
55+
platformProviders?: Provider[];
56+
}): Promise<string>;
57+
5058
// @public
5159
export function renderApplication<T>(rootComponent: Type<T>, options: {
5260
appId: string;

packages/core/src/core_render3_private_export.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export {
219219
export {
220220
setDocument as ɵsetDocument
221221
} from './render3/interfaces/document';
222+
export { getComponentDef as ɵgetComponentDef} from './render3/definition';
222223
export {
223224
compileComponent as ɵcompileComponent,
224225
compileDirective as ɵcompileDirective,
@@ -281,4 +282,5 @@ export {
281282
noSideEffects as ɵnoSideEffects,
282283
} from './util/closure';
283284

285+
284286
// clang-format on

packages/platform-server/src/utils.ts

Lines changed: 54 additions & 6 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, EnvironmentProviders, importProvidersFrom, InjectionToken, NgModuleRef, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵinternalCreateApplication as internalCreateApplication, ɵisPromise} from '@angular/core';
9+
import {ApplicationRef, EnvironmentProviders, importProvidersFrom, InjectionToken, NgModuleRef, PlatformRef, Provider, Renderer2, StaticProvider, Type, ɵgetComponentDef as getComponentDef, ɵinternalCreateApplication as internalCreateApplication, ɵisPromise} from '@angular/core';
1010
import {BrowserModule, ɵTRANSITION_ID} from '@angular/platform-browser';
1111
import {first} from 'rxjs/operators';
1212

@@ -153,11 +153,37 @@ export function renderModule<T>(moduleType: Type<T>, options: {
153153
return _render(platform, platform.bootstrapModule(moduleType));
154154
}
155155

156+
/**
157+
* Bootstraps an instance of an Angular application and renders it to a string.
158+
159+
* ```typescript
160+
* const bootstrap = () => bootstrapApplication(RootComponent, appConfig);
161+
* const output: string = await renderApplication(bootstrap);
162+
* ```
163+
*
164+
* @param bootstrap A method that when invoked returns a promise that returns an `ApplicationRef`
165+
* instance once resolved.
166+
* @param options Additional configuration for the render operation:
167+
* - `document` - the document of the page to render, either as an HTML string or
168+
* as a reference to the `document` instance.
169+
* - `url` - the URL for the current render request.
170+
* - `platformProviders` - the platform level providers for the current render request.
171+
*
172+
* @returns A Promise, that returns serialized (to a string) rendered page, once resolved.
173+
*
174+
* @publicApi
175+
* @developerPreview
176+
*/
177+
export function renderApplication<T>(bootstrap: () => Promise<ApplicationRef>, options: {
178+
document?: string|Document,
179+
url?: string,
180+
platformProviders?: Provider[],
181+
}): Promise<string>;
156182
/**
157183
* Bootstraps an instance of an Angular application and renders it to a string.
158184
*
159-
* Note: the root component passed into this function *must* be a standalone one (should have the
160-
* `standalone: true` flag in the `@Component` decorator config).
185+
* Note: the root component passed into this function *must* be a standalone one (should have
186+
* the `standalone: true` flag in the `@Component` decorator config).
161187
*
162188
* ```typescript
163189
* @Component({
@@ -186,18 +212,40 @@ export function renderModule<T>(moduleType: Type<T>, options: {
186212
* @developerPreview
187213
*/
188214
export function renderApplication<T>(rootComponent: Type<T>, options: {
215+
/** @deprecated use `APP_ID` token to set the application ID. */
189216
appId: string,
190217
document?: string|Document,
191218
url?: string,
192219
providers?: Array<Provider|EnvironmentProviders>,
193220
platformProviders?: Provider[],
194-
}): Promise<string> {
195-
const {document, url, platformProviders, appId} = options;
221+
}): Promise<string>;
222+
export function renderApplication<T>(
223+
rootComponentOrBootstrapFn: Type<T>|(() => Promise<ApplicationRef>), options: {
224+
appId?: string,
225+
document?: string|Document,
226+
url?: string,
227+
providers?: Array<Provider|EnvironmentProviders>,
228+
platformProviders?: Provider[],
229+
}): Promise<string> {
230+
const {document, url, platformProviders, appId = ''} = options;
196231
const platform = _getPlatform(platformDynamicServer, {document, url, platformProviders});
232+
233+
if (isBootstrapFn(rootComponentOrBootstrapFn)) {
234+
return _render(platform, rootComponentOrBootstrapFn());
235+
}
236+
197237
const appProviders = [
198238
importProvidersFrom(BrowserModule.withServerTransition({appId})),
199239
importProvidersFrom(ServerModule),
200240
...(options.providers ?? []),
201241
];
202-
return _render(platform, internalCreateApplication({rootComponent, appProviders}));
242+
243+
return _render(
244+
platform,
245+
internalCreateApplication({rootComponent: rootComponentOrBootstrapFn, appProviders}));
246+
}
247+
248+
function isBootstrapFn(value: unknown): value is() => Promise<ApplicationRef> {
249+
// We can differentiate between a component and a bootstrap function by reading `cmp`:
250+
return typeof value === 'function' && !getComponentDef(value);
203251
}

packages/platform-server/test/integration_spec.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ 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, Component, destroyPlatform, getPlatform, HostListener, Inject, inject as coreInject, Injectable, Input, NgModule, NgZone, PLATFORM_ID, ViewEncapsulation} from '@angular/core';
13+
import {ApplicationRef, Component, destroyPlatform, getPlatform, HostListener, importProvidersFrom, Inject, inject as coreInject, Injectable, Input, NgModule, NgZone, PLATFORM_ID, ViewEncapsulation} from '@angular/core';
1414
import {TestBed, waitForAsync} from '@angular/core/testing';
15-
import {BrowserModule, makeStateKey, Title, TransferState} from '@angular/platform-browser';
15+
import {bootstrapApplication, BrowserModule, makeStateKey, Title, TransferState} from '@angular/platform-browser';
1616
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformState, renderModule, ServerModule} from '@angular/platform-server';
1717
import {Observable} from 'rxjs';
1818
import {first} from 'rxjs/operators';
@@ -203,6 +203,13 @@ function createMyAsyncServerApp(standalone: boolean) {
203203
const MyAsyncServerApp = createMyAsyncServerApp(false);
204204
const MyAsyncServerAppStandalone = createMyAsyncServerApp(true);
205205

206+
const boostrapMyAsyncServerAppStandalone = () => bootstrapApplication(MyAsyncServerAppStandalone, {
207+
providers: [
208+
importProvidersFrom(BrowserModule.withServerTransition({appId: 'simple-cmp'})),
209+
importProvidersFrom(ServerModule),
210+
]
211+
});
212+
206213
@NgModule({
207214
declarations: [MyAsyncServerApp],
208215
imports: [BrowserModule.withServerTransition({appId: 'async-server'}), ServerModule],
@@ -702,6 +709,41 @@ describe('platform-server integration', () => {
702709
});
703710
}));
704711

712+
it(`using renderApplication with boostrapping function call works`, waitForAsync(() => {
713+
const document = TestBed.inject(DOCUMENT);
714+
715+
// Append root element based on the app selector.
716+
const rootEl = document.createElement('app');
717+
document.body.appendChild(rootEl);
718+
719+
// Append a special marker to verify that we use a correct instance
720+
// of the document for rendering.
721+
const markerEl = document.createComment('test marker');
722+
document.body.appendChild(markerEl);
723+
724+
const platformProviders = [{
725+
provide: SERVER_CONTEXT,
726+
useValue: 'ssr',
727+
}];
728+
729+
const render =
730+
renderApplication(boostrapMyAsyncServerAppStandalone, {document, platformProviders});
731+
732+
render
733+
.then(output => {
734+
expect(output).toBe(
735+
'<html><head><title>fakeTitle</title></head>' +
736+
'<body><app ng-version="0.0.0-PLACEHOLDER" ng-server-context="ssr">' +
737+
'Works!<h1 textcontent="fine">fine</h1></app>' +
738+
'<!--test marker--></body></html>');
739+
called = true;
740+
})
741+
.finally(() => {
742+
rootEl.remove();
743+
markerEl.remove();
744+
});
745+
}));
746+
705747
// Run the set of tests with regular and standalone components.
706748
[true, false].forEach((isStandalone: boolean) => {
707749
it(`using ${isStandalone ? 'renderApplication' : 'renderModule'} should work`,

0 commit comments

Comments
 (0)