Skip to content

Commit 1c18518

Browse files
committed
refactor(@angular/ssr): expose private APIs for build system integration and refactor app management
- Exposed several utility functions as private APIs to support the integration with the build system. - Removed `isDevMode` and caching logic from `AngularAppEngine`. This was necessary to better handle updates when using Vite. Instead, `AngularServerApp` is now treated as a singleton to simplify management. - Switched asset storage from an `Object` to a `Map` in the manifest for improved efficiency and consistency. This refactor sets the groundwork for seamless wiring with the build system.
1 parent d6155d2 commit 1c18518

13 files changed

+155
-160
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export { ServerRenderContext as ɵServerRenderContext } from './src/render';
10+
export { getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig } from './src/routes/ng-routes';
11+
export {
12+
getOrCreateAngularServerApp as ɵgetOrCreateAngularServerApp,
13+
destroyAngularServerApp as ɵdestroyAngularServerApp,
14+
} from './src/app';
15+
export {
16+
setAngularAppManifest as ɵsetAngularAppManifest,
17+
setAngularAppEngineManifest as ɵsetAngularAppEngineManifest,
18+
} from './src/manifest';

packages/angular/ssr/public_api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export {
1212
type CommonEngineOptions,
1313
} from './src/common-engine/common-engine';
1414

15-
export { getRoutesFromAngularRouterConfig as ɵgetRoutesFromAngularRouterConfig } from './src/routes/ng-routes';
15+
export * from './private_export';

packages/angular/ssr/src/app-engine.ts

Lines changed: 20 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { AngularServerApp } from './app';
109
import { Hooks } from './hooks';
1110
import { getPotentialLocaleIdFromUrl } from './i18n';
12-
import { getAngularAppEngineManifest } from './manifest';
11+
import { EntryPointExports, getAngularAppEngineManifest } from './manifest';
1312

1413
/**
1514
* Angular server application engine.
@@ -19,43 +18,33 @@ import { getAngularAppEngineManifest } from './manifest';
1918
export class AngularAppEngine {
2019
/**
2120
* Hooks for extending or modifying the behavior of the server application.
22-
* @internal This property is accessed by the Angular CLI when running the dev-server.
21+
* These hooks are used by the Angular CLI when running the development server and
22+
* provide extensibility points for the application lifecycle.
23+
*
24+
* @internal
2325
*/
2426
static hooks = new Hooks();
2527

2628
/**
27-
* Hooks for extending or modifying the behavior of the server application.
28-
* This instance can be used to attach custom functionality to various events in the server application lifecycle.
29+
* Provides access to the hooks for extending or modifying the server application's behavior.
30+
* This allows attaching custom functionality to various server application lifecycle events.
31+
*
2932
* @internal
3033
*/
3134
get hooks(): Hooks {
3235
return AngularAppEngine.hooks;
3336
}
3437

35-
/**
36-
* Specifies if the application is operating in development mode.
37-
* This property controls the activation of features intended for production, such as caching mechanisms.
38-
* @internal
39-
*/
40-
static isDevMode = false;
41-
4238
/**
4339
* The manifest for the server application.
4440
*/
4541
private readonly manifest = getAngularAppEngineManifest();
4642

