From 28926ba92cf3da7e45a7b8938bba49febdf58eb7 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 8 Sep 2025 09:27:03 +0000 Subject: [PATCH] feat(core): introduce `BootstrapContext` for improved server bootstrapping (#63562) 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); ``` A schematic is provided to automatically update `main.server.ts` files to pass the `BootstrapContext` to the `bootstrapApplication` call. In addition, `getPlatform()` and `destroyPlatform()` will now return `null` and be a no-op respectively when running in a server environment. PR Close #63562 --- adev/src/main.server.ts | 5 +- .../public-api/platform-browser/index.api.md | 7 +- .../public-api/platform-server/index.api.md | 5 +- .../src/main.server.ts | 5 +- .../projects/standalone/src/main.server.ts | 5 +- .../projects/standalone/src/main.server.ts | 5 +- modules/ssr-benchmarks/src/main.server.ts | 5 +- package.json | 3 +- packages/core/schematics/BUILD.bazel | 5 + packages/core/schematics/migrations.json | 5 + .../BUILD.bazel | 21 +++ .../README.md | 15 ++ .../index.ts | 20 +++ .../migration.ts | 149 ++++++++++++++++ ...d_bootstrap_context_to_server_main_spec.ts | 163 ++++++++++++++++++ .../src/application/create_application.ts | 19 +- packages/core/src/core_private_export.ts | 1 - packages/core/src/platform/bootstrap.ts | 8 +- packages/core/src/platform/platform.ts | 48 ++++-- .../forms_reactive/bundle.golden_symbols.json | 1 - .../bundle.golden_symbols.json | 1 - packages/platform-browser/src/browser.ts | 25 ++- .../platform-browser/src/platform-browser.ts | 1 + packages/platform-server/src/server.ts | 10 +- packages/platform-server/src/utils.ts | 27 ++- .../platform-server/test/hydration_utils.ts | 4 +- .../platform-server/test/integration_spec.ts | 6 +- pnpm-lock.yaml | 49 +++--- tools/pnpm-patches/@angular__ssr.patch | 22 +++ 29 files changed, 558 insertions(+), 82 deletions(-) create mode 100644 packages/core/schematics/migrations/add-bootstrap-context-to-server-main/BUILD.bazel create mode 100644 packages/core/schematics/migrations/add-bootstrap-context-to-server-main/README.md create mode 100644 packages/core/schematics/migrations/add-bootstrap-context-to-server-main/index.ts create mode 100644 packages/core/schematics/migrations/add-bootstrap-context-to-server-main/migration.ts create mode 100644 packages/core/schematics/test/add_bootstrap_context_to_server_main_spec.ts create mode 100644 tools/pnpm-patches/@angular__ssr.patch diff --git a/adev/src/main.server.ts b/adev/src/main.server.ts index 0e508858b9f..243b65ae925 100644 --- a/adev/src/main.server.ts +++ b/adev/src/main.server.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://angular.dev/license */ -import {bootstrapApplication} from '@angular/platform-browser'; +import {bootstrapApplication, BootstrapContext} from '@angular/platform-browser'; import {AppComponent} from './app/app.component'; import {config} from './app/app.config.server'; -const bootstrap = () => bootstrapApplication(AppComponent, config); +const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, config, context); export default bootstrap; diff --git a/goldens/public-api/platform-browser/index.api.md b/goldens/public-api/platform-browser/index.api.md index 97793ec2aed..c2d8ace6ce4 100644 --- a/goldens/public-api/platform-browser/index.api.md +++ b/goldens/public-api/platform-browser/index.api.md @@ -26,7 +26,12 @@ import { Type } from '@angular/core'; import { Version } from '@angular/core'; // @public -export function bootstrapApplication(rootComponent: Type, options?: ApplicationConfig): Promise; +export function bootstrapApplication(rootComponent: Type, options?: ApplicationConfig, context?: BootstrapContext): Promise; + +// @public +export interface BootstrapContext { + platformRef: PlatformRef; +} // @public export class BrowserModule { diff --git a/goldens/public-api/platform-server/index.api.md b/goldens/public-api/platform-server/index.api.md index 210b2c93381..476de591676 100644 --- a/goldens/public-api/platform-server/index.api.md +++ b/goldens/public-api/platform-server/index.api.md @@ -5,6 +5,7 @@ ```ts import { ApplicationRef } from '@angular/core'; +import { BootstrapContext } from '@angular/platform-browser'; import { EnvironmentProviders } from '@angular/core'; import * as i0 from '@angular/core'; import * as i1 from '@angular/platform-browser'; @@ -27,7 +28,7 @@ export interface PlatformConfig { url?: string; } -// @public (undocumented) +// @public export function platformServer(extraProviders?: StaticProvider[] | undefined): PlatformRef; // @public @@ -45,7 +46,7 @@ export class PlatformState { export function provideServerRendering(): EnvironmentProviders; // @public -export function renderApplication(bootstrap: () => Promise, options: { +export function renderApplication(bootstrap: (context: BootstrapContext) => Promise, options: { document?: string | Document; url?: string; platformProviders?: Provider[]; diff --git a/integration/platform-server-hydration/src/main.server.ts b/integration/platform-server-hydration/src/main.server.ts index e83e990a42c..b8ddf22d452 100644 --- a/integration/platform-server-hydration/src/main.server.ts +++ b/integration/platform-server-hydration/src/main.server.ts @@ -1,7 +1,8 @@ -import {bootstrapApplication} from '@angular/platform-browser'; +import {bootstrapApplication, BootstrapContext} from '@angular/platform-browser'; import {AppComponent} from './app/app.component'; import {config} from './app/app.config.server'; -const bootstrap = () => bootstrapApplication(AppComponent, config); +const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, config, context); export default bootstrap; diff --git a/integration/platform-server-zoneless/projects/standalone/src/main.server.ts b/integration/platform-server-zoneless/projects/standalone/src/main.server.ts index e83e990a42c..b8ddf22d452 100644 --- a/integration/platform-server-zoneless/projects/standalone/src/main.server.ts +++ b/integration/platform-server-zoneless/projects/standalone/src/main.server.ts @@ -1,7 +1,8 @@ -import {bootstrapApplication} from '@angular/platform-browser'; +import {bootstrapApplication, BootstrapContext} from '@angular/platform-browser'; import {AppComponent} from './app/app.component'; import {config} from './app/app.config.server'; -const bootstrap = () => bootstrapApplication(AppComponent, config); +const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, config, context); export default bootstrap; diff --git a/integration/platform-server/projects/standalone/src/main.server.ts b/integration/platform-server/projects/standalone/src/main.server.ts index e83e990a42c..b8ddf22d452 100644 --- a/integration/platform-server/projects/standalone/src/main.server.ts +++ b/integration/platform-server/projects/standalone/src/main.server.ts @@ -1,7 +1,8 @@ -import {bootstrapApplication} from '@angular/platform-browser'; +import {bootstrapApplication, BootstrapContext} from '@angular/platform-browser'; import {AppComponent} from './app/app.component'; import {config} from './app/app.config.server'; -const bootstrap = () => bootstrapApplication(AppComponent, config); +const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, config, context); export default bootstrap; diff --git a/modules/ssr-benchmarks/src/main.server.ts b/modules/ssr-benchmarks/src/main.server.ts index c2a6866f6c7..bbb42b8278d 100644 --- a/modules/ssr-benchmarks/src/main.server.ts +++ b/modules/ssr-benchmarks/src/main.server.ts @@ -7,12 +7,13 @@ */ import {ɵenableProfiling} from '@angular/core'; -import {bootstrapApplication} from '@angular/platform-browser'; +import {bootstrapApplication, BootstrapContext} from '@angular/platform-browser'; import {AppComponent} from './app/app.component'; import {config} from './app/app.config.server'; import {renderApplication, ɵENABLE_DOM_EMULATION} from '@angular/platform-server'; -const bootstrap = () => bootstrapApplication(AppComponent, config); +const bootstrap = (context: BootstrapContext) => + bootstrapApplication(AppComponent, config, context); /** * Function that will profile the server-side rendering diff --git a/package.json b/package.json index 080a47193d0..f81be6c4ab8 100644 --- a/package.json +++ b/package.json @@ -220,7 +220,8 @@ }, "pnpm": { "patchedDependencies": { - "dagre-d3-es@7.0.11": "tools/pnpm-patches/dagre-d3-es+7.0.11.patch" + "dagre-d3-es@7.0.11": "tools/pnpm-patches/dagre-d3-es+7.0.11.patch", + "@angular/ssr": "tools/pnpm-patches/@angular__ssr.patch" }, "packageExtensions": { "grpc-gcp": { diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 6e940e23494..d91c2e9f38f 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -114,6 +114,10 @@ bundle_entrypoints = [ "application-config-core", "packages/core/schematics/migrations/application-config-core/index.js", ], + [ + "add-bootstrap-context-to-server-main", + "packages/core/schematics/migrations/add-bootstrap-context-to-server-main/index.js", + ], ] rollup.rollup( @@ -125,6 +129,7 @@ rollup.rollup( "//:node_modules/magic-string", "//:node_modules/semver", "//packages/core/schematics:tsconfig_build", + "//packages/core/schematics/migrations/add-bootstrap-context-to-server-main", "//packages/core/schematics/migrations/application-config-core", "//packages/core/schematics/migrations/control-flow-migration", "//packages/core/schematics/migrations/ngclass-to-class-migration", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 97766bdc7b4..2872200af1d 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -21,6 +21,11 @@ "version": "21.0.0", "description": "Moves imports of `ApplicationConfig` from `@angular/platform-browser` to `@angular/core`", "factory": "./bundles/application-config-core.cjs#migrate" + }, + "add-bootstrap-context-to-server-main": { + "version": "21.0.0", + "description": "Adds `BootstrapContext` to `bootstrapApplication` calls in `main.server.ts` to support server rendering.", + "factory": "./bundles/add-bootstrap-context-to-server-main.cjs#migrate" } } } diff --git a/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/BUILD.bazel b/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/BUILD.bazel new file mode 100644 index 00000000000..2dfc4405b22 --- /dev/null +++ b/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/BUILD.bazel @@ -0,0 +1,21 @@ +load("//tools:defaults.bzl", "ts_project") + +package( + default_visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], +) + +ts_project( + name = "add-bootstrap-context-to-server-main", + srcs = glob(["**/*.ts"]), + deps = [ + "//:node_modules/@angular-devkit/schematics", + "//:node_modules/typescript", + "//packages/compiler-cli/private", + "//packages/core/schematics/utils", + "//packages/core/schematics/utils/tsurge", + "//packages/core/schematics/utils/tsurge/helpers/angular_devkit", + ], +) diff --git a/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/README.md b/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/README.md new file mode 100644 index 00000000000..ee241c82124 --- /dev/null +++ b/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/README.md @@ -0,0 +1,15 @@ +# Add Bootstrap Context to Server Main Migration + +This schematic updates `main.server.ts` files to correctly handle server-side rendering with `bootstrapApplication`. + +## How it works + +The migration performs the following transformations: + +1. **Identifies `bootstrapApplication` calls:** It specifically targets `main.server.ts` files to find calls to `bootstrapApplication` that are missing a third `context` argument. + +2. **Updates the function signature:** It adds a `(context: BootstrapContext)` parameter to the arrow function that typically wraps the `bootstrapApplication` call in a server entry file. + +3. **Passes the context:** It adds `, context` to the `bootstrapApplication` call, passing the newly added parameter. + +4. **Updates imports:** It ensures that `BootstrapContext` is imported from `@angular/platform-browser`. \ No newline at end of file diff --git a/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/index.ts b/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/index.ts new file mode 100644 index 00000000000..c8859b1af64 --- /dev/null +++ b/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/index.ts @@ -0,0 +1,20 @@ +/*! + * @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.dev/license + */ + +import {Rule} from '@angular-devkit/schematics'; +import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit'; +import {AddBootstrapContextToServerMainMigration} from './migration'; + +export function migrate(): Rule { + return async (tree) => { + await runMigrationInDevkit({ + tree, + getMigration: () => new AddBootstrapContextToServerMainMigration(), + }); + }; +} diff --git a/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/migration.ts b/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/migration.ts new file mode 100644 index 00000000000..3d87a9ff61f --- /dev/null +++ b/packages/core/schematics/migrations/add-bootstrap-context-to-server-main/migration.ts @@ -0,0 +1,149 @@ +/*! + * @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.dev/license + */ + +import {ImportManager} from '@angular/compiler-cli/private/migrations'; +import ts from 'typescript'; +import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import_manager'; +import { + confirmAsSerializable, + ProgramInfo, + Replacement, + Serializable, + TsurgeFunnelMigration, +} from '../../utils/tsurge'; +import {TextUpdate} from '../../utils/tsurge/replacement'; +import {projectFile} from '../../utils/tsurge/project_paths'; + +export interface CompilationUnitData { + replacements: Replacement[]; +} + +function findArrowFunction(node: ts.Node): ts.ArrowFunction | undefined { + let current: ts.Node | undefined = node; + while (current) { + if (ts.isArrowFunction(current)) { + return current; + } + current = current.parent; + } + return undefined; +} + +export class AddBootstrapContextToServerMainMigration extends TsurgeFunnelMigration< + CompilationUnitData, + CompilationUnitData +> { + override async analyze(info: ProgramInfo): Promise> { + const replacements: Replacement[] = []; + let importManager: ImportManager | null = null; + + for (const sourceFile of info.sourceFiles) { + if (!sourceFile.fileName.endsWith('main.server.ts')) { + continue; + } + + const bootstrapAppCalls: ts.CallExpression[] = []; + ts.forEachChild(sourceFile, function findCalls(node) { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'bootstrapApplication' && + node.arguments.length < 3 + ) { + bootstrapAppCalls.push(node); + } + ts.forEachChild(node, findCalls); + }); + + if (bootstrapAppCalls.length === 0) { + continue; + } + + for (const node of bootstrapAppCalls) { + const end = node.arguments[node.arguments.length - 1].getEnd(); + replacements.push( + new Replacement( + projectFile(sourceFile, info), + new TextUpdate({ + position: end, + end: end, + toInsert: ', context', + }), + ), + ); + + const arrowFunction = findArrowFunction(node); + if (arrowFunction && arrowFunction.parameters.length === 0) { + replacements.push( + new Replacement( + projectFile(sourceFile, info), + new TextUpdate({ + position: arrowFunction.parameters.end, + end: arrowFunction.parameters.end, + toInsert: 'context: BootstrapContext', + }), + ), + ); + } + } + + importManager ??= new ImportManager({ + generateUniqueIdentifier: () => null, + shouldUseSingleQuotes: () => true, + }); + + importManager.addImport({ + exportSymbolName: 'BootstrapContext', + exportModuleSpecifier: '@angular/platform-browser', + requestedFile: sourceFile, + }); + } + + if (importManager !== null) { + applyImportManagerChanges(importManager, replacements, info.sourceFiles, info); + } + + return confirmAsSerializable({replacements}); + } + + override async migrate(globalData: CompilationUnitData) { + return confirmAsSerializable(globalData); + } + + override async combine( + unitA: CompilationUnitData, + unitB: CompilationUnitData, + ): Promise> { + const seen = new Set(); + const combined: Replacement[] = []; + + [unitA.replacements, unitB.replacements].forEach((replacements) => { + replacements.forEach((current) => { + const {position, end, toInsert} = current.update.data; + const key = current.projectFile.id + '/' + position + '/' + end + '/' + toInsert; + + if (!seen.has(key)) { + seen.add(key); + combined.push(current); + } + }); + }); + + return confirmAsSerializable({replacements: combined}); + } + + override async globalMeta( + combinedData: CompilationUnitData, + ): Promise> { + return confirmAsSerializable(combinedData); + } + + override async stats() { + return confirmAsSerializable({}); + } +} diff --git a/packages/core/schematics/test/add_bootstrap_context_to_server_main_spec.ts b/packages/core/schematics/test/add_bootstrap_context_to_server_main_spec.ts new file mode 100644 index 00000000000..ba83c50649b --- /dev/null +++ b/packages/core/schematics/test/add_bootstrap_context_to_server_main_spec.ts @@ -0,0 +1,163 @@ +/*! + * @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.dev/license + */ + +import {normalize, virtualFs} from '@angular-devkit/core'; +import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; +import {HostTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing/index.js'; +import {resolve} from 'path'; + +describe('bootstrapApplication for server migration', () => { + let runner: SchematicTestRunner; + let host: TempScopedNodeJsSyncHost; + let tree: UnitTestTree; + + const migrationsJsonPath = resolve('../migrations.json'); + + function writeFile(filePath: string, contents: string) { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + function runMigration() { + return runner.runSchematic('add-bootstrap-context-to-server-main', {}, tree); + } + + beforeEach(() => { + runner = new SchematicTestRunner('test', migrationsJsonPath); + host = new TempScopedNodeJsSyncHost(); + tree = new UnitTestTree(new HostTree(host)); + + writeFile('/tsconfig.json', '{}'); + writeFile( + '/angular.json', + JSON.stringify({ + version: 1, + projects: { + t: { + root: '', + architect: { + build: {options: {tsConfig: './tsconfig.json'}}, + server: {options: {main: './main.server.ts'}}, + }, + }, + }, + }), + ); + }); + + it('should add BootstrapContext to bootstrapApplication call', async () => { + const inputContent = ` +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = () => bootstrapApplication(AppComponent, config); + +export default bootstrap; +`; + const expectedContent = ` +import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context); + +export default bootstrap; +`; + writeFile('/main.server.ts', inputContent); + await runMigration(); + const newContent = tree.readContent('/main.server.ts'); + expect(newContent).toEqual(expectedContent); + }); + + it('should add BootstrapContext to bootstrapApplication call in a block body', async () => { + const inputContent = ` +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = () => { + return bootstrapApplication(AppComponent, config); +}; + +export default bootstrap; +`; + const expectedContent = ` +import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = (context: BootstrapContext) => { + return bootstrapApplication(AppComponent, config, context); +}; + +export default bootstrap; +`; + writeFile('/main.server.ts', inputContent); + await runMigration(); + const newContent = tree.readContent('/main.server.ts'); + expect(newContent).toEqual(expectedContent); + }); + + it('should not change bootstrapApplication call that already has a context', async () => { + const inputContent = ` +import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context); + +export default bootstrap; +`; + writeFile('/main.server.ts', inputContent); + await runMigration(); + const newContent = tree.readContent('/main.server.ts'); + expect(newContent).toEqual(inputContent); + }); + + it('should add BootstrapContext to existing platform-browser import', async () => { + const inputContent = ` +import { bootstrapApplication, BrowserModule } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = () => bootstrapApplication(AppComponent, config); + +export default bootstrap; +`; + const expectedContent = ` +import { bootstrapApplication, BrowserModule, BootstrapContext } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = (context: BootstrapContext) => bootstrapApplication(AppComponent, config, context); + +export default bootstrap; +`; + writeFile('/main.server.ts', inputContent); + await runMigration(); + const newContent = tree.readContent('/main.server.ts'); + expect(newContent).toEqual(expectedContent); + }); + + it('should not modify other files', async () => { + const inputContent = ` +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/app.component'; +import { config } from './app/app.config.server'; + +const bootstrap = () => bootstrapApplication(AppComponent, config); + +export default bootstrap; +`; + writeFile('/main.ts', inputContent); + await runMigration(); + const newContent = tree.readContent('/main.ts'); + expect(newContent).toEqual(inputContent); + }); +}); diff --git a/packages/core/src/application/create_application.ts b/packages/core/src/application/create_application.ts index 95f9406edb0..24709540ec6 100644 --- a/packages/core/src/application/create_application.ts +++ b/packages/core/src/application/create_application.ts @@ -21,6 +21,8 @@ import {bootstrap} from '../platform/bootstrap'; import {profiler} from '../render3/profiler'; import {ProfilerEvent} from '../render3/profiler_types'; import {errorHandlerEnvironmentInitializer} from '../error_handler'; +import {RuntimeError, RuntimeErrorCode} from '../errors'; +import {PlatformRef} from '../platform/platform_ref'; /** * Internal create application API that implements the core application creation logic and optional @@ -38,17 +40,28 @@ export function internalCreateApplication(config: { rootComponent?: Type; appProviders?: Array; platformProviders?: Provider[]; + platformRef?: PlatformRef; }): Promise { + const {rootComponent, appProviders, platformProviders, platformRef} = config; profiler(ProfilerEvent.BootstrapApplicationStart); + + if (typeof ngServerMode !== 'undefined' && ngServerMode && !platformRef) { + throw new RuntimeError( + RuntimeErrorCode.PLATFORM_NOT_FOUND, + ngDevMode && + 'Missing Platform: This may be due to using `bootstrapApplication` on the server without passing a `BootstrapContext`. ' + + 'Please make sure that `bootstrapApplication` is called with a `context` argument.', + ); + } + try { - const {rootComponent, appProviders, platformProviders} = config; + const platformInjector = + platformRef?.injector ?? createOrReusePlatformInjector(platformProviders as StaticProvider[]); if ((typeof ngDevMode === 'undefined' || ngDevMode) && rootComponent !== undefined) { assertStandaloneComponentType(rootComponent); } - const platformInjector = createOrReusePlatformInjector(platformProviders as StaticProvider[]); - // Create root application injector based on a set of providers configured at the platform // bootstrap level as well as providers passed to the bootstrap call by a user. const allAppProviders = [ diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 61f00adfe41..39996ca4434 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -123,7 +123,6 @@ export { restoreComponentResolutionQueue as ɵrestoreComponentResolutionQueue, } from './metadata/resource_loading'; export {PendingTasksInternal as ɵPendingTasksInternal} from './pending_tasks_internal'; -export {ALLOW_MULTIPLE_PLATFORMS as ɵALLOW_MULTIPLE_PLATFORMS} from './platform/platform'; export {ENABLE_ROOT_COMPONENT_BOOTSTRAP as ɵENABLE_ROOT_COMPONENT_BOOTSTRAP} from './platform/bootstrap'; export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities'; export {AnimationRendererType as ɵAnimationRendererType} from './render/api'; diff --git a/packages/core/src/platform/bootstrap.ts b/packages/core/src/platform/bootstrap.ts index f44625f1867..2b24356db58 100644 --- a/packages/core/src/platform/bootstrap.ts +++ b/packages/core/src/platform/bootstrap.ts @@ -157,11 +157,11 @@ export function bootstrap( const localeId = envInjector.get(LOCALE_ID, DEFAULT_LOCALE_ID); setLocaleId(localeId || DEFAULT_LOCALE_ID); - const enableRootComponentBoostrap = envInjector.get( + const enableRootComponentbootstrap = envInjector.get( ENABLE_ROOT_COMPONENT_BOOTSTRAP, true, ); - if (!enableRootComponentBoostrap) { + if (!enableRootComponentbootstrap) { if (isApplicationBootstrapConfig(config)) { return envInjector.get(ApplicationRef); } @@ -192,8 +192,8 @@ export function bootstrap( } /** - * Having a separate symbol for the module boostrap implementation allows us to - * tree shake the module based boostrap implementation in standalone apps. + * Having a separate symbol for the module bootstrap implementation allows us to + * tree shake the module based bootstrap implementation in standalone apps. */ let moduleBootstrapImpl: undefined | typeof _moduleDoBootstrap; diff --git a/packages/core/src/platform/platform.ts b/packages/core/src/platform/platform.ts index 36179d84406..7427b2ead94 100644 --- a/packages/core/src/platform/platform.ts +++ b/packages/core/src/platform/platform.ts @@ -27,14 +27,6 @@ import {PLATFORM_DESTROY_LISTENERS} from './platform_destroy_listeners'; let _platformInjector: Injector | null = null; -/** - * Internal token to indicate whether having multiple bootstrapped platform should be allowed (only - * one bootstrapped platform is allowed by default). This token helps to support SSR scenarios. - */ -export const ALLOW_MULTIPLE_PLATFORMS = new InjectionToken( - ngDevMode ? 'AllowMultipleToken' : '', -); - /** * Creates a platform. * Platforms must be created on launch using this function. @@ -42,15 +34,20 @@ export const ALLOW_MULTIPLE_PLATFORMS = new InjectionToken( * @publicApi */ export function createPlatform(injector: Injector): PlatformRef { - if (_platformInjector && !_platformInjector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { + if (getPlatform()) { throw new RuntimeError( RuntimeErrorCode.MULTIPLE_PLATFORMS, ngDevMode && 'There can be only one platform. Destroy the previous one to create a new one.', ); } + publishDefaultGlobalUtils(); publishSignalConfiguration(); - _platformInjector = injector; + + // During SSR, using this setting and using an injector from the global can cause the + // injector to be used for a different requjest due to concurrency. + _platformInjector = typeof ngServerMode === 'undefined' || !ngServerMode ? injector : null; + const platform = injector.get(PlatformRef); runPlatformInitializers(injector); return platform; @@ -76,19 +73,19 @@ export function createPlatformFactory( const marker = new InjectionToken(desc); return (extraProviders: StaticProvider[] = []) => { let platform = getPlatform(); - if (!platform || platform.injector.get(ALLOW_MULTIPLE_PLATFORMS, false)) { + if (!platform) { const platformProviders: StaticProvider[] = [ ...providers, ...extraProviders, {provide: marker, useValue: true}, ]; - if (parentPlatformFactory) { - parentPlatformFactory(platformProviders); - } else { + + platform = + parentPlatformFactory?.(platformProviders) ?? createPlatform(createPlatformInjector(platformProviders, desc)); - } } - return assertPlatform(marker); + + return typeof ngServerMode !== 'undefined' && ngServerMode ? platform : assertPlatform(marker); }; } @@ -114,7 +111,6 @@ function createPlatformInjector(providers: StaticProvider[] = [], name?: string) */ export function assertPlatform(requiredToken: any): PlatformRef { const platform = getPlatform(); - if (!platform) { throw new RuntimeError(RuntimeErrorCode.PLATFORM_NOT_FOUND, ngDevMode && 'No platform exists!'); } @@ -133,11 +129,16 @@ export function assertPlatform(requiredToken: any): PlatformRef { } /** - * Returns the current platform. + * Returns the current platform in the browser environment. In the server environment, + * returns `null`. If you need access to the platform information, inject `PlatformRef` in your application. * * @publicApi */ export function getPlatform(): PlatformRef | null { + if (typeof ngServerMode !== 'undefined' && ngServerMode) { + return null; + } + return _platformInjector?.get(PlatformRef) ?? null; } @@ -145,6 +146,8 @@ export function getPlatform(): PlatformRef | null { * Destroys the current Angular platform and all Angular applications on the page. * Destroys all modules and listeners registered with the platform. * + * This function should not be used in a server environment, as it will be a no-op. + * * @publicApi */ export function destroyPlatform(): void { @@ -162,9 +165,16 @@ export function createOrReusePlatformInjector(providers: StaticProvider[] = []): if (_platformInjector) return _platformInjector; publishDefaultGlobalUtils(); + // Otherwise, setup a new platform injector and run platform initializers. const injector = createPlatformInjector(providers); - _platformInjector = injector; + + // During SSR, using this setting and using an injector from the global can cause the + // injector to be used for a different request due to concurrency. + if (typeof ngServerMode === 'undefined' || !ngServerMode) { + _platformInjector = injector; + } + publishSignalConfiguration(); runPlatformInitializers(injector); return injector; diff --git a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json index da336b1e79d..0389e384c76 100644 --- a/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json @@ -2,7 +2,6 @@ "chunks": { "main": [ "AFTER_RENDER_SEQUENCES_TO_ADD", - "ALLOW_MULTIPLE_PLATFORMS", "ANIMATIONS_DISABLED", "APP_BOOTSTRAP_LISTENER", "APP_ID", diff --git a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json index 87fc938601a..aaa5ac72275 100644 --- a/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json +++ b/packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json @@ -2,7 +2,6 @@ "chunks": { "main": [ "AFTER_RENDER_SEQUENCES_TO_ADD", - "ALLOW_MULTIPLE_PLATFORMS", "ANIMATIONS_DISABLED", "APP_BOOTSTRAP_LISTENER", "APP_ID", diff --git a/packages/platform-browser/src/browser.ts b/packages/platform-browser/src/browser.ts index d7b003e2597..cdebb4414a6 100644 --- a/packages/platform-browser/src/browser.ts +++ b/packages/platform-browser/src/browser.ts @@ -51,6 +51,19 @@ import {KeyEventsPlugin} from './dom/events/key_events'; import {SharedStylesHost} from './dom/shared_styles_host'; import {RuntimeErrorCode} from './errors'; +/** + * A context object that can be passed to `bootstrapApplication` to provide a pre-existing platform + * injector. + * + * @publicApi + */ +export interface BootstrapContext { + /** + * A reference to a platform. + */ + platformRef: PlatformRef; +} + /** * Bootstraps an instance of an Angular application and renders a standalone component as the * application's root component. More information about standalone components can be found in [this @@ -106,6 +119,9 @@ import {RuntimeErrorCode} from './errors'; * @param rootComponent A reference to a standalone component that should be rendered. * @param options Extra configuration for the bootstrap operation, see `ApplicationConfig` for * additional info. + * @param context Optional context object that can be used to provide a pre-existing + * platform injector. This is useful for advanced use-cases, for example, server-side + * rendering, where the platform is created for each request. * @returns A promise that returns an `ApplicationRef` instance once resolved. * * @publicApi @@ -113,8 +129,13 @@ import {RuntimeErrorCode} from './errors'; export function bootstrapApplication( rootComponent: Type, options?: ApplicationConfig, + context?: BootstrapContext, ): Promise { - const config = {rootComponent, ...createProvidersConfig(options)}; + const config = { + rootComponent, + platformRef: context?.platformRef, + ...createProvidersConfig(options), + }; // Attempt to resolve component resources before bootstrapping in JIT mode, // however don't interrupt the bootstrapping process. @@ -142,7 +163,7 @@ export function bootstrapApplication( * * @publicApi */ -export function createApplication(options?: ApplicationConfig) { +export function createApplication(options?: ApplicationConfig): Promise { return internalCreateApplication(createProvidersConfig(options)); } diff --git a/packages/platform-browser/src/platform-browser.ts b/packages/platform-browser/src/platform-browser.ts index 20605e936bc..e75fd77fd3d 100644 --- a/packages/platform-browser/src/platform-browser.ts +++ b/packages/platform-browser/src/platform-browser.ts @@ -8,6 +8,7 @@ export { bootstrapApplication, + BootstrapContext, BrowserModule, createApplication, platformBrowser, diff --git a/packages/platform-server/src/server.ts b/packages/platform-server/src/server.ts index b9ed814208d..f48b14f69c5 100644 --- a/packages/platform-server/src/server.ts +++ b/packages/platform-server/src/server.ts @@ -26,7 +26,6 @@ import { Provider, StaticProvider, Testability, - ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS, ɵsetDocument, ɵTESTABILITY as TESTABILITY, } from '@angular/core'; @@ -54,8 +53,6 @@ export const INTERNAL_SERVER_PLATFORM_PROVIDERS: StaticProvider[] = [ deps: [DOCUMENT, [Optional, INITIAL_CONFIG]], }, {provide: PlatformState, deps: [DOCUMENT]}, - // Add special provider that allows multiple instances of platformServer* to be created. - {provide: ALLOW_MULTIPLE_PLATFORMS, useValue: true}, ]; function initDominoAdapter(injector: Injector) { @@ -113,6 +110,13 @@ function _document(injector: Injector) { } /** + * Creates a server-side instance of an Angular platform. + * + * This platform should be used when performing server-side rendering of an Angular application. + * Standalone applications can be bootstrapped on the server using the `bootstrapApplication` + * function from `@angular/platform-browser`. When using `bootstrapApplication`, the `platformServer` + * should be created first and passed to the bootstrap function using the `BootstrapContext`. + * * @publicApi */ export function platformServer(extraProviders?: StaticProvider[] | undefined): PlatformRef { diff --git a/packages/platform-server/src/utils.ts b/packages/platform-server/src/utils.ts index aee7fe5c936..92baf0b6038 100644 --- a/packages/platform-server/src/utils.ts +++ b/packages/platform-server/src/utils.ts @@ -22,9 +22,10 @@ import { ɵstartMeasuring as startMeasuring, ɵstopMeasuring as stopMeasuring, } from '@angular/core'; +import {BootstrapContext} from '@angular/platform-browser'; -import {PlatformState} from './platform_state'; import {platformServer} from './server'; +import {PlatformState} from './platform_state'; import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens'; import {createScript} from './transfer_state'; @@ -291,14 +292,24 @@ export async function renderModule( /** * Bootstraps an instance of an Angular application and renders it to a string. - + * + * @usageNotes + * * ```ts - * const bootstrap = () => bootstrapApplication(RootComponent, appConfig); - * const output: string = await renderApplication(bootstrap); + * import { BootstrapContext, bootstrapApplication } from '@angular/platform-browser'; + * import { renderApplication } from '@angular/platform-server'; + * import { ApplicationConfig } from '@angular/core'; + * import { AppComponent } from './app.component'; + * + * const appConfig: ApplicationConfig = { providers: [...] }; + * const bootstrap = (context: BootstrapContext) => + * bootstrapApplication(AppComponent, config, context); + * const output = await renderApplication(bootstrap); * ``` * * @param bootstrap A method that when invoked returns a promise that returns an `ApplicationRef` - * instance once resolved. + * instance once resolved. The method is invoked with an `Injector` instance that + * provides access to the platform-level dependency injection context. * @param options Additional configuration for the render operation: * - `document` - the document of the page to render, either as an HTML string or * as a reference to the `document` instance. @@ -309,8 +320,8 @@ export async function renderModule( * * @publicApi */ -export async function renderApplication( - bootstrap: () => Promise, +export async function renderApplication( + bootstrap: (context: BootstrapContext) => Promise, options: {document?: string | Document; url?: string; platformProviders?: Provider[]}, ): Promise { const renderAppLabel = 'renderApplication'; @@ -321,7 +332,7 @@ export async function renderApplication( const platformRef = createServerPlatform(options); try { startMeasuring(bootstrapLabel); - const applicationRef = await bootstrap(); + const applicationRef = await bootstrap({platformRef}); stopMeasuring(bootstrapLabel); startMeasuring(_renderLabel); diff --git a/packages/platform-server/test/hydration_utils.ts b/packages/platform-server/test/hydration_utils.ts index 255767551ad..22e4f1f64e9 100644 --- a/packages/platform-server/test/hydration_utils.ts +++ b/packages/platform-server/test/hydration_utils.ts @@ -20,6 +20,7 @@ import { } from '@angular/core'; import { bootstrapApplication, + BootstrapContext, HydrationFeature, provideClientHydration, HydrationFeatureKind, @@ -263,7 +264,8 @@ export async function ssr( enableHydration ? provideClientHydration(...hydrationFeatures()) : [], ]; - const bootstrap = () => bootstrapApplication(component, {providers}); + const bootstrap = (context: BootstrapContext) => + bootstrapApplication(component, {providers}, context); return await renderApplication(bootstrap, { document: options?.doc ?? defaultHtml, diff --git a/packages/platform-server/test/integration_spec.ts b/packages/platform-server/test/integration_spec.ts index d2f22815c22..d7c0b276b3d 100644 --- a/packages/platform-server/test/integration_spec.ts +++ b/packages/platform-server/test/integration_spec.ts @@ -50,6 +50,7 @@ import { import {TestBed} from '@angular/core/testing'; import { bootstrapApplication, + BootstrapContext, BrowserModule, provideClientHydration, Title, @@ -76,11 +77,12 @@ const APP_CONFIG: ApplicationConfig = { function getStandaloneBootstrapFn( component: Type, providers: Array = [], -): () => Promise { - return () => +): (context: BootstrapContext) => Promise { + return (context: BootstrapContext) => bootstrapApplication( component, mergeApplicationConfig(APP_CONFIG, {providers: [...providers, provideNgReflectAttributes()]}), + context, ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b663d8b727..3e010c6fef2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,9 @@ packageExtensionsChecksum: sha256-3L73Fw32UVtE6x5BJxJPBtQtH/mgsr31grNpdhHP1IY= pnpmfileChecksum: sha256-+7KlkWdGv7+7wb/qym+KoHN4eBGq9TNblo2Ln6HTxz0= patchedDependencies: + '@angular/ssr': + hash: bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad + path: tools/pnpm-patches/@angular__ssr.patch dagre-d3-es@7.0.11: hash: 08ae1f30d46402ef53e1486454faa875683cf4152a8aab71ac255078ebfaccdd path: tools/pnpm-patches/dagre-d3-es+7.0.11.patch @@ -23,7 +26,7 @@ importers: dependencies: '@angular-devkit/build-angular': specifier: 21.0.0-next.1 - version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@20.19.13)(bufferutil@4.0.9)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(protractor@7.0.0)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) + version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@20.19.13)(bufferutil@4.0.9)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(protractor@7.0.0)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) '@angular-devkit/core': specifier: 21.0.0-next.1 version: 21.0.0-next.1(chokidar@4.0.3) @@ -38,7 +41,7 @@ importers: version: link:packages/benchpress '@angular/build': specifier: 21.0.0-next.1 - version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@20.19.13)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) + version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@20.19.13)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) '@angular/cdk': specifier: 21.0.0-next.2 version: 21.0.0-next.2(rxjs@7.8.2) @@ -89,7 +92,7 @@ importers: version: link:packages/service-worker '@angular/ssr': specifier: 21.0.0-next.1 - version: 21.0.0-next.1 + version: 21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad) '@angular/upgrade': specifier: 'workspace: *' version: link:packages/upgrade @@ -522,13 +525,13 @@ importers: version: 5.36.0 '@angular-devkit/build-angular': specifier: 21.0.0-next.1 - version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@24.3.0)(bufferutil@4.0.9)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(protractor@7.0.0)(tsx@4.20.5)(typescript@5.9.2)(utf-8-validate@6.0.5)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) + version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@24.3.0)(bufferutil@4.0.9)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(protractor@7.0.0)(tsx@4.20.5)(typescript@5.9.2)(utf-8-validate@6.0.5)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) '@angular/animations': specifier: workspace:* version: link:../packages/animations '@angular/build': specifier: 21.0.0-next.1 - version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) + version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) '@angular/cdk': specifier: 21.0.0-next.2 version: 21.0.0-next.2(rxjs@7.8.2) @@ -567,7 +570,7 @@ importers: version: link:../packages/router '@angular/ssr': specifier: 21.0.0-next.1 - version: 21.0.0-next.1 + version: 21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad) '@codemirror/autocomplete': specifier: 6.18.6 version: 6.18.6 @@ -816,7 +819,7 @@ importers: version: link:../packages/benchpress '@angular/build': specifier: 21.0.0-next.1 - version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) + version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) '@angular/common': specifier: workspace:* version: link:../packages/common @@ -987,7 +990,7 @@ importers: version: link:../../../animations '@angular/build': specifier: 21.0.0-next.1 - version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) + version: 21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) '@angular/common': specifier: workspace:* version: link:../../../common @@ -11949,13 +11952,13 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@20.19.13)(bufferutil@4.0.9)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(protractor@7.0.0)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': + '@angular-devkit/build-angular@21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@20.19.13)(bufferutil@4.0.9)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(protractor@7.0.0)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2100.0-next.1(chokidar@4.0.3) '@angular-devkit/build-webpack': 0.2100.0-next.1(chokidar@4.0.3)(webpack-dev-server@5.2.2(bufferutil@4.0.9)(webpack@5.101.3(esbuild@0.25.9)))(webpack@5.101.3(esbuild@0.25.9)) '@angular-devkit/core': 21.0.0-next.1(chokidar@4.0.3) - '@angular/build': 21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@20.19.13)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(less@4.4.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) + '@angular/build': 21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@20.19.13)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(less@4.4.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) '@angular/compiler-cli': link:packages/compiler-cli '@angular/core': link:packages/core '@angular/localize': link:packages/localize @@ -12013,7 +12016,7 @@ snapshots: webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.101.3(esbuild@0.25.9)) optionalDependencies: - '@angular/ssr': 21.0.0-next.1 + '@angular/ssr': 21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad) esbuild: 0.25.9 karma: 6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) protractor: 7.0.0 @@ -12039,13 +12042,13 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-angular@21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@24.3.0)(bufferutil@4.0.9)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(protractor@7.0.0)(tsx@4.20.5)(typescript@5.9.2)(utf-8-validate@6.0.5)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': + '@angular-devkit/build-angular@21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@24.3.0)(bufferutil@4.0.9)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(protractor@7.0.0)(tsx@4.20.5)(typescript@5.9.2)(utf-8-validate@6.0.5)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2100.0-next.1(chokidar@4.0.3) '@angular-devkit/build-webpack': 0.2100.0-next.1(chokidar@4.0.3)(webpack-dev-server@5.2.2(bufferutil@4.0.9)(webpack@5.101.3(esbuild@0.25.9)))(webpack@5.101.3(esbuild@0.25.9)) '@angular-devkit/core': 21.0.0-next.1(chokidar@4.0.3) - '@angular/build': 21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) + '@angular/build': 21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1) '@angular/compiler-cli': link:packages/compiler-cli '@angular/core': link:packages/core '@angular/localize': link:packages/localize @@ -12103,7 +12106,7 @@ snapshots: webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(webpack@5.101.3(esbuild@0.25.9)) optionalDependencies: - '@angular/ssr': 21.0.0-next.1 + '@angular/ssr': 21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad) esbuild: 0.25.9 karma: 6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) protractor: 7.0.0 @@ -12166,7 +12169,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular/build@21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@20.19.13)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(less@4.4.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': + '@angular/build@21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@20.19.13)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(less@4.4.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2100.0-next.1(chokidar@4.0.3) @@ -12204,7 +12207,7 @@ snapshots: vite: 7.1.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(sass@1.91.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) watchpack: 2.4.4 optionalDependencies: - '@angular/ssr': 21.0.0-next.1 + '@angular/ssr': 21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad) karma: 6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) less: 4.4.1 lmdb: 3.4.2 @@ -12223,7 +12226,7 @@ snapshots: - tsx - yaml - '@angular/build@21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@20.19.13)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': + '@angular/build@21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@20.19.13)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@20.19.13)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2100.0-next.1(chokidar@4.0.3) @@ -12261,7 +12264,7 @@ snapshots: vite: 7.1.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1) watchpack: 2.4.4 optionalDependencies: - '@angular/ssr': 21.0.0-next.1 + '@angular/ssr': 21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad) karma: 6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) less: 4.4.1 lmdb: 3.4.2 @@ -12280,7 +12283,7 @@ snapshots: - tsx - yaml - '@angular/build@21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': + '@angular/build@21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.43.1)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2100.0-next.1(chokidar@4.0.3) @@ -12318,7 +12321,7 @@ snapshots: vite: 7.1.3(@types/node@24.3.0)(jiti@1.21.7)(less@4.4.1)(sass@1.91.0)(terser@5.43.1)(tsx@4.20.5)(yaml@2.8.1) watchpack: 2.4.4 optionalDependencies: - '@angular/ssr': 21.0.0-next.1 + '@angular/ssr': 21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad) karma: 6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) less: 4.4.1 lmdb: 3.4.2 @@ -12337,7 +12340,7 @@ snapshots: - tsx - yaml - '@angular/build@21.0.0-next.1(@angular/ssr@21.0.0-next.1)(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': + '@angular/build@21.0.0-next.1(@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad))(@types/node@24.3.0)(chokidar@4.0.3)(jiti@1.21.7)(karma@6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(postcss@8.5.6)(terser@5.44.0)(tslib@2.8.1)(tsx@4.20.5)(typescript@5.9.2)(vitest@3.2.4(@types/node@24.3.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1))(yaml@2.8.1)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2100.0-next.1(chokidar@4.0.3) @@ -12375,7 +12378,7 @@ snapshots: vite: 7.1.3(@types/node@24.3.0)(jiti@1.21.7)(less@4.4.1)(sass@1.91.0)(terser@5.44.0)(tsx@4.20.5)(yaml@2.8.1) watchpack: 2.4.4 optionalDependencies: - '@angular/ssr': 21.0.0-next.1 + '@angular/ssr': 21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad) karma: 6.4.4(bufferutil@4.0.9)(utf-8-validate@6.0.5) less: 4.4.1 lmdb: 3.4.2 @@ -12534,7 +12537,7 @@ snapshots: - '@modelcontextprotocol/sdk' - '@react-native-async-storage/async-storage' - '@angular/ssr@21.0.0-next.1': + '@angular/ssr@21.0.0-next.1(patch_hash=bce5fa7069bf90818d317d479ff7a7652139b4046a45193a38bb34692b3fa5ad)': dependencies: '@angular/common': link:packages/common '@angular/core': link:packages/core diff --git a/tools/pnpm-patches/@angular__ssr.patch b/tools/pnpm-patches/@angular__ssr.patch new file mode 100644 index 00000000000..a06387401a6 --- /dev/null +++ b/tools/pnpm-patches/@angular__ssr.patch @@ -0,0 +1,22 @@ +diff --git a/fesm2022/ssr.mjs b/fesm2022/ssr.mjs +index 4976c181079b86773efbd49f5101fb1cdd670205..b20e3d886663011e31ba15a40e72969b92e39fd2 100755 +--- a/fesm2022/ssr.mjs ++++ b/fesm2022/ssr.mjs +@@ -352,7 +352,7 @@ async function renderAngular(html, bootstrap, url, platformProviders, serverCont + applicationRef = moduleRef.injector.get(ApplicationRef); + } + else { +- applicationRef = await bootstrap(); ++ applicationRef = await bootstrap({platformRef}); + } + // Block until application is stable. + await applicationRef.whenStable(); +@@ -1244,7 +1244,7 @@ async function getRoutesFromAngularRouterConfig(bootstrap, document, url, invoke + applicationRef = moduleRef.injector.get(ApplicationRef); + } + else { +- applicationRef = await bootstrap(); ++ applicationRef = await bootstrap({platformRef}); + } + const injector = applicationRef.injector; + const router = injector.get(Router);