-
Notifications
You must be signed in to change notification settings - Fork 27.1k
Expand file tree
/
Copy pathutils.ts
More file actions
353 lines (317 loc) Β· 12 KB
/
utils.ts
File metadata and controls
353 lines (317 loc) Β· 12 KB
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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
/**
* @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 {
APP_ID,
ApplicationRef,
CSP_NONCE,
InjectionToken,
PlatformRef,
Provider,
Renderer2,
StaticProvider,
Type,
Ι΅annotateForHydration as annotateForHydration,
Ι΅IS_HYDRATION_DOM_REUSE_ENABLED as IS_HYDRATION_DOM_REUSE_ENABLED,
Ι΅SSR_CONTENT_INTEGRITY_MARKER as SSR_CONTENT_INTEGRITY_MARKER,
Ι΅startMeasuring as startMeasuring,
Ι΅stopMeasuring as stopMeasuring,
} from '@angular/core';
import {BootstrapContext} from '@angular/platform-browser';
import {platformServer} from './server';
import {PlatformState} from './platform_state';
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
import {createScript} from './transfer_state';
/**
* Event dispatch (JSAction) script is inlined into the HTML by the build
* process to avoid extra blocking request on a page. The script looks like this:
* ```html
* <script type="text/javascript" id="ng-event-dispatch-contract">...</script>
* ```
* This const represents the "id" attribute value.
*/
export const EVENT_DISPATCH_SCRIPT_ID = 'ng-event-dispatch-contract';
interface PlatformOptions {
document?: string | Document;
url?: string;
platformProviders?: Provider[];
}
/**
* Creates an instance of a server platform (with or without JIT compiler support
* depending on the `ngJitMode` global const value), using provided options.
*/
function createServerPlatform(options: PlatformOptions): PlatformRef {
const extraProviders = options.platformProviders ?? [];
const measuringLabel = 'createServerPlatform';
startMeasuring(measuringLabel);
const platform = platformServer([
{provide: INITIAL_CONFIG, useValue: {document: options.document, url: options.url}},
extraProviders,
]);
stopMeasuring(measuringLabel);
return platform;
}
/**
* Finds and returns inlined event dispatch script if it exists.
* See the `EVENT_DISPATCH_SCRIPT_ID` const docs for additional info.
*/
function findEventDispatchScript(doc: Document) {
return doc.getElementById(EVENT_DISPATCH_SCRIPT_ID);
}
/**
* Removes inlined event dispatch script if it exists.
* See the `EVENT_DISPATCH_SCRIPT_ID` const docs for additional info.
*/
function removeEventDispatchScript(doc: Document) {
findEventDispatchScript(doc)?.remove();
}
/**
* Annotate nodes for hydration and remove event dispatch script when not needed.
*/
function prepareForHydration(platformState: PlatformState, applicationRef: ApplicationRef): void {
const measuringLabel = 'prepareForHydration';
startMeasuring(measuringLabel);
const environmentInjector = applicationRef.injector;
const doc = platformState.getDocument();
if (!environmentInjector.get(IS_HYDRATION_DOM_REUSE_ENABLED, false)) {
// Hydration is diabled, remove inlined event dispatch script.
// (which was injected by the build process) from the HTML.
removeEventDispatchScript(doc);
return;
}
appendSsrContentIntegrityMarker(doc);
const eventTypesToReplay = annotateForHydration(applicationRef, doc);
if (eventTypesToReplay.regular.size || eventTypesToReplay.capture.size) {
insertEventRecordScript(
environmentInjector.get(APP_ID),
doc,
eventTypesToReplay,
environmentInjector.get(CSP_NONCE, null),
);
} else {
// No events to replay, we should remove inlined event dispatch script
// (which was injected by the build process) from the HTML.
removeEventDispatchScript(doc);
}
stopMeasuring(measuringLabel);
}
/**
* Creates a marker comment node and append it into the `<body>`.
* Some CDNs have mechanisms to remove all comment node from HTML.
* This behaviour breaks hydration, so we'll detect on the client side if this
* marker comment is still available or else throw an error
*/
function appendSsrContentIntegrityMarker(doc: Document) {
// Adding a ng hydration marker comment
const comment = doc.createComment(SSR_CONTENT_INTEGRITY_MARKER);
doc.body.firstChild
? doc.body.insertBefore(comment, doc.body.firstChild)
: doc.body.append(comment);
}
/**
* Adds the `ng-server-context` attribute to host elements of all bootstrapped components
* within a given application.
*/
function appendServerContextInfo(applicationRef: ApplicationRef) {
const injector = applicationRef.injector;
let serverContext = sanitizeServerContext(injector.get(SERVER_CONTEXT, DEFAULT_SERVER_CONTEXT));
applicationRef.components.forEach((componentRef) => {
const renderer = componentRef.injector.get(Renderer2);
const element = componentRef.location.nativeElement;
if (element) {
renderer.setAttribute(element, 'ng-server-context', serverContext);
}
});
}
function insertEventRecordScript(
appId: string,
doc: Document,
eventTypesToReplay: {regular: Set<string>; capture: Set<string>},
nonce: string | null,
): void {
const measuringLabel = 'insertEventRecordScript';
startMeasuring(measuringLabel);
const {regular, capture} = eventTypesToReplay;
const eventDispatchScript = findEventDispatchScript(doc);
// Note: this is only true when build with the CLI tooling, which inserts the script in the HTML
if (eventDispatchScript) {
// This is defined in packages/core/primitives/event-dispatch/contract_binary.ts
const replayScriptContents =
`window.__jsaction_bootstrap(` +
`document.body,` +
`"${appId}",` +
`${JSON.stringify(Array.from(regular))},` +
`${JSON.stringify(Array.from(capture))}` +
`);`;
const replayScript = createScript(doc, replayScriptContents, nonce);
// Insert replay script right after inlined event dispatch script, since it
// relies on `__jsaction_bootstrap` to be defined in the global scope.
eventDispatchScript.after(replayScript);
}
stopMeasuring(measuringLabel);
}
/**
* Renders an Angular application to a string.
*
* @private
*
* @param platformRef - Reference to the Angular platform.
* @param applicationRef - Reference to the Angular application.
* @returns A promise that resolves to the rendered string.
*/
export async function renderInternal(
platformRef: PlatformRef,
applicationRef: ApplicationRef,
): Promise<string> {
const platformState = platformRef.injector.get(PlatformState);
prepareForHydration(platformState, applicationRef);
appendServerContextInfo(applicationRef);
// Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
const environmentInjector = applicationRef.injector;
const callbacks = environmentInjector.get(BEFORE_APP_SERIALIZED, null);
if (callbacks) {
const asyncCallbacks: Promise<void>[] = [];
for (const callback of callbacks) {
try {
const callbackResult = callback();
if (callbackResult) {
asyncCallbacks.push(callbackResult);
}
} catch (e) {
// Ignore exceptions.
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
}
}
if (asyncCallbacks.length) {
for (const result of await Promise.allSettled(asyncCallbacks)) {
if (result.status === 'rejected') {
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', result.reason);
}
}
}
}
return platformState.renderToString();
}
/**
* Destroy the application in a macrotask, this allows pending promises to be settled and errors
* to be surfaced to the users.
*/
function asyncDestroyPlatform(platformRef: PlatformRef): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(() => {
platformRef.destroy();
resolve();
}, 0);
});
}
/**
* Specifies the value that should be used if no server context value has been provided.
*/
const DEFAULT_SERVER_CONTEXT = 'other';
/**
* An internal token that allows providing extra information about the server context
* (e.g. whether SSR or SSG was used). The value is a string and characters other
* than [a-zA-Z0-9\-] are removed. See the default value in `DEFAULT_SERVER_CONTEXT` const.
*/
export const SERVER_CONTEXT = new InjectionToken<string>('SERVER_CONTEXT');
/**
* Sanitizes provided server context:
* - removes all characters other than a-z, A-Z, 0-9 and `-`
* - returns `other` if nothing is provided or the string is empty after sanitization
*/
function sanitizeServerContext(serverContext: string): string {
const context = serverContext.replace(/[^a-zA-Z0-9\-]/g, '');
return context.length > 0 ? context : DEFAULT_SERVER_CONTEXT;
}
/**
* Bootstraps an application using provided NgModule and serializes the page content to string.
*
* @param moduleType A reference to an NgModule that should be used for bootstrap.
* @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.
* - `url` - the URL for the current render request.
* - `extraProviders` - set of platform level providers for the current render request.
*
* @publicApi
*/
export async function renderModule<T>(
moduleType: Type<T>,
options: {document?: string | Document; url?: string; extraProviders?: StaticProvider[]},
): Promise<string> {
const {document, url, extraProviders: platformProviders} = options;
const platformRef = createServerPlatform({document, url, platformProviders});
try {
const moduleRef = await platformRef.bootstrapModule(moduleType);
const applicationRef = moduleRef.injector.get(ApplicationRef);
const measuringLabel = 'whenStable';
startMeasuring(measuringLabel);
// Block until application is stable.
await applicationRef.whenStable();
stopMeasuring(measuringLabel);
return await renderInternal(platformRef, applicationRef);
} finally {
await asyncDestroyPlatform(platformRef);
}
}
/**
* Bootstraps an instance of an Angular application and renders it to a string.
*
* @usageNotes
*
* ```ts
* 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. 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.
* - `url` - the URL for the current render request.
* - `platformProviders` - the platform level providers for the current render request.
*
* @returns A Promise, that returns serialized (to a string) rendered page, once resolved.
*
* @publicApi
*/
export async function renderApplication(
bootstrap: (context: BootstrapContext) => Promise<ApplicationRef>,
options: {document?: string | Document; url?: string; platformProviders?: Provider[]},
): Promise<string> {
const renderAppLabel = 'renderApplication';
const bootstrapLabel = 'bootstrap';
const _renderLabel = '_render';
startMeasuring(renderAppLabel);
const platformRef = createServerPlatform(options);
try {
startMeasuring(bootstrapLabel);
const applicationRef = await bootstrap({platformRef});
stopMeasuring(bootstrapLabel);
startMeasuring(_renderLabel);
const measuringLabel = 'whenStable';
startMeasuring(measuringLabel);
// Block until application is stable.
await applicationRef.whenStable();
stopMeasuring(measuringLabel);
const rendered = await renderInternal(platformRef, applicationRef);
stopMeasuring(_renderLabel);
return rendered;
} finally {
await asyncDestroyPlatform(platformRef);
stopMeasuring(renderAppLabel);
}
}