4743
/**
48-
* Map of locale strings to corresponding `AngularServerApp` instances.
49-
* Each instance represents an Angular server application.
50-
*/
51-
private readonly appsCache = new Map<string, AngularServerApp>();
52-
53-
/**
54-
* Renders an HTTP request using the appropriate Angular server application and returns a response.
44+
* Renders a response for the given HTTP request using the server application.
5545
*
56-
* This method determines the entry point for the Angular server application based on the request URL,
57-
* and caches the server application instances for reuse. If the application is in development mode,
58-
* the cache is bypassed and a new instance is created for each request.
46+
* This method processes the request, determines the appropriate route and rendering context,
47+
* and returns an HTTP response.
5948
*
6049
* If the request URL appears to be for a file (excluding `/index.html`), the method returns `null`.
6150
* A request to `https://www.example.com/page/index.html` will render the Angular route
@@ -69,25 +58,14 @@ export class AngularAppEngine {
6958
async render(request: Request, requestContext?: unknown): Promise<Response | null> {
7059
// Skip if the request looks like a file but not `/index.html`.
7160
const url = new URL(request.url);
72-
7361
const entryPoint = this.getEntryPointFromUrl(url);
7462
if (!entryPoint) {
7563
return null;
7664
}
7765

78-
const [locale, loadModule] = entryPoint;
79-
let serverApp = this.appsCache.get(locale);
80-
if (!serverApp) {
81-
const { AngularServerApp } = await loadModule();
82-
serverApp = new AngularServerApp({
83-
isDevMode: AngularAppEngine.isDevMode,
84-
hooks: this.hooks,
85-
});
86-
87-
if (!AngularAppEngine.isDevMode) {
88-
this.appsCache.set(locale, serverApp);
89-
}
90-
}
66+
const { ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp } = await entryPoint();
67+
const serverApp = getOrCreateAngularServerApp();
68+
serverApp.hooks = this.hooks;
9169

9270
return serverApp.render(request, requestContext);
9371
}
@@ -99,30 +77,18 @@ export class AngularAppEngine {
9977
* If there is only one entry point available, it is returned regardless of the URL.
10078
* Otherwise, the method extracts a potential locale identifier from the URL and looks up the corresponding entry point.
10179
*
102-
* @param url - The URL used to derive the locale and determine the entry point.
103-
* @returns An array containing:
104-
* - The first element is the locale extracted from the URL.
105-
* - The second element is a function that returns a promise resolving to an object with the `AngularServerApp` type.
106-
*
107-
* Returns `null` if no matching entry point is found for the extracted locale.
80+
* @param url - The URL used to derive the locale and determine the appropriate entry point.
81+
* @returns A function that returns a promise resolving to an object with the `EntryPointExports` type,
82+
* or `undefined` if no matching entry point is found for the extracted locale.
10883
*/
109-
private getEntryPointFromUrl(url: URL):
110-
| [
111-
locale: string,
112-
loadModule: () => Promise<{
113-
AngularServerApp: typeof AngularServerApp;
114-
}>,
115-
]
116-
| null {
117-
// Find bundle for locale
84+
private getEntryPointFromUrl(url: URL): (() => Promise<EntryPointExports>) | undefined {
11885
const { entryPoints, basePath } = this.manifest;
11986
if (entryPoints.size === 1) {
120-
return entryPoints.entries().next().value;
87+
return entryPoints.values().next().value;
12188
}
12289

12390
const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);
124-
const entryPoint = entryPoints.get(potentialLocale);
12591

126-
return entryPoint ? [potentialLocale, entryPoint] : null;
92+
return entryPoints.get(potentialLocale);
12793
}
12894
}

packages/angular/ssr/src/app.ts

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,48 +12,23 @@ import { getAngularAppManifest } from './manifest';
1212
import { ServerRenderContext, render } from './render';
1313
import { ServerRouter } from './routes/router';
1414

