@@ -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,18 @@ 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. ` +
116+ `Note: If you know which element in the DOM this will be inserted, consider adding ngSkipHydration to prevent this error. \n\n` ;
117+ }
118+
104119 throw new RuntimeError ( RuntimeErrorCode . HYDRATION_NODE_MISMATCH , message ) ;
105120 }
106121}
@@ -413,11 +428,32 @@ function getHydrationErrorFooter(componentClassName?: string): string {
413428 `To fix this problem:\n` +
414429 ` * check ${ componentInfo } component for hydration-related issues\n` +
415430 ` * check to see if your template has valid HTML structure\n` +
431+ ` * check if there are any third-party scripts that manipulate the DOM. More info: ${ DOC_PAGE_BASE_URL } ${ THIRD_PARTY_SCRIPTS_URL } \n` +
416432 ` * or skip hydration by adding the \`ngSkipHydration\` attribute ` +
417433 `to its host node in a template\n\n`
418434 ) ;
419435}
420436
437+ /**
438+ * Checks if a given RNode is likely to have been added by a third-party script
439+ * or browser extension, by checking whether Angular has any knowledge of it
440+ * via patched data. Nodes created and managed by Angular will always have
441+ * patched data attached to them.
442+ */
443+ function isLikelyExternalSourceNode ( rNode : RNode ) : boolean {
444+ const node = rNode as Node ;
445+ if ( node . nodeType !== Node . ELEMENT_NODE ) {
446+ return false ;
447+ }
448+ // If Angular has patched this node, it was created within Angular's context.
449+ if ( readPatchedData ( node as HTMLElement ) ) {
450+ return false ;
451+ }
452+ // No patched data means Angular has no record of this node —
453+ // it was likely injected by a third-party script or browser extension.
454+ return true ;
455+ }
456+
421457/**
422458 * An attribute related note for hydration errors
423459 */
0 commit comments