/** * @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.io/license */ import {APP_BOOTSTRAP_LISTENER, ApplicationRef, whenStable} from '../application_ref'; import {ENABLED_SSR_FEATURES} from '../application_tokens'; import {Console} from '../console'; import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, Injector, makeEnvironmentProviders} from '../di'; import {inject} from '../di/injector_compatibility'; import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors'; import {enableLocateOrCreateContainerRefImpl} from '../linker/view_container_ref'; 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 {getDocument} from '../render3/interfaces/document'; import {isPlatformBrowser} from '../render3/util/misc_utils'; import {TransferState} from '../transfer_state'; import {NgZone} from '../zone'; import {cleanupDehydratedViews} from './cleanup'; import {IS_HYDRATION_DOM_REUSE_ENABLED, PRESERVE_HOST_CONTENT} from './tokens'; import {enableRetrieveHydrationInfoImpl, NGH_DATA_KEY, SSR_CONTENT_INTEGRITY_MARKER} from './utils'; import {enableFindMatchingDehydratedViewImpl} from './views'; /** * Indicates whether the hydration-related code was added, * prevents adding it multiple times. */ let isHydrationSupportEnabled = 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. */ 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(); } } /** * 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. ` + `Note: this feature is in Developer Preview mode. ` + `Learn more at https://angular.io/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, injector: Injector): Promise { const whenStablePromise = whenStable(appRef); if (typeof ngDevMode !== 'undefined' && ngDevMode) { const timeoutTime = APPLICATION_IS_STABLE_TIMEOUT; const console = injector.get(Console); const ngZone = 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; } /** * 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 { return makeEnvironmentProviders([ { provide: IS_HYDRATION_DOM_REUSE_ENABLED, useFactory: () => { let isEnabled = true; if (isPlatformBrowser()) { // 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 && (typeof ngDevMode !== 'undefined' && ngDevMode)) { 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.'); // tslint:disable-next-line:no-console console.warn(message); } } if (isEnabled) { inject(ENABLED_SSR_FEATURES).add('hydration'); } return isEnabled; }, }, { provide: ENVIRONMENT_INITIALIZER, useValue: () => { // Since this function is used across both server and client, // make sure that the runtime code is only added when invoked // on the client. Moving forward, the `isPlatformBrowser` check should // be replaced with a tree-shakable alternative (e.g. `isServer` // flag). if (isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) { verifySsrContentsIntegrity(); enableHydrationRuntimeSupport(); } }, multi: true, }, { 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 isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED); } }, { provide: APP_BOOTSTRAP_LISTENER, useFactory: () => { if (isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) { const appRef = inject(ApplicationRef); const injector = inject(Injector); 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 // to ensure that change detection is properly run afterward. whenStableWithTimeout(appRef, injector).then(() => { NgZone.assertInAngularZone(); cleanupDehydratedViews(appRef); if (typeof ngDevMode !== 'undefined' && ngDevMode) { printHydrationStats(injector); } }); }; } return () => {}; // noop }, multi: true, } ]); } /** * * @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)); } /** * Verifies whether the DOM contains a special marker added during SSR time to make sure * there is no SSR'ed contents transformations happen after SSR is completed. Typically that * happens either by CDN or during the build process as an optimization to remove comment nodes. * Hydration process requires comment nodes produced by Angular to locate correct DOM segments. * When this special marker is *not* present - throw an error and do not proceed with hydration, * since it will not be able to function correctly. * * Note: this function is invoked only on the client, so it's safe to use DOM APIs. */ function verifySsrContentsIntegrity(): void { const doc = getDocument(); let hydrationMarker: Node|undefined; for (const node of doc.body.childNodes) { if (node.nodeType === Node.COMMENT_NODE && node.textContent?.trim() === SSR_CONTENT_INTEGRITY_MARKER) { hydrationMarker = node; break; } } if (!hydrationMarker) { throw new RuntimeError( RuntimeErrorCode.MISSING_SSR_CONTENT_INTEGRITY_MARKER, typeof ngDevMode !== 'undefined' && ngDevMode && 'Angular hydration logic detected that HTML content of this page was modified after it ' + 'was produced during server side rendering. Make sure that there are no optimizations ' + 'that remove comment nodes from HTML enabled on your CDN. Angular hydration ' + 'relies on HTML produced by the server, including whitespaces and comment nodes.'); } }