Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f0b0980

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 e06980b commit f0b0980

File tree

15 files changed

+66
-40
lines changed

15 files changed

+66
-40
lines changed

goldens/public-api/angular/ssr/node/index.api.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
```ts
66

77
import { ApplicationRef } from '@angular/core';
8+
import { BootstrapContext } from '@angular/platform-browser';
89
import { Http2ServerRequest } from 'node:http2';
910
import { Http2ServerResponse } from 'node:http2';
1011
import { IncomingMessage } from 'node:http';
@@ -26,14 +27,14 @@ export class CommonEngine {
2627

2728
// @public (undocumented)
2829
export interface CommonEngineOptions {
29-
bootstrap?: Type<{}> | (() => Promise<ApplicationRef>);
30+
bootstrap?: Type<{}> | ((context: BootstrapContext) => Promise<ApplicationRef>);
3031
enablePerformanceProfiler?: boolean;
3132
providers?: StaticProvider[];
3233
}
3334

3435
// @public (undocumented)
3536
export interface CommonEngineRenderOptions {
36-
bootstrap?: Type<{}> | (() => Promise<ApplicationRef>);
37+
bootstrap?: Type<{}> | ((context: BootstrapContext) => Promise<ApplicationRef>);
3738
// (undocumented)
3839
document?: string;
3940
// (undocumented)

packages/angular/build/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ ts_project(
124124
"//:node_modules/@angular/compiler-cli",
125125
"//:node_modules/@angular/core",
126126
"//:node_modules/@angular/localize",
127+
"//:node_modules/@angular/platform-browser",
127128
"//:node_modules/@angular/platform-server",
128129
"//:node_modules/@angular/service-worker",
129130
"//:node_modules/@types/babel__core",

packages/angular/build/src/utils/server-rendering/load-esm-from-memory.ts

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

99
import type { ApplicationRef, Type } from '@angular/core';
10+
import type { BootstrapContext } from '@angular/platform-browser';
1011
import type { ɵextractRoutesAndCreateRouteTree, ɵgetOrCreateAngularServerApp } from '@angular/ssr';
1112
import { assertIsError } from '../error';
1213
import { loadEsmModule } from '../load-esm';
@@ -15,7 +16,7 @@ import { loadEsmModule } from '../load-esm';
1516
* Represents the exports available from the main server bundle.
1617
*/
1718
interface MainServerBundleExports {
18-
default: (() => Promise<ApplicationRef>) | Type<unknown>;
19+
default: ((context: BootstrapContext) => Promise<ApplicationRef>) | Type<unknown>;
1920
ɵextractRoutesAndCreateRouteTree: typeof ɵextractRoutesAndCreateRouteTree;
2021
ɵgetOrCreateAngularServerApp: typeof ɵgetOrCreateAngularServerApp;
2122
}

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

Lines changed: 6 additions & 3 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[];
@@ -34,7 +35,7 @@ export interface CommonEngineOptions {
3435

3536
export interface CommonEngineRenderOptions {
3637
/** A method that when invoked returns a promise that returns an `ApplicationRef` instance once resolved or an NgModule. */
37-
bootstrap?: Type<{}> | (() => Promise<ApplicationRef>);
38+
bootstrap?: Type<{}> | ((context: BootstrapContext) => Promise<ApplicationRef>);
3839

3940
/** A set of platform level providers for the current request. */
4041
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/manifest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import type { BootstrapContext } from '@angular/platform-browser';
910
import type { SerializableRouteTreeNode } from './routes/route-tree';
1011
import { AngularBootstrap } from './utils/ng';
1112

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/ssr/test/testing-utils.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -90,15 +90,19 @@ export function setAngularAppTestingManifest(
9090
`,
9191
},
9292
},
93-
bootstrap: async () => () => {
94-
return bootstrapApplication(rootComponent, {
95-
providers: [
96-
provideZonelessChangeDetection(),
97-
provideRouter(routes),
98-
provideServerRendering(withRoutes(serverRoutes)),
99-
...extraProviders,
100-
],
101-
});
93+
bootstrap: async () => (context) => {
94+
return bootstrapApplication(
95+
rootComponent,
96+
{
97+
providers: [
98+
provideZonelessChangeDetection(),
99+
provideRouter(routes),
100+
provideServerRendering(withRoutes(serverRoutes)),
101+
...extraProviders,
102+
],
103+
},
104+
context,
105+
);
102106
},
103107
});
104108
}

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
}

0 commit comments

Comments
 (0)