feat(platform-browser): make incremental hydration default behavior

This commit updates provideClientHydration to automatically enable incremental hydration by default. It also introduces a new withNoIncrementalHydration feature for opting out, adds conflict safety checks, and includes a schematic migration.
This commit is contained in:
Jessica Janiuk 2026-04-08 12:15:53 -07:00 committed by Kirill Cherkashin
parent 47fcbc4704
commit 68628dd45b
15 changed files with 522 additions and 20 deletions

View file

@ -116,7 +116,9 @@ export enum HydrationFeatureKind {
// (undocumented)
IncrementalHydration = 4,
// (undocumented)
NoHttpTransferCache = 0
NoHttpTransferCache = 0,
// (undocumented)
NoIncrementalHydration = 5
}
// @public
@ -215,6 +217,9 @@ export function withIncrementalHydration(): HydrationFeature<HydrationFeatureKin
// @public
export function withNoHttpTransferCache(): HydrationFeature<HydrationFeatureKind.NoHttpTransferCache>;
// @public
export function withNoIncrementalHydration(): HydrationFeature<HydrationFeatureKind.NoIncrementalHydration>;
// (No @packageDocumentation comment for this package)
```

View file

@ -1,5 +1,5 @@
{
"dist/browser/main-[hash].js": 232126,
"dist/browser/main-[hash].js": 240329,
"dist/browser/polyfills-[hash].js": 35726,
"dist/browser/event-dispatch-contract.min.js": 476
}

View file

@ -133,6 +133,10 @@ bundle_entrypoints = [
"can-match-snapshot-required",
"packages/core/schematics/migrations/can-match-snapshot-required/index.js",
],
[
"incremental-hydration",
"packages/core/schematics/migrations/incremental-hydration/index.js",
],
]
rollup.rollup(
@ -147,6 +151,7 @@ rollup.rollup(
"//packages/core/schematics/migrations/can-match-snapshot-required",
"//packages/core/schematics/migrations/change-detection-eager",
"//packages/core/schematics/migrations/http-xhr-backend",
"//packages/core/schematics/migrations/incremental-hydration",
"//packages/core/schematics/migrations/strict-template",
"//packages/core/schematics/ng-generate/cleanup-unused-imports",
"//packages/core/schematics/ng-generate/common-to-standalone-migration",

View file

@ -19,6 +19,11 @@
"version": "22.0.0",
"description": "Adds the required third argument to canMatch callsites.",
"factory": "./bundles/can-match-snapshot-required.cjs#migrate"
},
"incremental-hydration": {
"version": "22.0.0",
"description": "Adds withNoIncrementalHydration() opt out to provideClientHydration() when incremental hydration is not enabled to retain pre-v22 behavior-.",
"factory": "./bundles/incremental-hydration.cjs#migrate"
}
}
}

View file

@ -0,0 +1,27 @@
load("//tools:defaults.bzl", "ts_project")
package(
default_visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
)
ts_project(
name = "incremental-hydration",
srcs = glob(
["**/*.ts"],
exclude = ["*.spec.ts"],
),
deps = [
"//:node_modules/@angular-devkit/schematics",
"//:node_modules/@types/node",
"//:node_modules/typescript",
"//packages/compiler-cli",
"//packages/compiler-cli/private",
"//packages/core/schematics/utils",
"//packages/core/schematics/utils/tsurge",
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
"//packages/core/schematics/utils/tsurge/helpers/ast",
],
)

View file

@ -0,0 +1,20 @@
/**
* @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 {Rule} from '@angular-devkit/schematics';
import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
import {IncrementalHydrationMigration} from './migration';
export function migrate(): Rule {
return async (tree, context) => {
await runMigrationInDevkit({
tree,
getMigration: (fs) => new IncrementalHydrationMigration(),
});
};
}

View file

@ -0,0 +1,54 @@
/**
* @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 {absoluteFrom} from '@angular/compiler-cli';
import {initMockFileSystem} from '@angular/compiler-cli/private/testing';
import {runTsurgeMigration} from '../../utils/tsurge/testing';
import {IncrementalHydrationMigration} from './migration';
describe('IncrementalHydration migration', () => {
beforeEach(() => {
initMockFileSystem('Native');
});
it('should not change anything if withIncrementalHydration() is present', async () => {
const {fs} = await runTsurgeMigration(new IncrementalHydrationMigration(), [
{
name: absoluteFrom('/index.ts'),
isProgramRootFile: true,
contents: `
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';
provideClientHydration(withIncrementalHydration());
`,
},
]);
const content = fs.readFile(absoluteFrom('/index.ts'));
expect(content).not.toContain('withNoIncrementalHydration()');
expect(content).toContain('withIncrementalHydration()');
expect(content).toContain('provideClientHydration()');
});
it('should add withNoIncrementalHydration() if withIncrementalHydration is absent', async () => {
const {fs} = await runTsurgeMigration(new IncrementalHydrationMigration(), [
{
name: absoluteFrom('/index.ts'),
isProgramRootFile: true,
contents: `
import { provideClientHydration } from '@angular/platform-browser';
provideClientHydration();
`,
},
]);
const content = fs.readFile(absoluteFrom('/index.ts'));
expect(content).toContain('withNoIncrementalHydration()');
});
});

View file

@ -0,0 +1,123 @@
/**
* @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 {ImportManager} from '@angular/compiler-cli/private/migrations';
import ts from 'typescript';
import {
confirmAsSerializable,
ProgramInfo,
projectFile,
Replacement,
Serializable,
TextUpdate,
TsurgeFunnelMigration,
} from '../../utils/tsurge';
import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import_manager';
export interface IncrementalHydrationMigrationData {
replacements: Replacement[];
}
export class IncrementalHydrationMigration extends TsurgeFunnelMigration<
IncrementalHydrationMigrationData,
IncrementalHydrationMigrationData
> {
override async analyze(
info: ProgramInfo,
): Promise<Serializable<IncrementalHydrationMigrationData>> {
const {sourceFiles, program} = info;
const typeChecker = program.getTypeChecker();
const replacements: Replacement[] = [];
const importManager = new ImportManager();
const printer = ts.createPrinter();
for (const sf of sourceFiles) {
ts.forEachChild(sf, function visit(node: ts.Node) {
if (
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === 'provideClientHydration'
) {
let hasIncremental = false;
let incrementalArgNode: ts.CallExpression | null = null;
for (const arg of node.arguments) {
if (
ts.isCallExpression(arg) &&
ts.isIdentifier(arg.expression) &&
arg.expression.text === 'withIncrementalHydration'
) {
hasIncremental = true;
incrementalArgNode = arg;
break;
}
}
if (!hasIncremental) {
// Add withNoIncrementalHydration()
const withNoIncrementalExpr = importManager.addImport({
exportModuleSpecifier: '@angular/platform-browser',
exportSymbolName: 'withNoIncrementalHydration',
requestedFile: sf,
});
const exprText = printer.printNode(ts.EmitHint.Unspecified, withNoIncrementalExpr, sf);
const insertPos = node.arguments.end;
const toInsert = node.arguments.length > 0 ? `, ${exprText}()` : `${exprText}()`;
replacements.push(
new Replacement(
projectFile(sf, info),
new TextUpdate({
position: insertPos,
end: insertPos,
toInsert: toInsert,
}),
),
);
}
}
ts.forEachChild(node, visit);
});
}
applyImportManagerChanges(importManager, replacements, sourceFiles, info);
return confirmAsSerializable({
replacements,
});
}
override async combine(
unitA: IncrementalHydrationMigrationData,
unitB: IncrementalHydrationMigrationData,
): Promise<Serializable<IncrementalHydrationMigrationData>> {
return confirmAsSerializable({
replacements: [...unitA.replacements, ...unitB.replacements],
});
}
override async globalMeta(
combinedData: IncrementalHydrationMigrationData,
): Promise<Serializable<IncrementalHydrationMigrationData>> {
return confirmAsSerializable(combinedData);
}
override async stats(
globalMetadata: IncrementalHydrationMigrationData,
): Promise<Serializable<unknown>> {
return confirmAsSerializable({});
}
override async migrate(
globalData: IncrementalHydrationMigrationData,
): Promise<{replacements: Replacement[]}> {
return {replacements: globalData.replacements};
}
}

View file

@ -2,6 +2,7 @@
"chunks": {
"main": [
"ACCEPT_HEADER_VALUE",
"AFTER_RENDER_PHASES",
"AFTER_RENDER_SEQUENCES_TO_ADD",
"ALLOWED_METHODS",
"ANIMATIONS",
@ -11,20 +12,26 @@
"APP_ID",
"APP_ID_ATTRIBUTE_NAME",
"APP_INITIALIZER",
"ActionResolver",
"AfterRenderImpl",
"AfterRenderManager",
"AfterRenderSequence",
"AnonymousSubject",
"ApplicationInitStatus",
"ApplicationRef",
"Attribute",
"BINDING",
"BLOOM_BUCKET_BITS",
"BLOOM_MASK",
"BLOOM_SIZE",
"BODY",
"BROWSER_MODULE_PROVIDERS",
"BUBBLE_EVENT_TYPES",
"BehaviorSubject",
"BrowserDomAdapter",
"BrowserXhr",
"CACHE_OPTIONS",
"CAPTURE_EVENT_TYPES",
"CHILD_HEAD",
"CHILD_TAIL",
"CIRCULAR",
@ -35,15 +42,18 @@
"COMPLETE_NOTIFICATION",
"COMPONENT_REGEX",
"COMPONENT_VARIABLE",
"COMPOSED_PATH_ERROR_MESSAGE",
"CONTAINERS",
"CONTAINER_HEADER_OFFSET",
"CONTENT_ATTR",
"CONTEXT",
"CSP_NONCE",
"CachedInjectorService",
"ChainedInjector",
"ChangeDetectionScheduler",
"ChangeDetectionSchedulerImpl",
"ChangeDetectionStrategy",
"Char",
"ComponentFactory",
"ComponentFactory2",
"ComponentFactoryResolver",
@ -56,22 +66,38 @@
"DECLARATION_LCONTAINER",
"DECLARATION_VIEW",
"DEFAULT_APP_ID",
"DEFAULT_EVENT_TYPE",
"DEFAULT_LOCALE_ID",
"DEFER_BLOCK_CONFIG",
"DEFER_BLOCK_ID",
"DEFER_BLOCK_SSR_ID_ATTRIBUTE",
"DEFER_BLOCK_STATE",
"DEFER_BLOCK_STATE",
"DEFER_HYDRATE_TRIGGERS",
"DEFER_PARENT_BLOCK_ID",
"DEHYDRATED_BLOCK_REGISTRY",
"DEHYDRATED_VIEWS",
"DISCONNECTED_NODES",
"DI_DECORATOR_FLAG",
"DOCUMENT",
"DOCUMENT2",
"DefaultDomRenderer2",
"DeferBlockBehavior",
"DeferBlockState",
"DeferDependenciesLoadingState",
"DeferEventEntry",
"DehydratedBlockRegistry",
"DestroyRef",
"Dispatcher",
"DomAdapter",
"DomEventsPlugin",
"DomRendererFactory2",
"EAGER_CONTENT_LISTENERS_KEY",
"EFFECTS",
"EFFECTS_TO_SCHEDULE",
"ELEMENT_CONTAINERS",
"EMBEDDED_VIEW_INJECTOR",
"EMPTY_ACTION_MAP",
"EMPTY_ARRAY",
"EMPTY_OBJ",
"EMPTY_OBSERVER",
@ -81,6 +107,8 @@
"ENVIRONMENT",
"ENVIRONMENT_INITIALIZER",
"EVENT_MANAGER_PLUGINS",
"EVENT_REPLAY_ENABLED_DEFAULT",
"EVENT_REPLAY_QUEUE",
"EffectRefImpl",
"EffectScheduler",
"ElementRef",
@ -88,10 +116,16 @@
"EnvironmentInjector",
"EnvironmentNgModuleRefAdapter",
"ErrorHandler",
"EventContract",
"EventContractContainer",
"EventDispatcher",
"EventEmitter",
"EventEmitter_",
"EventInfoWrapper",
"EventManager",
"EventManagerPlugin",
"EventPhase",
"EventType",
"FLAGS",
"HEADERS",
"HEADER_OFFSET",
@ -99,6 +133,7 @@
"HOST_ATTR",
"HTTP_ROOT_INTERCEPTOR_FNS",
"HTTP_TRANSFER_CACHE_ORIGIN_MAP",
"HYDRATE_TRIGGER_CLEANUP_FNS",
"HYDRATION",
"HelloWorld",
"HttpEventType",
@ -107,33 +142,45 @@
"HttpResponseBase",
"HydrationFeatureKind",
"ID",
"IDLE_SERVICE",
"INJECTOR",
"INJECTOR",
"INJECTOR_DEF_TYPES",
"INJECTOR_SCOPE",
"INTERNAL_APPLICATION_ERROR_HANDLER",
"INTERNAL_BROWSER_PLATFORM_PROVIDERS",
"IS_EVENT_REPLAY_ENABLED",
"IS_HYDRATION_DOM_REUSE_ENABLED",
"IS_INCREMENTAL_HYDRATION_ENABLED",
"IdleScheduler",
"InjectionToken",
"Injector",
"InputFlags",
"JSACTION_BLOCK_ELEMENT_MAP",
"JSACTION_EVENT_CONTRACT",
"JSON_CONTENT_TYPE",
"KeyEventsPlugin",
"LOADING_AFTER_SLOT",
"LOCALE_ID",
"LOCALE_ID",
"MATH_ML_NAMESPACE",
"MAXIMUM_REFRESH_RERUNS",
"MAXIMUM_REFRESH_RERUNS",
"MINIMUM_SLOT",
"MODIFIER_KEYS",
"MODIFIER_KEY_GETTERS",
"MONKEY_PATCH_KEY_NAME",
"MOUSE_SPECIAL_EVENT_TYPES",
"MOUSE_SPECIAL_SUPPORT",
"MOVED_VIEWS",
"MULTIPLIER",
"NAMESPACE_URIS",
"NATIVE",
"NEXT",
"NEXT_DEFER_BLOCK_STATE",
"NGH_ATTR_NAME",
"NGH_DATA_KEY",
"NGH_DEFER_BLOCKS_KEY",
"NG_COMP_DEF",
"NG_DIR_DEF",
"NG_ELEMENT_ID",
@ -166,6 +213,7 @@
"NoneEncapsulationDomRenderer",
"NoopNgZone",
"NullInjector",
"ON_COMPLETE_FNS",
"ON_DESTROY_HOOKS",
"ObjectUnsubscribedError",
"Observable",
@ -175,11 +223,16 @@
"PLATFORM_DESTROY_LISTENERS",
"PLATFORM_ID",
"PLATFORM_INITIALIZER",
"PREFETCH_TRIGGER_CLEANUP_FNS",
"PREORDER_HOOK_FLAGS",
"PRESERVE_HOST_CONTENT",
"PRESERVE_HOST_CONTENT_DEFAULT",
"PREVENT_DEFAULT_ERROR_MESSAGE",
"PROPAGATION_STOPPED_SYMBOL",
"PendingTasks",
"PendingTasksInternal",
"ProfilerEvent",
"Property",
"QUERIES",
"R3Injector",
"REACTIVE_LVIEW_CONSUMER_NODE",
@ -188,6 +241,7 @@
"REFERENCE_NODE_BODY",
"REFERENCE_NODE_HOST",
"REF_EXTRACTOR_REGEXP",
"REGEXP_SEMICOLON",
"REMOVE_STYLES_ON_COMPONENT_DESTROY",
"REMOVE_STYLES_ON_COMPONENT_DESTROY_DEFAULT",
"RENDERER",
@ -195,6 +249,8 @@
"RESPONSE_TYPE",
"RendererFactory2",
"RendererStyleFlags2",
"RequestIdleCallbackService",
"Restriction",
"RetrievingInjector",
"RuntimeError",
"SCHEDULE_IN_ROOT_ZONE",
@ -203,6 +259,7 @@
"SIMPLE_CHANGES_STORE",
"SKIP_HYDRATION_ATTR_NAME",
"SKIP_HYDRATION_ATTR_NAME_LOWER_CASE",
"SSR_BLOCK_STATE",
"SSR_CONTENT_INTEGRITY_MARKER",
"STABILITY_WARNING_THRESHOLD",
"STATUS",
@ -224,16 +281,20 @@
"TEXT_CONTENT_TYPE",
"THROW_IF_NOT_FOUND",
"TRACKED_LVIEWS",
"TRANSFER_STATE_DEFER_BLOCKS_INFO",
"TRANSFER_STATE_TOKEN_ID",
"TRIGGER_CLEANUP_FNS",
"TVIEW",
"TYPE",
"T_HOST",
"TimerScheduler",
"TracingAction",
"TracingService",
"TransferState",
"USE_VALUE",
"UnsubscriptionError",
"VIEW_REFS",
"ViewContext",
"ViewEncapsulation",
"ViewRef",
"XhrFactory",
@ -267,6 +328,7 @@
"_applyRootElementTransformImpl",
"_bind",
"_callAndReportToErrorHandler",
"_cancelIdleCallback",
"_currentInjector",
"_document",
"_findAndReconcileMatchingDehydratedViewsImpl",
@ -285,20 +347,34 @@
"_platformInjector",
"_populateDehydratedViewsInLContainer",
"_processI18nInsertBefore",
"_requestIdleCallback",
"_retrieveDeferBlockDataImpl",
"_retrieveHydrationInfoImpl",
"_stashEventListenerImpl",
"_wasLastNodeCreated",
"acceptNode",
"activeConsumer",
"addAfterRenderSequencesForView",
"addDepsToRegistry",
"addEventListener",
"addLViewToLContainer",
"addServerStyles",
"addToAnimationQueue",
"addToArray",
"addToEndOfViewTree",
"addViewToDOM",
"afterEveryRenderImpl",
"afterNextRender",
"aggregateDescendantAnimations",
"allLeavingAnimations",
"allocExpando",
"allocLFrame",
"angularZoneInstanceIdProperty",
"appendChild",
"appendDeferBlocksToJSActionMap",
"applyContainer",
"applyDeferBlockState",
"applyDeferBlockStateWithSchedulingImpl",
"applyNodes",
"applyProjectionRecursive",
"applyRootElementTransform",
@ -306,8 +382,11 @@
"applyToElementOrContainer",
"applyValueToInputField",
"applyView",
"appsWithEventReplay",
"areAnimationSupported",
"arrRemove",
"arrayInsert2",
"arraySplice",
"assertNotDestroyed",
"assertTypeDefined",
"attachPatchData",
@ -331,11 +410,16 @@
"cleanUpView",
"cleanupDehydratedIcuData",
"cleanupDehydratedViews",
"cleanupHydratedDeferBlocks",
"cleanupI18nHydrationData",
"cleanupLContainer",
"cleanupLView",
"cleanupMatchingDehydratedViews",
"cleanupParentContainer",
"cleanupRemainingHydrationQueue",
"clearAppScopedEarlyEventContract",
"clearElementContents",
"cloneEventInfo",
"collectAllViewLeaveAnimations",
"collectNativeNodes",
"collectNativeNodesInLContainer",
@ -354,27 +438,34 @@
"convertToInjectOptions",
"couldBeInjectableType",
"createAnchorNode",
"createAndRenderEmbeddedLView",
"createBlockSummary",
"createCommentNode",
"createComponentLView",
"createContainerAnchorImpl",
"createDeferBlockInjector",
"createDirectivesInstances",
"createElementNode",
"createElementRef",
"createEnvironmentInjector",
"createErrorClass",
"createEventInfoFromParameters",
"createHostElement",
"createInjector",
"createInjectorWithoutInjectorInstances",
"createIntersectionObserver",
"createInvalidObservableTypeError",
"createLFrame",
"createLView",
"createLinkElement",
"createMouseSpecialEvent",
"createNodeInjector",
"createNotification",
"createOperatorSubscriber",
"createOrReusePlatformInjector",
"createPlatformInjector",
"createProvidersConfig",
"createReplayQueuedBlockEventsFn",
"createRootLViewEnvironment",
"createRootTView",
"createRootViewInjector",
@ -391,6 +482,7 @@
"deepForEachProvider",
"defaultErrorHandler",
"defaultThrowError",
"deferBlockHasErrored",
"delayChangeDetectionForEvents",
"destroyLView",
"destroyViewTree",
@ -410,12 +502,15 @@
"enableApplyRootElementTransformImpl",
"enableFindMatchingDehydratedViewImpl",
"enableHydrationRuntimeSupport",
"enableIncrementalHydrationRuntimeSupport",
"enableLocateOrCreateContainerAnchorImpl",
"enableLocateOrCreateContainerRefImpl",
"enableLocateOrCreateElementContainerNodeImpl",
"enableLocateOrCreateElementNodeImpl",
"enableLocateOrCreateTextNodeImpl",
"enableRetrieveDeferBlockDataImpl",
"enableRetrieveHydrationInfoImpl",
"enableStashEventListenerImpl",
"enterDI",
"enterSkipHydrationBlock",
"enterView",
@ -440,6 +535,7 @@
"finalizeConsumerAfterComputation",
"findAndReconcileMatchingDehydratedViewsImpl",
"findMatchingDehydratedView",
"findMatchingDehydratedViewForDeferBlock",
"findMatchingDehydratedViewImpl",
"forEachSingleProvider",
"forkInnerZoneWithAngularBehavior",
@ -455,9 +551,18 @@
"fromIterable",
"fromPromise",
"fromReadableStreamLike",
"gatherDeferBlocksByJSActionAttribute",
"gatherDeferBlocksCommentNodes",
"generateHash",
"get",
"getAction",
"getActionElement",
"getActiveConsumer",
"getAppScopedQueuedEventInfos",
"getBaseElementHref",
"getBeforeNodeForView",
"getBrowserEventType",
"getCleanupFnKeyByType",
"getClosestRElement",
"getClosureSafeProperty",
"getComponentDef",
@ -465,6 +570,7 @@
"getComponentLViewByIndex",
"getComponentName",
"getConstant",
"getContainer",
"getCurrentDirectiveIndex",
"getCurrentInjector",
"getCurrentParentTNode",
@ -472,12 +578,20 @@
"getCurrentTNodePlaceholderOk",
"getDOM",
"getDeclarationTNode",
"getDeferBlockDataIndex",
"getDirectiveDef",
"getDocument",
"getEvent",
"getEventType",
"getFactoryDef",
"getFirstLContainer",
"getFirstNativeNode",
"getGlobalLocale",
"getHeadersToInclude",
"getHooks",
"getHydrateTimerTrigger",
"getHydrateViewportTrigger",
"getIdleRequestKey",
"getInheritedInjectableDef",
"getInitialLViewFlagsFromDef",
"getInjectFlag",
@ -487,9 +601,14 @@
"getInjectorIndex",
"getInsertInFrontOfRNode",
"getInsertInFrontOfRNodeWithNoI18n",
"getIntersectionObserverKey",
"getIsReplay",
"getLDeferBlockDetails",
"getLNodeForHydration",
"getLView",
"getLViewParent",
"getLoadingBlockAfter",
"getMinimumDurationForState",
"getNamespace",
"getNativeByTNode",
"getNearestLContainer",
@ -501,19 +620,25 @@
"getNullInjector",
"getOrBorrowReactiveLViewConsumer",
"getOrCreateComponentTView",
"getOrCreateEnvironmentInjector",
"getOrCreateInjectable",
"getOrCreateNodeInjectorForNode",
"getOrCreateTNode",
"getOrCreateTemporaryConsumer",
"getOwnDefinition",
"getParentBlockHydrationQueue",
"getParentInjectorIndex",
"getParentInjectorLocation",
"getParentInjectorView",
"getParentInjectorViewOffset",
"getParentRElement",
"getParsed",
"getPipeDef",
"getPrimaryBlockTNode",
"getProjectionNodes",
"getPromiseCtor",
"getQueuedEventInfos",
"getResolved",
"getRootTViewTemplate",
"getRuntimeErrorCode",
"getSegmentHead",
@ -521,17 +646,23 @@
"getSerializedContainerViews",
"getSimpleChangesStore",
"getSymbolIterator",
"getTDeferBlockDetails",
"getTNode",
"getTNodeFromLView",
"getTView",
"getTargetElement",
"getTemplateIndexForState",
"getTextNodeContent",
"getTimestamp",
"getUndecoratedInjectableFactory",
"getUniqueLViewId",
"handleStoppedNotification",
"handleUncaughtError",
"handleUnhandledError",
"hasApplyArgsData",
"hasAuthHeaders",
"hasDeps",
"hasHydrateTrigger",
"hasInSkipHydrationBlockFlag",
"hasLift",
"hasMatchingDehydratedView",
@ -539,6 +670,8 @@
"hasParentInjector",
"hasSkipHydrationAttrOnRElement",
"hasSkipHydrationAttrOnTNode",
"hoverEventNames",
"hydrateAndInvokeBlockListeners",
"icuContainerIterate",
"identity",
"importProvidersFrom",
@ -547,6 +680,7 @@
"inferTagNameFromDefinition",
"initDisconnectedNodes",
"initDomAdapter",
"initEventReplay",
"initFeatures",
"initTNodeFlags",
"initializeDirectives",
@ -557,22 +691,31 @@
"injectElementRef",
"injectInjectorOnly",
"injectRootLimpMode",
"injectViewContext",
"injectableDefOrInjectorDefFactory",
"innerFrom",
"insertAnchorNode",
"insertBloom",
"insertView",
"instantiateAllDirectives",
"instructionState",
"interactionEventNames",
"internalCreateApplication",
"internalImportProvidersFrom",
"intersectionObservers",
"invokeAllTriggerCleanupFns",
"invokeDirectivesHostBindings",
"invokeHostBindingsInCreationMode",
"invokeListeners",
"invokeRegisteredReplayListeners",
"invokeTriggerCleanupFns",
"isAngularZoneProperty",
"isAnimationProp",
"isApplicationBootstrapConfig",
"isArrayLike",
"isAsyncIterable",
"isBoundToModule",
"isCaptureEventType",
"isComponentDef",
"isComponentHost",
"isContentQueryHost",
@ -591,12 +734,18 @@
"isInInjectionContext",
"isInSkipHydrationBlock",
"isInSkipHydrationBlock2",
"isIncrementalHydrationEnabled",
"isIncrementalHydrationRuntimeSupportEnabled",
"isInlineTemplate",
"isInputBinding",
"isInteropObservable",
"isIterable",
"isLContainer",
"isLView",
"isMac",
"isMiddleClick",
"isModifiedClickEvent",
"isMouseSpecialEvent",
"isNotFound",
"isObserver",
"isPositive",
@ -608,11 +757,14 @@
"isScheduler",
"isSchedulerTick",
"isSsrContentsIntegrity",
"isStashEventListenerImplEnabled",
"isSubscribable",
"isSubscriber",
"isSubscription",
"isTemplateNode",
"isTimerTrigger",
"isTypeProvider",
"isValidStateChange",
"isValueProvider",
"iterator",
"last",
@ -660,6 +812,7 @@
"navigateToNode",
"nextNgElementId",
"nextNotification",
"nextRender",
"ngOnChangesSetInput",
"ngZoneInstanceId",
"noSideEffects",
@ -670,17 +823,31 @@
"observable",
"observeOn",
"of",
"onDeferBlockCompletion",
"onEnter",
"onIdle",
"onLeave",
"onTimer",
"onViewport",
"onViewportWrapper",
"operate",
"parseAndConvertInputsForDefinition",
"parseAndConvertOutputsForDefinition",
"parseCache",
"parseCookieValue",
"patchEventInstance",
"performanceMarkFeature",
"pipeFromArray",
"popScheduler",
"populateDehydratedViewsInLContainerImpl",
"populateHydratingStateForQueue",
"prepareEventForBubbling",
"prepareEventForDispatch",
"prepareEventForReplay",
"preventDefault",
"process",
"processAndInitTriggers",
"processBlockData",
"processCleanups",
"processHostBindingOpCodes",
"processInjectorTypesWithProviders",
@ -691,6 +858,8 @@
"profiler",
"profilerCallbacks",
"projectNodes",
"promiseWithResolvers",
"propagationStopped",
"provideClientHydration",
"provideZonelessChangeDetectionInternal",
"providerToFactory",
@ -700,6 +869,7 @@
"readableStreamLikeToAsyncGenerator",
"refreshContentQueries",
"refreshView",
"registerDispatcher",
"registerHostBindingOpCodes",
"registerLView",
"registerPostOrderHooks",
@ -707,16 +877,25 @@
"relativePath",
"rememberChangeHistoryAndInvokeOnChangesHook",
"remove",
"removeAllEventListeners",
"removeAnimationsFromQueue",
"removeDehydratedView",
"removeDehydratedViewList",
"removeDehydratedViews",
"removeElements",
"removeEventListener",
"removeEventListeners",
"removeFromArray",
"removeLViewFromLContainer",
"removeLViewOnDestroy",
"removeListeners",
"removeListenersFromBlocks",
"removeStaleDehydratedBranch",
"removeViewFromDOM",
"renderChildComponents",
"renderComponent",
"renderDeferBlockState",
"renderDeferStateAfterResourceLoading",
"renderView",
"reportUnhandledError",
"requiresRefreshOrTraversal",
@ -724,6 +903,8 @@
"resetPreOrderHookFlags",
"resolveDirectives",
"resolveForwardRef",
"retrieveDeferBlockData",
"retrieveDeferBlockDataImpl",
"retrieveHydrationInfo",
"retrieveHydrationInfoImpl",
"retrieveStateFromCache",
@ -743,44 +924,67 @@
"scheduleObservable",
"schedulePromise",
"scheduleReadableStreamLike",
"scheduleTimerTrigger",
"scheduled",
"searchTokensOnInjector",
"selectIndexInternal",
"set",
"setAction",
"setActiveConsumer",
"setAllInputsForProperty",
"setBindingIndex",
"setBindingRootForHostBindings",
"setContainer",
"setCurrentDirectiveIndex",
"setCurrentInjector",
"setCurrentQueryIndex",
"setCurrentTNode",
"setDocument",
"setEvent",
"setEventType",
"setIdleTriggers",
"setImmediateTriggers",
"setIncludeViewProviders",
"setInjectImplementation",
"setInputsFromAttrs",
"setIsI18nHydrationSupportEnabled",
"setIsRefreshingViews",
"setIsReplay",
"setLocaleId",
"setParsed",
"setResolved",
"setRootDomAdapter",
"setSegmentHead",
"setSelectedIndex",
"setShadowStylingInputFlags",
"setSimpleChangesStore",
"setStashFn",
"setTargetElement",
"setThrowInvalidWriteToSignalError",
"setTimerTriggers",
"setTimestamp",
"setUpAttributes",
"setViewportTriggers",
"setupHostDirectiveInputsOrOutputs",
"setupInitialInputs",
"setupSelectorMatchedInputsOrOutputs",
"setupStaticAttributes",
"sharedMapFunction",
"sharedStashFunction",
"shimContentAttribute",
"shimHostAttribute",
"shimStylesContent",
"shouldAddViewToDom",
"shouldBeIgnoredByZone",
"shouldCacheRequest",
"shouldEnableEventReplay",
"shouldPreventDefaultBeforeDispatching",
"shouldSearchParent",
"shouldTriggerDeferBlock",
"siblingAfter",
"skipTextNodes",
"sortAndConcatParams",
"stashEventListeners",
"storeLViewOnDestroy",
"stringifyCSSSelector",
"stringifyCSSSelectorList",
@ -794,19 +998,28 @@
"trackLeavingNodes",
"trackMovedView",
"transferCacheInterceptorFn",
"triggerDeferBlock",
"triggerHydrationForBlockQueue",
"triggerHydrationFromBlockName",
"triggerResourceLoading",
"triggerResourceLoadingForHydration",
"uniqueIdCounter",
"unregisterLView",
"unsetAction",
"unwrapRNode",
"updateAncestorTraversalFlagsOnAttach",
"updateMicroTaskStatus",
"verifySsrContentsIntegrity",
"viewAttachedToChangeDetector",
"viewShouldHaveReactiveConsumer",
"viewportTriggers",
"walkProviderTree",
"wasLastNodeCreated",
"whenStableWithTimeout",
"withDomHydration",
"withEventReplay",
"withHttpTransferCache",
"withIncrementalHydration",
"writeDirectClass",
"writeDirectStyle",
"writeToDirectiveInput"

View file

@ -38,6 +38,7 @@ export enum HydrationFeatureKind {
I18nSupport,
EventReplay,
IncrementalHydration,
NoIncrementalHydration,
}
/**
@ -144,6 +145,16 @@ export function withIncrementalHydration(): HydrationFeature<HydrationFeatureKin
return hydrationFeature(HydrationFeatureKind.IncrementalHydration, ɵwithIncrementalHydration());
}
/**
* Disables support for incremental hydration (which is enabled by default).
*
* @publicApi 22.0
* @see {@link provideClientHydration}
*/
export function withNoIncrementalHydration(): HydrationFeature<HydrationFeatureKind.NoIncrementalHydration> {
return hydrationFeature(HydrationFeatureKind.NoIncrementalHydration);
}
/**
* Returns an `ENVIRONMENT_INITIALIZER` token setup with a function
* that verifies whether enabledBlocking initial navigation is used in an application
@ -240,16 +251,22 @@ export function provideClientHydration(
HydrationFeatureKind.HttpTransferCacheOptions,
);
if (
typeof ngDevMode !== 'undefined' &&
ngDevMode &&
featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) &&
hasHttpTransferCacheOptions
) {
throw new RuntimeError(
RuntimeErrorCode.HYDRATION_CONFLICTING_FEATURES,
'Configuration error: found both withHttpTransferCacheOptions() and withNoHttpTransferCache() in the same call to provideClientHydration(), which is a contradiction.',
);
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
if (featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) && hasHttpTransferCacheOptions) {
throw new RuntimeError(
RuntimeErrorCode.HYDRATION_CONFLICTING_FEATURES,
'Configuration error: found both withHttpTransferCacheOptions() and withNoHttpTransferCache() in the same call to provideClientHydration(), which is a contradiction.',
);
}
if (
featuresKind.has(HydrationFeatureKind.IncrementalHydration) &&
featuresKind.has(HydrationFeatureKind.NoIncrementalHydration)
) {
throw new RuntimeError(
RuntimeErrorCode.HYDRATION_CONFLICTING_FEATURES,
'Configuration error: found both withIncrementalHydration() and withNoIncrementalHydration() in the same call to provideClientHydration(), which is a contradiction.',
);
}
}
return makeEnvironmentProviders([
@ -261,6 +278,9 @@ export function provideClientHydration(
featuresKind.has(HydrationFeatureKind.NoHttpTransferCache) || hasHttpTransferCacheOptions
? []
: ɵwithHttpTransferCache({}),
featuresKind.has(HydrationFeatureKind.NoIncrementalHydration)
? []
: ɵwithIncrementalHydration(),
providers,
]);
}

View file

@ -29,6 +29,7 @@ export {
withHttpTransferCacheOptions,
withI18nSupport,
withIncrementalHydration,
withNoIncrementalHydration,
withNoHttpTransferCache,
} from './hydration';
export {

View file

@ -20,7 +20,12 @@ import {TestBed} from '@angular/core/testing';
import {withBody} from '@angular/private/testing';
import {BehaviorSubject} from 'rxjs';
import {provideClientHydration, withNoHttpTransferCache} from '../public_api';
import {
provideClientHydration,
withNoHttpTransferCache,
withIncrementalHydration,
withNoIncrementalHydration,
} from '../public_api';
import {withHttpTransferCacheOptions} from '../src/hydration';
describe('provideClientHydration', () => {
@ -164,4 +169,12 @@ describe('provideClientHydration', () => {
TestBed.inject(HttpTestingController).expectNone(url);
});
});
describe('incremental hydration conflicts', () => {
it('should throw when both withIncrementalHydration and withNoIncrementalHydration are provided', () => {
expect(() => {
provideClientHydration(withIncrementalHydration(), withNoIncrementalHydration());
}).toThrowError(/Configuration error: found both withIncrementalHydration/);
});
});
});

View file

@ -24,6 +24,7 @@ import {
bootstrapApplication,
provideClientHydration,
withEventReplay,
withNoIncrementalHydration,
} from '@angular/platform-browser';
import {EventPhase} from '@angular/core/primitives/event-dispatch';
@ -662,7 +663,9 @@ describe('event replay', () => {
onClick() {}
}
const html = await ssr(SimpleComponent, {});
const html = await ssr(SimpleComponent, {
hydrationFeatures: () => [withNoIncrementalHydration()],
});
const ssrContents = getAppContents(html);
// Expect that there are no JSAction artifacts in the HTML

View file

@ -93,12 +93,24 @@ import {
} from './hydration_utils';
describe('platform-server full application hydration integration', () => {
const originalWindow = globalThis.window;
beforeAll(async () => {
globalThis.window = globalThis as unknown as Window & typeof globalThis;
await import('../../core/primitives/event-dispatch/contract_bundle_min.js' as string);
});
afterAll(() => {
globalThis.window = originalWindow;
});
beforeEach(() => {
resetNgDevModeCounters();
});
afterEach(() => {
destroyPlatform();
window._ejsas = {};
});
describe('hydration', () => {

View file

@ -41,6 +41,7 @@ import {
provideClientHydration,
withEventReplay,
withIncrementalHydration,
withNoIncrementalHydration,
} from '@angular/platform-browser';
import {provideRouter, RouterLink, RouterOutlet, Routes} from '@angular/router';
import {getAppContents, prepareEnvironmentAndHydrate, resetTViewsFor} from './dom_utils';
@ -2998,7 +2999,7 @@ describe('platform-server partial hydration integration', () => {
});
describe('misconfiguration', () => {
it('should log a warning when `withIncrementalHydration()` is missing in SSR setup', async () => {
it('should log a warning when incremental hydration is disabled in SSR setup', async () => {
@Component({
selector: 'app',
template: `
@ -3012,8 +3013,8 @@ describe('platform-server partial hydration integration', () => {
const appId = 'custom-app-id';
const providers = [{provide: APP_ID, useValue: appId}];
// Empty list, `withIncrementalHydration()` is not included intentionally.
const hydrationFeatures = () => [];
// Explicitly disabled using withNoIncrementalHydration()
const hydrationFeatures = () => [withNoIncrementalHydration()];
const consoleSpy = spyOn(console, 'warn');
resetIncrementalHydrationEnabledWarnedForTests();
@ -3022,7 +3023,7 @@ describe('platform-server partial hydration integration', () => {
expect(consoleSpy).toHaveBeenCalledWith(jasmine.stringMatching('NG0508'));
});
it('should log a warning when `withIncrementalHydration()` is missing in hydration setup', async () => {
it('should log a warning when incremental hydration is disabled in hydration setup', async () => {
@Component({
selector: 'app',
template: `
@ -3051,8 +3052,8 @@ describe('platform-server partial hydration integration', () => {
const doc = getDocument();
await prepareEnvironmentAndHydrate(doc, html, SimpleComponent, {
envProviders: [...providers, {provide: PLATFORM_ID, useValue: 'browser'}],
// Empty list, `withIncrementalHydration()` is not included intentionally.
hydrationFeatures: () => [],
// Explicitly disabled using withNoIncrementalHydration()
hydrationFeatures: () => [withNoIncrementalHydration()],
});
expect(consoleSpy).toHaveBeenCalledTimes(1);