Skip to content

Commit 66cf119

Browse files
committed
feat(@angular/ssr): introduce BootstrapContext for isolated server-side rendering
This commit introduces a number of changes to the server bootstrapping process to make it more robust and less error-prone, especially for concurrent requests. Previously, the server rendering process relied on a module-level global platform injector. This could lead to issues in server-side rendering environments where multiple requests are processed concurrently, as they could inadvertently share or overwrite the global injector state. The new approach introduces a `BootstrapContext` that is passed to the `bootstrapApplication` function. This context provides a platform reference that is scoped to the individual request, ensuring that each server-side render has an isolated platform injector. This prevents state leakage between concurrent requests and makes the overall process more reliable. BREAKING CHANGE: The server-side bootstrapping process has been changed to eliminate the reliance on a global platform injector. Before: ```ts const bootstrap = () => bootstrapApplication(AppComponent, config); ``` After: ```ts const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context); ```
1 parent e6a3b55 commit 66cf119

File tree

8 files changed

+39
-22
lines changed

8 files changed

+39
-22
lines changed

packages/angular/ssr/node/src/common-engine/common-engine.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import { ApplicationRef, StaticProvider, Type } from '@angular/core';
10+
import { BootstrapContext } from '@angular/platform-browser';
1011
import { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
1112
import * as fs from 'node:fs';
1213
import { dirname, join, normalize, resolve } from 'node:path';
@@ -23,7 +24,7 @@ const SSG_MARKER_REGEXP = /ng-server-context=["']\w*\|?ssg\|?\w*["']/;
2324

2425
export interface CommonEngineOptions {
2526
/** A method that when invoked returns a promise that returns an `ApplicationRef` instance once resolved or an NgModule. */
26-
bootstrap?: Type<{}> | (() => Promise<ApplicationRef>);
27+
bootstrap?: Type<{}> | ((context: BootstrapContext) => Promise<ApplicationRef>);
2728

2829
/** A set of platform level providers for all requests. */
2930
providers?: StaticProvider[];
@@ -197,7 +198,9 @@ async function exists(path: fs.PathLike): Promise<boolean> {
197198
}
198199
}
199200

200-
function isBootstrapFn(value: unknown): value is () => Promise<ApplicationRef> {
201+
function isBootstrapFn(
202+
value: unknown,
203+
): value is (context: BootstrapContext) => Promise<ApplicationRef> {
201204
// We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property:
202205
return typeof value === 'function' && !('ɵmod' in value);
203206
}

packages/angular/ssr/src/routes/ng-routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ export async function getRoutesFromAngularRouterConfig(
634634
const moduleRef = await platformRef.bootstrapModule(bootstrap);
635635
applicationRef = moduleRef.injector.get(ApplicationRef);
636636
} else {
637-
applicationRef = await bootstrap();
637+
applicationRef = await bootstrap({ platformRef });
638638
}
639639

640640
const injector = applicationRef.injector;

packages/angular/ssr/src/routes/route-config.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -356,20 +356,23 @@ export function withAppShell(
356356
* when using the `bootstrapApplication` function:
357357
*
358358
* ```ts
359-
* import { bootstrapApplication } from '@angular/platform-browser';
359+
* import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
360360
* import { provideServerRendering, withRoutes, withAppShell } from '@angular/ssr';
361361
* import { AppComponent } from './app/app.component';
362362
* import { SERVER_ROUTES } from './app/app.server.routes';
363363
* import { AppShellComponent } from './app/app-shell.component';
364364
*
365-
* bootstrapApplication(AppComponent, {
366-
* providers: [
367-
* provideServerRendering(
368-
* withRoutes(SERVER_ROUTES),
369-
* withAppShell(AppShellComponent)
370-
* )
371-
* ]
372-
* });
365+
* const bootstrap = (context: BootstrapContext) =>
366+
* bootstrapApplication(AppComponent, {
367+
* providers: [
368+
* provideServerRendering(
369+
* withRoutes(SERVER_ROUTES),
370+
* withAppShell(AppShellComponent),
371+
* ),
372+
* ],
373+
* }, context);
374+
*
375+
* export default bootstrap;
373376
* ```
374377
* @see {@link withRoutes} configures server-side routing
375378
* @see {@link withAppShell} configures the application shell

packages/angular/ssr/src/utils/ng.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type Type,
1515
ɵConsole,
1616
} from '@angular/core';
17+
import { BootstrapContext } from '@angular/platform-browser';
1718
import {
1819
INITIAL_CONFIG,
1920
ɵSERVER_CONTEXT as SERVER_CONTEXT,
@@ -31,7 +32,9 @@ import { joinUrlParts, stripIndexHtmlFromURL } from './url';
3132
* - A reference to an Angular component or module (`Type<unknown>`) that serves as the root of the application.
3233
* - A function that returns a `Promise<ApplicationRef>`, which resolves with the root application reference.
3334
*/
34-
export type AngularBootstrap = Type<unknown> | (() => Promise<ApplicationRef>);
35+
export type AngularBootstrap =
36+
| Type<unknown>
37+
| ((context: BootstrapContext) => Promise<ApplicationRef>);
3538

3639
/**
3740
* Renders an Angular application or module to an HTML string.
@@ -90,7 +93,7 @@ export async function renderAngular(
9093
const moduleRef = await platformRef.bootstrapModule(bootstrap);
9194
applicationRef = moduleRef.injector.get(ApplicationRef);
9295
} else {
93-
applicationRef = await bootstrap();
96+
applicationRef = await bootstrap({ platformRef });
9497
}
9598

9699
// Block until application is stable.

packages/angular_devkit/build_angular/src/builders/app-shell/render-worker.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import type { ApplicationRef, StaticProvider, Type } from '@angular/core';
10+
import type { BootstrapContext } from '@angular/platform-browser';
1011
import type { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
1112
import assert from 'node:assert';
1213
import { workerData } from 'node:worker_threads';
@@ -33,7 +34,7 @@ interface ServerBundleExports {
3334
renderApplication?: typeof renderApplication;
3435

3536
/** Standalone application bootstrapping function. */
36-
default?: () => Promise<ApplicationRef>;
37+
default?: (context: BootstrapContext) => Promise<ApplicationRef>;
3738
}
3839

3940
/**
@@ -121,7 +122,9 @@ async function render({ serverBundlePath, document, url }: RenderRequest): Promi
121122
return Promise.race([renderAppPromise, renderingTimeout]).finally(() => clearTimeout(timer));
122123
}
123124

124-
function isBootstrapFn(value: unknown): value is () => Promise<ApplicationRef> {
125+
function isBootstrapFn(
126+
value: unknown,
127+
): value is (context: BootstrapContext) => Promise<ApplicationRef> {
125128
// We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property:
126129
return typeof value === 'function' && !('ɵmod' in value);
127130
}

packages/angular_devkit/build_angular/src/builders/prerender/render-worker.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
import type { ApplicationRef, StaticProvider, Type } from '@angular/core';
10+
import type { BootstrapContext } from '@angular/platform-browser';
1011
import type { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';
1112
import assert from 'node:assert';
1213
import * as fs from 'node:fs';
@@ -42,7 +43,7 @@ interface ServerBundleExports {
4243
renderApplication?: typeof renderApplication;
4344

4445
/** Standalone application bootstrapping function. */
45-
default?: (() => Promise<ApplicationRef>) | Type<unknown>;
46+
default?: ((context: BootstrapContext) => Promise<ApplicationRef>) | Type<unknown>;
4647
}
4748

4849
/**
@@ -148,7 +149,9 @@ async function render({
148149
return result;
149150
}
150151

151-
function isBootstrapFn(value: unknown): value is () => Promise<ApplicationRef> {
152+
function isBootstrapFn(
153+
value: unknown,
154+
): value is (context: BootstrapContext) => Promise<ApplicationRef> {
152155
// We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property:
153156
return typeof value === 'function' && !('ɵmod' in value);
154157
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { bootstrapApplication } from '@angular/platform-browser';
1+
import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser';
22
import { <%= appComponentName %> } from '<%= appComponentPath %>';
33
import { config } from './app/app.config.server';
44

5-
const bootstrap = () => bootstrapApplication(<%= appComponentName %>, config);
5+
const bootstrap = (context: BootstrapContext) =>
6+
bootstrapApplication(<%= appComponentName %>, config, BootstrapContext);
67

78
export default bootstrap;
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { bootstrapApplication } from '@angular/platform-browser';
1+
import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser';
22
import { <%= appComponentName %> } from '<%= appComponentPath %>';
33
import { config } from './app/app.config.server';
44

5-
const bootstrap = () => bootstrapApplication(<%= appComponentName %>, config);
5+
const bootstrap = (context: BootstrapContext) =>
6+
bootstrapApplication(<%= appComponentName %>, config, BootstrapContext);
67

78
export default bootstrap;

0 commit comments

Comments
 (0)