From dd2f53b9cd0b86e2adce6484d421956ccce8b58f Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Wed, 1 Oct 2025 23:00:05 +0200 Subject: [PATCH] refactor(core): warning when `hydration` trigger is used without hydration being enabled (#64185) This replaces the error we were throwing before the change. This allows component with defer triggerrs to be used on both SSR'd and CSR. fixes #64184 PR Close #64185 --- packages/core/src/core_private_export.ts | 1 + packages/core/src/defer/instructions.ts | 13 ++++-- packages/core/src/hydration/utils.ts | 26 ++++++++---- .../test/incremental_hydration_spec.ts | 41 +++++++++---------- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/packages/core/src/core_private_export.ts b/packages/core/src/core_private_export.ts index 4544a89ed58..d74a25bab58 100644 --- a/packages/core/src/core_private_export.ts +++ b/packages/core/src/core_private_export.ts @@ -102,6 +102,7 @@ export { HydrationInfo as ɵHydrationInfo, readHydrationInfo as ɵreadHydrationInfo, SSR_CONTENT_INTEGRITY_MARKER as ɵSSR_CONTENT_INTEGRITY_MARKER, + resetIncrementalHydrationEnabledWarnedForTests as ɵresetIncrementalHydrationEnabledWarnedForTests, } from './hydration/utils'; export { CurrencyIndex as ɵCurrencyIndex, diff --git a/packages/core/src/defer/instructions.ts b/packages/core/src/defer/instructions.ts index 9626d5b2dd0..4050aee27de 100644 --- a/packages/core/src/defer/instructions.ts +++ b/packages/core/src/defer/instructions.ts @@ -52,7 +52,11 @@ import { trackTriggerForDebugging, } from './utils'; import {DEHYDRATED_BLOCK_REGISTRY, DehydratedBlockRegistry} from './registry'; -import {assertIncrementalHydrationIsConfigured, assertSsrIdDefined} from '../hydration/utils'; +import { + warnIncrementalHydrationNotConfigured, + assertSsrIdDefined, + isIncrementalHydrationEnabled, +} from '../hydration/utils'; import {ɵɵdeferEnableTimerScheduling, renderPlaceholder} from './rendering'; import { @@ -136,6 +140,7 @@ export function ɵɵdefer( const tNode = declareNoDirectiveHostTemplate(lView, tView, index, null, 0, 0); const injector = lView[INJECTOR]; + const incrementalHydrationEnabled = isIncrementalHydrationEnabled(injector); if (tView.firstCreatePass) { performanceMarkFeature('NgDefer'); @@ -143,8 +148,8 @@ export function ɵɵdefer( if (typeof ngHmrMode !== 'undefined' && ngHmrMode) { logHmrWarning(injector); } - if (hasHydrateTriggers(flags)) { - assertIncrementalHydrationIsConfigured(injector); + if (hasHydrateTriggers(flags) && !incrementalHydrationEnabled) { + warnIncrementalHydrationNotConfigured(); } } @@ -198,7 +203,7 @@ export function ɵɵdefer( setLDeferBlockDetails(lView, adjustedIndex, lDetails); let registry: DehydratedBlockRegistry | null = null; - if (ssrUniqueId !== null) { + if (ssrUniqueId !== null && incrementalHydrationEnabled) { // Store this defer block in the registry, to have an access to // internal data structures from hydration runtime code. registry = injector.get(DEHYDRATED_BLOCK_REGISTRY); diff --git a/packages/core/src/hydration/utils.ts b/packages/core/src/hydration/utils.ts index 96c0111102f..28322c5408d 100644 --- a/packages/core/src/hydration/utils.ts +++ b/packages/core/src/hydration/utils.ts @@ -34,7 +34,7 @@ import { SerializedView, } from './interfaces'; import {IS_INCREMENTAL_HYDRATION_ENABLED, JSACTION_BLOCK_ELEMENT_MAP} from './tokens'; -import {RuntimeError, RuntimeErrorCode} from '../errors'; +import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors'; import {DeferBlockTrigger, HydrateTriggerDetails} from '../defer/interfaces'; import {hoverEventNames, interactionEventNames} from '../../primitives/defer/src/triggers'; import {DEHYDRATED_BLOCK_REGISTRY} from '../defer/registry'; @@ -411,15 +411,23 @@ export function isIncrementalHydrationEnabled(injector: Injector): boolean { }); } +let incrementalHydrationEnabledWarned = false; +export function resetIncrementalHydrationEnabledWarnedForTests() { + incrementalHydrationEnabledWarned = false; +} + /** Throws an error if the incremental hydration is not enabled */ -export function assertIncrementalHydrationIsConfigured(injector: Injector) { - if (!isIncrementalHydrationEnabled(injector)) { - throw new RuntimeError( - RuntimeErrorCode.MISCONFIGURED_INCREMENTAL_HYDRATION, - 'Angular has detected that some `@defer` blocks use `hydrate` triggers, ' + - 'but incremental hydration was not enabled. Please ensure that the `withIncrementalHydration()` ' + - 'call is added as an argument for the `provideClientHydration()` function call ' + - 'in your application config.', +export function warnIncrementalHydrationNotConfigured(): void { + if (!incrementalHydrationEnabledWarned) { + incrementalHydrationEnabledWarned = true; + console.warn( + formatRuntimeError( + RuntimeErrorCode.MISCONFIGURED_INCREMENTAL_HYDRATION, + 'Angular has detected that some `@defer` blocks use `hydrate` triggers, ' + + 'but incremental hydration was not enabled. Please ensure that the `withIncrementalHydration()` ' + + 'call is added as an argument for the `provideClientHydration()` function call ' + + 'in your application config.', + ), ); } } diff --git a/packages/platform-server/test/incremental_hydration_spec.ts b/packages/platform-server/test/incremental_hydration_spec.ts index 8656c91da8b..8ff166fc286 100644 --- a/packages/platform-server/test/incremental_hydration_spec.ts +++ b/packages/platform-server/test/incremental_hydration_spec.ts @@ -24,6 +24,7 @@ import { ɵJSACTION_BLOCK_ELEMENT_MAP as JSACTION_BLOCK_ELEMENT_MAP, ɵJSACTION_EVENT_CONTRACT as JSACTION_EVENT_CONTRACT, ɵgetDocument as getDocument, + ɵresetIncrementalHydrationEnabledWarnedForTests as resetIncrementalHydrationEnabledWarnedForTests, ɵTimerScheduler as TimerScheduler, provideZoneChangeDetection, } from '@angular/core'; @@ -2870,7 +2871,7 @@ describe('platform-server partial hydration integration', () => { }); describe('misconfiguration', () => { - it('should throw an error when `withIncrementalHydration()` is missing in SSR setup', async () => { + it('should log a warning when `withIncrementalHydration()` is missing in SSR setup', async () => { @Component({ selector: 'app', template: ` @@ -2886,17 +2887,15 @@ describe('platform-server partial hydration integration', () => { // Empty list, `withIncrementalHydration()` is not included intentionally. const hydrationFeatures = () => []; + const consoleSpy = spyOn(console, 'warn'); + resetIncrementalHydrationEnabledWarnedForTests(); - let producedError; - try { - await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures}); - } catch (error: unknown) { - producedError = error; - } - expect((producedError as Error).message).toContain('NG0508'); + await ssr(SimpleComponent, {envProviders: providers, hydrationFeatures}); + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledWith(jasmine.stringMatching('NG0508')); }); - it('should throw an error when `withIncrementalHydration()` is missing in hydration setup', async () => { + it('should log a warning when `withIncrementalHydration()` is missing in hydration setup', async () => { @Component({ selector: 'app', template: ` @@ -2919,18 +2918,18 @@ describe('platform-server partial hydration integration', () => { //////////////////////////////// - let producedError; - try { - const doc = getDocument(); - await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, { - envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}], - // Empty list, `withIncrementalHydration()` is not included intentionally. - hydrationFeatures: () => [], - }); - } catch (error: unknown) { - producedError = error; - } - expect((producedError as Error).message).toContain('NG0508'); + const consoleSpy = spyOn(console, 'warn'); + resetIncrementalHydrationEnabledWarnedForTests(); + + const doc = getDocument(); + await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, { + envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}], + // Empty list, `withIncrementalHydration()` is not included intentionally. + hydrationFeatures: () => [], + }); + + expect(consoleSpy).toHaveBeenCalledTimes(1); + expect(consoleSpy).toHaveBeenCalledWith(jasmine.stringMatching('NG0508')); }); }); });