diff --git a/goldens/public-api/platform-browser/index.api.md b/goldens/public-api/platform-browser/index.api.md index 088c3e45ce4..4c74537c658 100644 --- a/goldens/public-api/platform-browser/index.api.md +++ b/goldens/public-api/platform-browser/index.api.md @@ -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; +// @public +export function withNoIncrementalHydration(): HydrationFeature; + // (No @packageDocumentation comment for this package) ``` diff --git a/integration/platform-server-hydration/size.json b/integration/platform-server-hydration/size.json index c08ba357b77..82a21b1fbab 100644 --- a/integration/platform-server-hydration/size.json +++ b/integration/platform-server-hydration/size.json @@ -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 } diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index bb167dbfba0..6ae425532d1 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -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", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index f73d43b60c5..b4602f936b0 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -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" } } } diff --git a/packages/core/schematics/migrations/incremental-hydration/BUILD.bazel b/packages/core/schematics/migrations/incremental-hydration/BUILD.bazel new file mode 100644 index 00000000000..a81deb25071 --- /dev/null +++ b/packages/core/schematics/migrations/incremental-hydration/BUILD.bazel @@ -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", + ], +) diff --git a/packages/core/schematics/migrations/incremental-hydration/index.ts b/packages/core/schematics/migrations/incremental-hydration/index.ts new file mode 100644 index 00000000000..b3aeee5a8ab --- /dev/null +++ b/packages/core/schematics/migrations/incremental-hydration/index.ts @@ -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(), + }); + }; +} diff --git a/packages/core/schematics/migrations/incremental-hydration/migration.spec.ts b/packages/core/schematics/migrations/incremental-hydration/migration.spec.ts new file mode 100644 index 00000000000..9c65bfff83e --- /dev/null +++ b/packages/core/schematics/migrations/incremental-hydration/migration.spec.ts @@ -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()'); + }); +}); diff --git a/packages/core/schematics/migrations/incremental-hydration/migration.ts b/packages/core/schematics/migrations/incremental-hydration/migration.ts new file mode 100644 index 00000000000..326fa3d8721 --- /dev/null +++ b/packages/core/schematics/migrations/incremental-hydration/migration.ts @@ -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> { + 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> { + return confirmAsSerializable({ + replacements: [...unitA.replacements, ...unitB.replacements], + }); + } + + override async globalMeta( + combinedData: IncrementalHydrationMigrationData, + ): Promise> { + return confirmAsSerializable(combinedData); + } + + override async stats( + globalMetadata: IncrementalHydrationMigrationData, + ): Promise> { + return confirmAsSerializable({}); + } + + override async migrate( + globalData: IncrementalHydrationMigrationData, + ): Promise<{replacements: Replacement[]}> { + return {replacements: globalData.replacements}; + } +} diff --git a/packages/core/test/bundling/hydration/bundle.golden_symbols.json b/packages/core/test/bundling/hydration/bundle.golden_symbols.json index 781fd99e5f1..f72dfa68985 100644 --- a/packages/core/test/bundling/hydration/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hydration/bundle.golden_symbols.json @@ -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" diff --git a/packages/platform-browser/src/hydration.ts b/packages/platform-browser/src/hydration.ts index 9b7c6a03ca1..d1f727419e5 100644 --- a/packages/platform-browser/src/hydration.ts +++ b/packages/platform-browser/src/hydration.ts @@ -38,6 +38,7 @@ export enum HydrationFeatureKind { I18nSupport, EventReplay, IncrementalHydration, + NoIncrementalHydration, } /** @@ -144,6 +145,16 @@ export function withIncrementalHydration(): HydrationFeature { + 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, ]); } diff --git a/packages/platform-browser/src/platform-browser.ts b/packages/platform-browser/src/platform-browser.ts index cd4328cb713..8e3b27de3f5 100644 --- a/packages/platform-browser/src/platform-browser.ts +++ b/packages/platform-browser/src/platform-browser.ts @@ -29,6 +29,7 @@ export { withHttpTransferCacheOptions, withI18nSupport, withIncrementalHydration, + withNoIncrementalHydration, withNoHttpTransferCache, } from './hydration'; export { diff --git a/packages/platform-browser/test/hydration_spec.ts b/packages/platform-browser/test/hydration_spec.ts index b97e2f24d5c..22696ba38ed 100644 --- a/packages/platform-browser/test/hydration_spec.ts +++ b/packages/platform-browser/test/hydration_spec.ts @@ -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/); + }); + }); }); diff --git a/packages/platform-server/test/event_replay_spec.ts b/packages/platform-server/test/event_replay_spec.ts index 7e5db2bd95f..fcf1ad46094 100644 --- a/packages/platform-server/test/event_replay_spec.ts +++ b/packages/platform-server/test/event_replay_spec.ts @@ -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 diff --git a/packages/platform-server/test/full_app_hydration_spec.ts b/packages/platform-server/test/full_app_hydration_spec.ts index 0e89828b3b9..5dc576d25f4 100644 --- a/packages/platform-server/test/full_app_hydration_spec.ts +++ b/packages/platform-server/test/full_app_hydration_spec.ts @@ -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', () => { diff --git a/packages/platform-server/test/incremental_hydration_spec.ts b/packages/platform-server/test/incremental_hydration_spec.ts index 9cd0bb7de18..cce69e36e4c 100644 --- a/packages/platform-server/test/incremental_hydration_spec.ts +++ b/packages/platform-server/test/incremental_hydration_spec.ts @@ -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);