@@ -14,10 +14,14 @@ import {HOST, LView, TVIEW} from '../render3/interfaces/view';
1414import { getParentRElement } from '../render3/node_manipulation' ;
1515import { unwrapRNode } from '../render3/util/view_utils' ;
1616
17+ import { readPatchedData } from '../render3/context_discovery' ;
1718import { markRNodeAsHavingHydrationMismatch } from './utils' ;
19+ import { DOC_PAGE_BASE_URL } from '../../../core/src/error_details_base_url' ;
1820
1921const AT_THIS_LOCATION = '<-- AT THIS LOCATION' ;
2022
23+ const THIRD_PARTY_SCRIPTS_URL = `/guide/hydration#third-party-scripts-with-dom-manipulation` ;
24+
2125/**
2226 * Retrieves a user friendly string for a given TNodeType for use in
2327 * friendly error messages
@@ -100,7 +104,17 @@ export function validateMatchingNode(
100104 }
101105
102106 const footer = getHydrationErrorFooter ( componentClassName ) ;
103- const message = header + expected + actual + getHydrationAttributeNote ( ) + footer ;
107+ let message = header + expected + actual + getHydrationAttributeNote ( ) + footer ;
108+
109+ // Check both when a mismatching node is found AND when the expected node is missing,
110+ // since third-party scripts can both inject extra nodes and remove existing ones.
111+ if ( ! node || ( node && isLikelyExternalSourceNode ( node ) ) ) {
112+ message +=
113+ `Note: It looks like this mismatch may have been caused by a third-party script or ` +
114+ `browser extension that modified the DOM outside of Angular's control. ` +
115+ `Angular hydration does not support nodes injected or removed outside of the Angular-managed DOM. \n\n` ;
116+ }
117+
104118 throw new RuntimeError ( RuntimeErrorCode . HYDRATION_NODE_MISMATCH , message ) ;
105119 }
106120}
@@ -413,11 +427,32 @@ function getHydrationErrorFooter(componentClassName?: string): string {
413427 `To fix this problem:\n` +
414428 ` * check ${ componentInfo } component for hydration-related issues\n` +
415429 ` * check to see if your template has valid HTML structure\n` +
430+ ` * check if there are any third-party scripts that manipulate the DOM. More info: ${ DOC_PAGE_BASE_URL } ${ THIRD_PARTY_SCRIPTS_URL } \n` +
416431 ` * or skip hydration by adding the \`ngSkipHydration\` attribute ` +
417432 `to its host node in a template\n\n`
418433 ) ;
419434}
420435
436+ /**
437+ * Checks if a given RNode is likely to have been added by a third-party script
438+ * or browser extension, by checking whether Angular has any knowledge of it
439+ * via patched data. Nodes created and managed by Angular will always have
440+ * patched data attached to them.
441+ */
442+ function isLikelyExternalSourceNode ( rNode : RNode ) : boolean {
443+ const node = rNode as Node ;
444+ if ( node . nodeType !== Node . ELEMENT_NODE ) {
445+ return false ;
446+ }
447+ // If Angular has patched this node, it was created within Angular's context.
448+ if ( readPatchedData ( node as HTMLElement ) ) {
449+ return false ;
450+ }
451+ // No patched data means Angular has no record of this node —
452+ // it was likely injected by a third-party script or browser extension.
453+ return true ;
454+ }
455+
421456/**
422457 * An attribute related note for hydration errors
423458 */
0 commit comments