forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathapp-engine.ts
203 lines (177 loc) · 7.58 KB
/
app-engine.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/**
* @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 type { AngularServerApp, getOrCreateAngularServerApp } from './app';
import { Hooks } from './hooks';
import { getPotentialLocaleIdFromUrl, getPreferredLocale } from './i18n';
import { EntryPointExports, getAngularAppEngineManifest } from './manifest';
import { joinUrlParts } from './utils/url';
/**
* Angular server application engine.
* Manages Angular server applications (including localized ones), handles rendering requests,
* and optionally transforms index HTML before rendering.
*
* @remarks This class should be instantiated once and used as a singleton across the server-side
* application to ensure consistent handling of rendering requests and resource management.
*/
export class AngularAppEngine {
/**
* A flag to enable or disable the rendering of prerendered routes.
*
* Typically used during development to avoid prerendering all routes ahead of time,
* allowing them to be rendered on the fly as requested.
*
* @private
*/
static ɵallowStaticRouteRender = false;
/**
* Hooks for extending or modifying the behavior of the server application.
* These hooks are used by the Angular CLI when running the development server and
* provide extensibility points for the application lifecycle.
*
* @private
*/
static ɵhooks = /* #__PURE__*/ new Hooks();
/**
* The manifest for the server application.
*/
private readonly manifest = getAngularAppEngineManifest();
/**
* A map of supported locales from the server application's manifest.
*/
private readonly supportedLocales: ReadonlyArray<string> = Object.keys(
this.manifest.supportedLocales,
);
/**
* A cache that holds entry points, keyed by their potential locale string.
*/
private readonly entryPointsCache = new Map<string, Promise<EntryPointExports>>();
/**
* Handles an incoming HTTP request by serving prerendered content, performing server-side rendering,
* or delivering a static file for client-side rendered routes based on the `RenderMode` setting.
*
* @param request - The HTTP request to handle.
* @param requestContext - Optional context for rendering, such as metadata associated with the request.
* @returns A promise that resolves to the resulting HTTP response object, or `null` if no matching Angular route is found.
*
* @remarks A request to `https://www.example.com/page/index.html` will serve or render the Angular route
* corresponding to `https://www.example.com/page`.
*/
async handle(request: Request, requestContext?: unknown): Promise<Response | null> {
const serverApp = await this.getAngularServerAppForRequest(request);
if (serverApp) {
return serverApp.handle(request, requestContext);
}
if (this.supportedLocales.length > 1) {
// Redirect to the preferred language if i18n is enabled.
return this.redirectBasedOnAcceptLanguage(request);
}
return null;
}
/**
* Handles requests for the base path when i18n is enabled.
* Redirects the user to a locale-specific path based on the `Accept-Language` header.
*
* @param request The incoming request.
* @returns A `Response` object with a 302 redirect, or `null` if i18n is not enabled
* or the request is not for the base path.
*/
private redirectBasedOnAcceptLanguage(request: Request): Response | null {
const { basePath, supportedLocales } = this.manifest;
// If the request is not for the base path, it's not our responsibility to handle it.
const { pathname } = new URL(request.url);
if (pathname !== basePath) {
return null;
}
// For requests to the base path (typically '/'), attempt to extract the preferred locale
// from the 'Accept-Language' header.
const preferredLocale = getPreferredLocale(
request.headers.get('Accept-Language') || '*',
this.supportedLocales,
);
if (preferredLocale) {
const subPath = supportedLocales[preferredLocale];
if (subPath !== undefined) {
return new Response(null, {
status: 302, // Use a 302 redirect as language preference may change.
headers: {
'Location': joinUrlParts(pathname, subPath),
'Vary': 'Accept-Language',
},
});
}
}
return null;
}
/**
* Retrieves the Angular server application instance for a given request.
*
* This method checks if the request URL corresponds to an Angular application entry point.
* If so, it initializes or retrieves an instance of the Angular server application for that entry point.
* Requests that resemble file requests (except for `/index.html`) are skipped.
*
* @param request - The incoming HTTP request object.
* @returns A promise that resolves to an `AngularServerApp` instance if a valid entry point is found,
* or `null` if no entry point matches the request URL.
*/
private async getAngularServerAppForRequest(request: Request): Promise<AngularServerApp | null> {
// Skip if the request looks like a file but not `/index.html`.
const url = new URL(request.url);
const entryPoint = await this.getEntryPointExportsForUrl(url);
if (!entryPoint) {
return null;
}
// Note: Using `instanceof` is not feasible here because `AngularServerApp` will
// be located in separate bundles, making `instanceof` checks unreliable.
const ɵgetOrCreateAngularServerApp =
entryPoint.ɵgetOrCreateAngularServerApp as typeof getOrCreateAngularServerApp;
const serverApp = ɵgetOrCreateAngularServerApp({
allowStaticRouteRender: AngularAppEngine.ɵallowStaticRouteRender,
hooks: AngularAppEngine.ɵhooks,
});
return serverApp;
}
/**
* Retrieves the exports for a specific entry point, caching the result.
*
* @param potentialLocale - The locale string used to find the corresponding entry point.
* @returns A promise that resolves to the entry point exports or `undefined` if not found.
*/
private getEntryPointExports(potentialLocale: string): Promise<EntryPointExports> | undefined {
const cachedEntryPoint = this.entryPointsCache.get(potentialLocale);
if (cachedEntryPoint) {
return cachedEntryPoint;
}
const { entryPoints } = this.manifest;
const entryPoint = entryPoints[potentialLocale];
if (!entryPoint) {
return undefined;
}
const entryPointExports = entryPoint();
this.entryPointsCache.set(potentialLocale, entryPointExports);
return entryPointExports;
}
/**
* Retrieves the entry point for a given URL by determining the locale and mapping it to
* the appropriate application bundle.
*
* This method determines the appropriate entry point and locale for rendering the application by examining the URL.
* If there is only one entry point available, it is returned regardless of the URL.
* Otherwise, the method extracts a potential locale identifier from the URL and looks up the corresponding entry point.
*
* @param url - The URL of the request.
* @returns A promise that resolves to the entry point exports or `undefined` if not found.
*/
private getEntryPointExportsForUrl(url: URL): Promise<EntryPointExports> | undefined {
const { basePath } = this.manifest;
if (this.supportedLocales.length === 1) {
return this.getEntryPointExports('');
}
const potentialLocale = getPotentialLocaleIdFromUrl(url, basePath);
return this.getEntryPointExports(potentialLocale) ?? this.getEntryPointExports('');
}
}