15-
/**
16-
* Configuration options for initializing a `AngularServerApp` instance.
17-
*/
18-
export interface AngularServerAppOptions {
19-
/**
20-
* Indicates whether the application is in development mode.
21-
*
22-
* When set to `true`, the application runs in development mode with additional debugging features.
23-
*/
24-
isDevMode?: boolean;
25-
26-
/**
27-
* Optional hooks for customizing the server application's behavior.
28-
*/
29-
hooks?: Hooks;
30-
}
31-
3215
/**
3316
* Represents a locale-specific Angular server application managed by the server application engine.
3417
*
3518
* The `AngularServerApp` class handles server-side rendering and asset management for a specific locale.
3619
*/
3720
export class AngularServerApp {
38-
/**
39-
* The manifest associated with this server application.
40-
* @internal
41-
*/
42-
readonly manifest = getAngularAppManifest();
43-
4421
/**
4522
* Hooks for extending or modifying the behavior of the server application.
4623
* This instance can be used to attach custom functionality to various events in the server application lifecycle.
47-
* @internal
4824
*/
49-
readonly hooks: Hooks;
25+
hooks = new Hooks();
5026

5127
/**
52-
* Specifies if the server application is operating in development mode.
53-
* This property controls the activation of features intended for production, such as caching mechanisms.
28+
* The manifest associated with this server application.
5429
* @internal
5530
*/
56-
readonly isDevMode: boolean;
31+
readonly manifest = getAngularAppManifest();
5732

5833
/**
5934
* An instance of ServerAsset that handles server-side asset.
@@ -66,18 +41,6 @@ export class AngularServerApp {
6641
*/
6742
private router: ServerRouter | undefined;
6843

69-
/**
70-
* Creates a new `AngularServerApp` instance with the provided configuration options.
71-
*
72-
* @param options - The configuration options for the server application.
73-
* - `isDevMode`: Flag indicating if the application is in development mode.
74-
* - `hooks`: Optional hooks for customizing application behavior.
75-
*/
76-
constructor(readonly options: AngularServerAppOptions) {
77-
this.isDevMode = options.isDevMode ?? false;
78-
this.hooks = options.hooks ?? new Hooks();
79-
}
80-
8144
/**
8245
* Renders a response for the given HTTP request using the server application.
8346
*
@@ -113,3 +76,34 @@ export class AngularServerApp {
11376
return render(this, request, serverContext, requestContext);
11477
}
11578
}
79+
80+
let angularServerApp: AngularServerApp | undefined;
81+
82+
/**
83+
* Retrieves or creates an instance of `AngularServerApp`.
84+
* - If an instance of `AngularServerApp` already exists, it will return the existing one.
85+
* - If no instance exists, it will create a new one with the provided options.
86+
* @returns The existing or newly created instance of `AngularServerApp`.
87+
*/
88+
export function getOrCreateAngularServerApp(): AngularServerApp {
89+
return (angularServerApp ??= new AngularServerApp());
90+
}
91+
92+
/**
93+
* Resets the instance of `AngularServerApp` to undefined, effectively
94+
* clearing the reference. Use this to recreate the instance.
95+
*/
96+
export function resetAngularServerApp(): void {
97+
angularServerApp = undefined;
98+
}
99+
100+
/**
101+
* Destroys the existing `AngularServerApp` instance, releasing associated resources and resetting the
102+
* reference to `undefined`.
103+
*
104+
* This function is primarily used to enable the recreation of the `AngularServerApp` instance,
105+
* typically when server configuration or application state needs to be refreshed.
106+
*/
107+
export function destroyAngularServerApp(): void {
108+
angularServerApp = undefined;
109+
}

packages/angular/ssr/src/assets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class ServerAssets {
2727
* @throws Error If the asset path is not found in the manifest, an error is thrown.
2828
*/
2929
async getServerAsset(path: string): Promise<string> {
30-
const asset = this.manifest.assets[path];
30+
const asset = this.manifest.assets.get(path);
3131
if (!asset) {
3232
throw new Error(`Server asset '${path}' does not exist.`);
3333
}

packages/angular/ssr/src/global.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
declare const ngDevMode: boolean | undefined;

packages/angular/ssr/src/manifest.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,25 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import type { AngularServerApp } from './app';
9+
import type { destroyAngularServerApp, getOrCreateAngularServerApp } from './app';
1010
import type { SerializableRouteTreeNode } from './routes/route-tree';
1111
import { AngularBootstrap } from './utils/ng';
1212

13+
/**
14+
* Represents the exports of an Angular server application entry point.
15+
*/
16+
export interface EntryPointExports {
17+
/**
18+
* A reference to the function that creates an Angular server application instance.
19+
*/
20+
ɵgetOrCreateAngularServerApp: typeof getOrCreateAngularServerApp;
21+
22+
/**
23+
* A reference to the function that destroys the `AngularServerApp` instance.
24+
*/
25+
ɵdestroyAngularServerApp: typeof destroyAngularServerApp;
26+
}
27+
1328
/**
1429
* Manifest for the Angular server application engine, defining entry points.
1530
*/
@@ -18,11 +33,9 @@ export interface AngularAppEngineManifest {
1833
* A map of entry points for the server application.
1934
* Each entry in the map consists of:
2035
* - `key`: The base href for the entry point.
21-
* - `value`: A function that returns a promise resolving to an object containing the `AngularServerApp` type.
36+
* - `value`: A function that returns a promise resolving to an object of type `EntryPointExports`.
2237
*/
23-
readonly entryPoints: Readonly<
24-
Map<string, () => Promise<{ AngularServerApp: typeof AngularServerApp }>>
25-
>;
38+
readonly entryPoints: Readonly<Map<string, () => Promise<EntryPointExports>>>;
2639

2740
/**
2841
* The base path for the server application.
@@ -36,12 +49,12 @@ export interface AngularAppEngineManifest {
3649
*/
3750
export interface AngularAppManifest {
3851
/**
39-
* A record of assets required by the server application.
40-
* Each entry in the record consists of:
52+
* A map of assets required by the server application.
53+
* Each entry in the map consists of:
4154
* - `key`: The path of the asset.
4255
* - `value`: A function returning a promise that resolves to the file contents of the asset.
4356
*/
44-
readonly assets: Readonly<Record<string, () => Promise<string>>>;
57+
readonly assets: Readonly<Map<string, () => Promise<string>>>;
4558

4659
/**
4760
* The bootstrap mechanism for the server application.

packages/angular/ssr/src/render.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,25 @@ export async function render(
6464
);
6565
}
6666

67-
const { manifest, hooks, isDevMode } = app;
68-
69-
if (isDevMode) {
67+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
7068
// Need to clean up GENERATED_COMP_IDS map in `@angular/core`.
7169
// Otherwise an incorrect component ID generation collision detected warning will be displayed in development.
7270
// See: https://github.com/angular/angular-cli/issues/25924
7371
ɵresetCompiledComponents();
74-
75-
// An Angular Console Provider that does not print a set of predefined logs.
76-
platformProviders.push({
77-
provide: ɵConsole,
78-
// Using `useClass` would necessitate decorating `Console` with `@Injectable`,
79-
// which would require switching from `ts_library` to `ng_module`. This change
80-
// would also necessitate various patches of `@angular/bazel` to support ESM.
81-
useFactory: () => new Console(),
82-
});
8372
}
8473

85-
let html = await app.assets.getIndexServerHtml();
74+
// An Angular Console Provider that does not print a set of predefined logs.
75+
platformProviders.push({
76+
provide: ɵConsole,
77+
// Using `useClass` would necessitate decorating `Console` with `@Injectable`,
78+
// which would require switching from `ts_library` to `ng_module`. This change
79+
// would also necessitate various patches of `@angular/bazel` to support ESM.
80+
useFactory: () => new Console(),
81+
});
82+
83+
const { manifest, hooks, assets } = app;
84+
85+
let html = await assets.getIndexServerHtml();
8686
// Skip extra microtask if there are no pre hooks.
8787
if (hooks.has('html:transform:pre')) {
8888
html = await hooks.run('html:transform:pre', { html });

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,12 @@ export async function getRoutesFromAngularRouterConfig(
188188
document: string,
189189
url: URL,
190190
): Promise<AngularRouterConfigResult> {
191-
// Need to clean up GENERATED_COMP_IDS map in `@angular/core`.
192-
// Otherwise an incorrect component ID generation collision detected warning will be displayed in development.
193-
// See: https://github.com/angular/angular-cli/issues/25924
194-
ɵresetCompiledComponents();
191+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
192+
// Need to clean up GENERATED_COMP_IDS map in `@angular/core`.
193+
// Otherwise an incorrect component ID generation collision detected warning will be displayed in development.
194+
// See: https://github.com/angular/angular-cli/issues/25924
195+
ɵresetCompiledComponents();
196+
}
195197

196198
const { protocol, host } = url;
197199

0 commit comments

Comments
 (0)