mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
411 lines
15 KiB
TypeScript
411 lines
15 KiB
TypeScript
/**
|
|
* @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_BOOTSTRAP_LISTENER, ApplicationRef} from '../application/application_ref';
|
|
import {Console} from '../console';
|
|
import {
|
|
ENVIRONMENT_INITIALIZER,
|
|
EnvironmentProviders,
|
|
Injector,
|
|
makeEnvironmentProviders,
|
|
Provider,
|
|
} from '../di';
|
|
import {inject} from '../di/injector_compatibility';
|
|
import {formatRuntimeError, RuntimeErrorCode} from '../errors';
|
|
import {enableLocateOrCreateContainerRefImpl} from '../linker/view_container_ref';
|
|
import {enableLocateOrCreateI18nNodeImpl} from '../render3/i18n/i18n_apply';
|
|
import {enableLocateOrCreateElementNodeImpl} from '../render3/instructions/element';
|
|
import {enableLocateOrCreateElementContainerNodeImpl} from '../render3/instructions/element_container';
|
|
import {enableApplyRootElementTransformImpl} from '../render3/instructions/shared';
|
|
import {enableLocateOrCreateContainerAnchorImpl} from '../render3/instructions/template';
|
|
import {enableLocateOrCreateTextNodeImpl} from '../render3/instructions/text';
|
|
import {TransferState} from '../transfer_state';
|
|
import {performanceMarkFeature} from '../util/performance';
|
|
import {NgZone} from '../zone';
|
|
import {withEventReplay} from './event_replay';
|
|
|
|
import {
|
|
ChangeDetectionScheduler,
|
|
NotificationSource,
|
|
} from '../change_detection/scheduling/zoneless_scheduling';
|
|
import {DEHYDRATED_BLOCK_REGISTRY, DehydratedBlockRegistry} from '../defer/registry';
|
|
import {processAndInitTriggers} from '../defer/triggering';
|
|
import {DOCUMENT} from '../document';
|
|
import {DOC_PAGE_BASE_URL} from '../error_details_base_url';
|
|
import {cleanupDehydratedViews} from './cleanup';
|
|
import {
|
|
enableClaimDehydratedIcuCaseImpl,
|
|
enablePrepareI18nBlockForHydrationImpl,
|
|
setIsI18nHydrationSupportEnabled,
|
|
} from './i18n';
|
|
import {gatherDeferBlocksCommentNodes} from './node_lookup_utils';
|
|
import {
|
|
IS_HYDRATION_DOM_REUSE_ENABLED,
|
|
IS_I18N_HYDRATION_ENABLED,
|
|
IS_INCREMENTAL_HYDRATION_ENABLED,
|
|
PRESERVE_HOST_CONTENT,
|
|
} from './tokens';
|
|
import {
|
|
appendDeferBlocksToJSActionMap,
|
|
countBlocksSkippedByHydration,
|
|
enableRetrieveDeferBlockDataImpl,
|
|
enableRetrieveHydrationInfoImpl,
|
|
isIncrementalHydrationEnabled,
|
|
NGH_DATA_KEY,
|
|
processBlockData,
|
|
verifySsrContentsIntegrity,
|
|
} from './utils';
|
|
import {enableFindMatchingDehydratedViewImpl} from './views';
|
|
|
|
/**
|
|
* Indicates whether the hydration-related code was added,
|
|
* prevents adding it multiple times.
|
|
*/
|
|
let isHydrationSupportEnabled = false;
|
|
|
|
/**
|
|
* Indicates whether the i18n-related code was added,
|
|
* prevents adding it multiple times.
|
|
*
|
|
* Note: This merely controls whether the code is loaded,
|
|
* while `setIsI18nHydrationSupportEnabled` determines
|
|
* whether i18n blocks are serialized or hydrated.
|
|
*/
|
|
let isI18nHydrationRuntimeSupportEnabled = false;
|
|
|
|
/**
|
|
* Indicates whether the incremental hydration code was added,
|
|
* prevents adding it multiple times.
|
|
*/
|
|
let isIncrementalHydrationRuntimeSupportEnabled = false;
|
|
|
|
/**
|
|
* Defines a period of time that Angular waits for the `ApplicationRef.isStable` to emit `true`.
|
|
* If there was no event with the `true` value during this time, Angular reports a warning.
|
|
*/
|
|
export const APPLICATION_IS_STABLE_TIMEOUT = 10_000;
|
|
|
|
/**
|
|
* Brings the necessary hydration code in tree-shakable manner.
|
|
* The code is only present when the `provideClientHydration` is
|
|
* invoked. Otherwise, this code is tree-shaken away during the
|
|
* build optimization step.
|
|
*
|
|
* This technique allows us to swap implementations of methods so
|
|
* tree shaking works appropriately when hydration is disabled or
|
|
* enabled. It brings in the appropriate version of the method that
|
|
* supports hydration only when enabled.
|
|
*/
|
|
function enableHydrationRuntimeSupport() {
|
|
if (!isHydrationSupportEnabled) {
|
|
isHydrationSupportEnabled = true;
|
|
enableRetrieveHydrationInfoImpl();
|
|
enableLocateOrCreateElementNodeImpl();
|
|
enableLocateOrCreateTextNodeImpl();
|
|
enableLocateOrCreateElementContainerNodeImpl();
|
|
enableLocateOrCreateContainerAnchorImpl();
|
|
enableLocateOrCreateContainerRefImpl();
|
|
enableFindMatchingDehydratedViewImpl();
|
|
enableApplyRootElementTransformImpl();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Brings the necessary i18n hydration code in tree-shakable manner.
|
|
* Similar to `enableHydrationRuntimeSupport`, the code is only
|
|
* present when `withI18nSupport` is invoked.
|
|
*/
|
|
function enableI18nHydrationRuntimeSupport() {
|
|
if (!isI18nHydrationRuntimeSupportEnabled) {
|
|
isI18nHydrationRuntimeSupportEnabled = true;
|
|
enableLocateOrCreateI18nNodeImpl();
|
|
enablePrepareI18nBlockForHydrationImpl();
|
|
enableClaimDehydratedIcuCaseImpl();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Brings the necessary incremental hydration code in tree-shakable manner.
|
|
* Similar to `enableHydrationRuntimeSupport`, the code is only
|
|
* present when `enableIncrementalHydrationRuntimeSupport` is invoked.
|
|
*/
|
|
function enableIncrementalHydrationRuntimeSupport() {
|
|
if (!isIncrementalHydrationRuntimeSupportEnabled) {
|
|
isIncrementalHydrationRuntimeSupportEnabled = true;
|
|
enableRetrieveDeferBlockDataImpl();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Outputs a message with hydration stats into a console.
|
|
*/
|
|
function printHydrationStats(injector: Injector) {
|
|
const console = injector.get(Console);
|
|
const message =
|
|
`Angular hydrated ${ngDevMode!.hydratedComponents} component(s) ` +
|
|
`and ${ngDevMode!.hydratedNodes} node(s), ` +
|
|
`${ngDevMode!.componentsSkippedHydration} component(s) were skipped. ` +
|
|
(isIncrementalHydrationEnabled(injector)
|
|
? `${ngDevMode!.deferBlocksWithIncrementalHydration} defer block(s) were configured to use incremental hydration. `
|
|
: '') +
|
|
`Learn more at ${DOC_PAGE_BASE_URL}/guide/hydration.`;
|
|
// tslint:disable-next-line:no-console
|
|
console.log(message);
|
|
}
|
|
|
|
/**
|
|
* Returns a Promise that is resolved when an application becomes stable.
|
|
*/
|
|
function whenStableWithTimeout(appRef: ApplicationRef): Promise<void> {
|
|
const whenStablePromise = appRef.whenStable();
|
|
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
|
|
const timeoutTime = APPLICATION_IS_STABLE_TIMEOUT;
|
|
const console = appRef.injector.get(Console);
|
|
const ngZone = appRef.injector.get(NgZone);
|
|
|
|
// The following call should not and does not prevent the app to become stable
|
|
// We cannot use RxJS timer here because the app would remain unstable.
|
|
// This also avoids an extra change detection cycle.
|
|
const timeoutId = ngZone.runOutsideAngular(() => {
|
|
return setTimeout(() => logWarningOnStableTimedout(timeoutTime, console), timeoutTime);
|
|
});
|
|
|
|
whenStablePromise.finally(() => clearTimeout(timeoutId));
|
|
}
|
|
|
|
return whenStablePromise;
|
|
}
|
|
|
|
/**
|
|
* Defines a name of an attribute that is added to the <body> tag
|
|
* in the `index.html` file in case a given route was configured
|
|
* with `RenderMode.Client`. 'cm' is an abbreviation for "Client Mode".
|
|
*/
|
|
export const CLIENT_RENDER_MODE_FLAG = 'ngcm';
|
|
|
|
/**
|
|
* Checks whether the `RenderMode.Client` was defined for the current route.
|
|
*/
|
|
function isClientRenderModeEnabled(doc: Document): boolean {
|
|
return (
|
|
(typeof ngServerMode === 'undefined' || !ngServerMode) &&
|
|
doc.body.hasAttribute(CLIENT_RENDER_MODE_FLAG)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns a set of providers required to setup hydration support
|
|
* for an application that is server side rendered. This function is
|
|
* included into the `provideClientHydration` public API function from
|
|
* the `platform-browser` package.
|
|
*
|
|
* The function sets up an internal flag that would be recognized during
|
|
* the server side rendering time as well, so there is no need to
|
|
* configure or change anything in NgUniversal to enable the feature.
|
|
*/
|
|
export function withDomHydration(): EnvironmentProviders {
|
|
const providers: Provider[] = [
|
|
{
|
|
provide: IS_HYDRATION_DOM_REUSE_ENABLED,
|
|
useFactory: () => {
|
|
let isEnabled = true;
|
|
if (typeof ngServerMode === 'undefined' || !ngServerMode) {
|
|
// On the client, verify that the server response contains
|
|
// hydration annotations. Otherwise, keep hydration disabled.
|
|
const transferState = inject(TransferState, {optional: true});
|
|
isEnabled = !!transferState?.get(NGH_DATA_KEY, null);
|
|
}
|
|
if (isEnabled) {
|
|
performanceMarkFeature('NgHydration');
|
|
}
|
|
return isEnabled;
|
|
},
|
|
},
|
|
{
|
|
provide: ENVIRONMENT_INITIALIZER,
|
|
useValue: () => {
|
|
// i18n support is enabled by calling withI18nSupport(), but there's
|
|
// no way to turn it off (e.g. for tests), so we turn it off by default.
|
|
setIsI18nHydrationSupportEnabled(false);
|
|
|
|
if (typeof ngServerMode !== 'undefined' && ngServerMode) {
|
|
// Since this function is used across both server and client,
|
|
// make sure that the runtime code is only added when invoked
|
|
// on the client (see the `enableHydrationRuntimeSupport` function
|
|
// call below).
|
|
return;
|
|
}
|
|
|
|
const doc = inject(DOCUMENT);
|
|
if (inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
|
|
verifySsrContentsIntegrity(doc);
|
|
enableHydrationRuntimeSupport();
|
|
} else if (
|
|
typeof ngDevMode !== 'undefined' &&
|
|
ngDevMode &&
|
|
!isClientRenderModeEnabled(doc)
|
|
) {
|
|
const console = inject(Console);
|
|
const message = formatRuntimeError(
|
|
RuntimeErrorCode.MISSING_HYDRATION_ANNOTATIONS,
|
|
'Angular hydration was requested on the client, but there was no ' +
|
|
'serialized information present in the server response, ' +
|
|
'thus hydration was not enabled. ' +
|
|
'Make sure the `provideClientHydration()` is included into the list ' +
|
|
'of providers in the server part of the application configuration.',
|
|
);
|
|
console.warn(message);
|
|
}
|
|
},
|
|
multi: true,
|
|
},
|
|
];
|
|
|
|
if (typeof ngServerMode === 'undefined' || !ngServerMode) {
|
|
providers.push(
|
|
{
|
|
provide: PRESERVE_HOST_CONTENT,
|
|
useFactory: () => {
|
|
// Preserve host element content only in a browser
|
|
// environment and when hydration is configured properly.
|
|
// On a server, an application is rendered from scratch,
|
|
// so the host content needs to be empty.
|
|
return inject(IS_HYDRATION_DOM_REUSE_ENABLED);
|
|
},
|
|
},
|
|
{
|
|
provide: APP_BOOTSTRAP_LISTENER,
|
|
useFactory: () => {
|
|
const scheduler = inject(ChangeDetectionScheduler);
|
|
if (inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
|
|
const appRef = inject(ApplicationRef);
|
|
|
|
return () => {
|
|
// Wait until an app becomes stable and cleanup all views that
|
|
// were not claimed during the application bootstrap process.
|
|
// The timing is similar to when we start the serialization process
|
|
// on the server.
|
|
//
|
|
// Note: the cleanup task *MUST* be scheduled within the Angular zone in Zone apps
|
|
// to ensure that change detection is properly run afterward.
|
|
whenStableWithTimeout(appRef).then(() => {
|
|
// Note: we have to check whether the application is destroyed before
|
|
// performing other operations with the `injector`.
|
|
// The application may be destroyed **before** it becomes stable, so when
|
|
// the `whenStableWithTimeout` resolves, the injector might already be in
|
|
// a destroyed state. Thus, calling `injector.get` would throw an error
|
|
// indicating that the injector has already been destroyed.
|
|
if (appRef.destroyed) {
|
|
return;
|
|
}
|
|
|
|
cleanupDehydratedViews(appRef);
|
|
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
|
|
countBlocksSkippedByHydration(appRef.injector);
|
|
printHydrationStats(appRef.injector);
|
|
}
|
|
// We need to schedule the execution of the render hooks because the hydration cleanup alters the DOM.
|
|
scheduler.notify(NotificationSource.RenderHook);
|
|
});
|
|
};
|
|
}
|
|
return () => {}; // noop
|
|
},
|
|
multi: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
return makeEnvironmentProviders(providers);
|
|
}
|
|
|
|
/**
|
|
* Returns a set of providers required to setup support for i18n hydration.
|
|
* Requires hydration to be enabled separately.
|
|
* @see [I18N](guide/hydration#i18n)
|
|
*/
|
|
export function withI18nSupport(): Provider[] {
|
|
return [
|
|
{
|
|
provide: IS_I18N_HYDRATION_ENABLED,
|
|
useFactory: () => inject(IS_HYDRATION_DOM_REUSE_ENABLED),
|
|
},
|
|
{
|
|
provide: ENVIRONMENT_INITIALIZER,
|
|
useValue: () => {
|
|
if (inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
|
|
enableI18nHydrationRuntimeSupport();
|
|
setIsI18nHydrationSupportEnabled(true);
|
|
performanceMarkFeature('NgI18nHydration');
|
|
}
|
|
},
|
|
multi: true,
|
|
},
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Returns a set of providers required to setup support for incremental hydration.
|
|
* Requires hydration to be enabled separately.
|
|
* Enabling incremental hydration also enables event replay for the entire app.
|
|
* @see [Incremental Hydration](guide/incremental-hydration#how-do-you-enable-incremental-hydration-in-angular)
|
|
*/
|
|
export function withIncrementalHydration(): Provider[] {
|
|
const providers: Provider[] = [
|
|
withEventReplay(),
|
|
{
|
|
provide: IS_INCREMENTAL_HYDRATION_ENABLED,
|
|
useValue: true,
|
|
},
|
|
{
|
|
provide: DEHYDRATED_BLOCK_REGISTRY,
|
|
useClass: DehydratedBlockRegistry,
|
|
},
|
|
{
|
|
provide: ENVIRONMENT_INITIALIZER,
|
|
useValue: () => {
|
|
enableIncrementalHydrationRuntimeSupport();
|
|
performanceMarkFeature('NgIncrementalHydration');
|
|
},
|
|
multi: true,
|
|
},
|
|
];
|
|
|
|
if (typeof ngServerMode === 'undefined' || !ngServerMode) {
|
|
providers.push({
|
|
provide: APP_BOOTSTRAP_LISTENER,
|
|
useFactory: () => {
|
|
const injector = inject(Injector);
|
|
const doc = inject(DOCUMENT);
|
|
|
|
return () => {
|
|
const deferBlockData = processBlockData(injector);
|
|
const commentsByBlockId = gatherDeferBlocksCommentNodes(doc, doc.body);
|
|
processAndInitTriggers(injector, deferBlockData, commentsByBlockId);
|
|
appendDeferBlocksToJSActionMap(doc, injector);
|
|
};
|
|
},
|
|
multi: true,
|
|
});
|
|
}
|
|
|
|
return providers;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param time The time in ms until the stable timedout warning message is logged
|
|
*/
|
|
function logWarningOnStableTimedout(time: number, console: Console): void {
|
|
const message =
|
|
`Angular hydration expected the ApplicationRef.isStable() to emit \`true\`, but it ` +
|
|
`didn't happen within ${time}ms. Angular hydration logic depends on the application becoming stable ` +
|
|
`as a signal to complete hydration process.`;
|
|
|
|
console.warn(formatRuntimeError(RuntimeErrorCode.HYDRATION_STABLE_TIMEDOUT, message));
|
|
}
|