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
This commit is contained in:
Matthieu Riegler
2025-10-01 23:00:05 +02:00
committed by Andrew Kushnir
parent e941e6b80f
commit dd2f53b9cd
4 changed files with 47 additions and 34 deletions
+1
View File
@@ -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,
+9 -4
View File
@@ -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);
+17 -9
View File
@@ -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.',
),
);
}
}
@@ -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'));
});
});
});