import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import type {HydrationDiffNode} from 'react-reconciler/src/ReactFiberHydrationDiffs';
import {
current,
runWithFiberInDEV,
} from 'react-reconciler/src/ReactCurrentFiber';
import {
HostComponent,
HostHoistable,
HostSingleton,
HostText,
} from 'react-reconciler/src/ReactWorkTags';
import {describeDiff} from 'react-reconciler/src/ReactFiberHydrationDiffs';
function describeAncestors(
ancestor: Fiber,
child: Fiber,
props: null | {children: null},
): string {
let fiber: null | Fiber = child;
let node: null | HydrationDiffNode = null;
let distanceFromLeaf = 0;
while (fiber) {
if (fiber === ancestor) {
distanceFromLeaf = 0;
}
node = {
fiber: fiber,
children: node !== null ? [node] : [],
serverProps:
fiber === child ? props : fiber === ancestor ? null : undefined,
serverTail: [],
distanceFromLeaf: distanceFromLeaf,
};
distanceFromLeaf++;
fiber = fiber.return;
}
if (node !== null) {
return describeDiff(node).replaceAll(/^[+-]/gm, '>');
}
return '';
}
type Info = {tag: string};
export type AncestorInfoDev = {
current: ?Info,
formTag: ?Info,
aTagInScope: ?Info,
buttonTagInScope: ?Info,
nobrTagInScope: ?Info,
pTagInButtonScope: ?Info,
listItemTagAutoclosing: ?Info,
dlItemTagAutoclosing: ?Info,
containerTagInScope: ?Info,
implicitRootScope: boolean,
};
const specialTags = [
'address',
'applet',
'area',
'article',
'aside',
'base',
'basefont',
'bgsound',
'blockquote',
'body',
'br',
'button',
'caption',
'center',
'col',
'colgroup',
'dd',
'details',
'dir',
'div',
'dl',
'dt',
'embed',
'fieldset',
'figcaption',
'figure',
'footer',
'form',
'frame',
'frameset',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'head',
'header',
'hgroup',
'hr',
'html',
'iframe',
'img',
'input',
'isindex',
'li',
'link',
'listing',
'main',
'marquee',
'menu',
'menuitem',
'meta',
'nav',
'noembed',
'noframes',
'noscript',
'object',
'ol',
'p',
'param',
'plaintext',
'pre',
'script',
'section',
'select',
'source',
'style',
'summary',
'table',
'tbody',
'td',
'template',
'textarea',
'tfoot',
'th',
'thead',
'title',
'tr',
'track',
'ul',
'wbr',
'xmp',
];
const inScopeTags = [
'applet',
'caption',
'html',
'table',
'td',
'th',
'marquee',
'object',
'template',
'foreignObject',
'desc',
'title',
];
const buttonScopeTags = __DEV__ ? inScopeTags.concat(['button']) : [];
const impliedEndTags = [
'dd',
'dt',
'li',
'option',
'optgroup',
'p',
'rp',
'rt',
];
const emptyAncestorInfoDev: AncestorInfoDev = {
current: null,
formTag: null,
aTagInScope: null,
buttonTagInScope: null,
nobrTagInScope: null,
pTagInButtonScope: null,
listItemTagAutoclosing: null,
dlItemTagAutoclosing: null,
containerTagInScope: null,
implicitRootScope: false,
};
function updatedAncestorInfoDev(
oldInfo: null | AncestorInfoDev,
tag: string,
): AncestorInfoDev {
if (__DEV__) {
const ancestorInfo = {...(oldInfo || emptyAncestorInfoDev)};
const info = {tag};
if (inScopeTags.indexOf(tag) !== -1) {
ancestorInfo.aTagInScope = null;
ancestorInfo.buttonTagInScope = null;
ancestorInfo.nobrTagInScope = null;
}
if (buttonScopeTags.indexOf(tag) !== -1) {
ancestorInfo.pTagInButtonScope = null;
}
if (
specialTags.indexOf(tag) !== -1 &&
tag !== 'address' &&
tag !== 'div' &&
tag !== 'p'
) {
ancestorInfo.listItemTagAutoclosing = null;
ancestorInfo.dlItemTagAutoclosing = null;
}
ancestorInfo.current = info;
if (tag === 'form') {
ancestorInfo.formTag = info;
}
if (tag === 'a') {
ancestorInfo.aTagInScope = info;
}
if (tag === 'button') {
ancestorInfo.buttonTagInScope = info;
}
if (tag === 'nobr') {
ancestorInfo.nobrTagInScope = info;
}
if (tag === 'p') {
ancestorInfo.pTagInButtonScope = info;
}
if (tag === 'li') {
ancestorInfo.listItemTagAutoclosing = info;
}
if (tag === 'dd' || tag === 'dt') {
ancestorInfo.dlItemTagAutoclosing = info;
}
if (tag === '#document' || tag === 'html') {
ancestorInfo.containerTagInScope = null;
} else if (!ancestorInfo.containerTagInScope) {
ancestorInfo.containerTagInScope = info;
}
if (
oldInfo === null &&
(tag === '#document' || tag === 'html' || tag === 'body')
) {
ancestorInfo.implicitRootScope = true;
} else if (ancestorInfo.implicitRootScope === true) {
ancestorInfo.implicitRootScope = false;
}
return ancestorInfo;
} else {
return (null: any);
}
}
function isTagValidWithParent(
tag: string,
parentTag: ?string,
implicitRootScope: boolean,
): boolean {
switch (parentTag) {
case 'select':
return (
tag === 'hr' ||
tag === 'option' ||
tag === 'optgroup' ||
tag === 'script' ||
tag === 'template' ||
tag === '#text'
);
case 'optgroup':
return tag === 'option' || tag === '#text';
case 'option':
return tag === '#text';
case 'tr':
return (
tag === 'th' ||
tag === 'td' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
case 'tbody':
case 'thead':
case 'tfoot':
return (
tag === 'tr' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
case 'colgroup':
return tag === 'col' || tag === 'template';
case 'table':
return (
tag === 'caption' ||
tag === 'colgroup' ||
tag === 'tbody' ||
tag === 'tfoot' ||
tag === 'thead' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
case 'head':
return (
tag === 'base' ||
tag === 'basefont' ||
tag === 'bgsound' ||
tag === 'link' ||
tag === 'meta' ||
tag === 'title' ||
tag === 'noscript' ||
tag === 'noframes' ||
tag === 'style' ||
tag === 'script' ||
tag === 'template'
);
case 'html':
if (implicitRootScope) {
break;
}
return tag === 'head' || tag === 'body' || tag === 'frameset';
case 'frameset':
return tag === 'frame';
case '#document':
if (implicitRootScope) {
break;
}
return tag === 'html';
}
switch (tag) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return (
parentTag !== 'h1' &&
parentTag !== 'h2' &&
parentTag !== 'h3' &&
parentTag !== 'h4' &&
parentTag !== 'h5' &&
parentTag !== 'h6'
);
case 'rp':
case 'rt':
return impliedEndTags.indexOf(parentTag) === -1;
case 'caption':
case 'col':
case 'colgroup':
case 'frameset':
case 'frame':
case 'tbody':
case 'td':
case 'tfoot':
case 'th':
case 'thead':
case 'tr':
return parentTag == null;
case 'head':
return implicitRootScope || parentTag === null;
case 'html':
return (
(implicitRootScope && parentTag === '#document') || parentTag === null
);
case 'body':
return (
(implicitRootScope &&
(parentTag === '#document' || parentTag === 'html')) ||
parentTag === null
);
}
return true;
}
function findInvalidAncestorForTag(
tag: string,
ancestorInfo: AncestorInfoDev,
): ?Info {
switch (tag) {
case 'address':
case 'article':
case 'aside':
case 'blockquote':
case 'center':
case 'details':
case 'dialog':
case 'dir':
case 'div':
case 'dl':
case 'fieldset':
case 'figcaption':
case 'figure':
case 'footer':
case 'header':
case 'hgroup':
case 'main':
case 'menu':
case 'nav':
case 'ol':
case 'p':
case 'section':
case 'summary':
case 'ul':
case 'pre':
case 'listing':
case 'table':
case 'hr':
case 'xmp':
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
return ancestorInfo.pTagInButtonScope;
case 'form':
return ancestorInfo.formTag || ancestorInfo.pTagInButtonScope;
case 'li':
return ancestorInfo.listItemTagAutoclosing;
case 'dd':
case 'dt':
return ancestorInfo.dlItemTagAutoclosing;
case 'button':
return ancestorInfo.buttonTagInScope;
case 'a':
return ancestorInfo.aTagInScope;
case 'nobr':
return ancestorInfo.nobrTagInScope;
}
return null;
}
const didWarn: {[string]: boolean} = {};
function findAncestor(parent: null | Fiber, tagName: string): null | Fiber {
while (parent) {
switch (parent.tag) {
case HostComponent:
case HostHoistable:
case HostSingleton:
if (parent.type === tagName) {
return parent;
}
}
parent = parent.return;
}
return null;
}
function validateDOMNesting(
childTag: string,
ancestorInfo: AncestorInfoDev,
): boolean {
if (__DEV__) {
ancestorInfo = ancestorInfo || emptyAncestorInfoDev;
const parentInfo = ancestorInfo.current;
const parentTag = parentInfo && parentInfo.tag;
const invalidParent = isTagValidWithParent(
childTag,
parentTag,
ancestorInfo.implicitRootScope,
)
? null
: parentInfo;
const invalidAncestor = invalidParent
? null
: findInvalidAncestorForTag(childTag, ancestorInfo);
const invalidParentOrAncestor = invalidParent || invalidAncestor;
if (!invalidParentOrAncestor) {
return true;
}
const ancestorTag = invalidParentOrAncestor.tag;
const warnKey =
String(!!invalidParent) + '|' + childTag + '|' + ancestorTag;
if (didWarn[warnKey]) {
return false;
}
didWarn[warnKey] = true;
const child = current;
const ancestor = child ? findAncestor(child.return, ancestorTag) : null;
const ancestorDescription =
child !== null && ancestor !== null
? describeAncestors(ancestor, child, null)
: '';
const tagDisplayName = '<' + childTag + '>';
if (invalidParent) {
let info = '';
if (ancestorTag === 'table' && childTag === 'tr') {
info +=
' Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by ' +
'the browser.';
}
console.error(
'In HTML, %s cannot be a child of <%s>.%s\n' +
'This will cause a hydration error.%s',
tagDisplayName,
ancestorTag,
info,
ancestorDescription,
);
} else {
console.error(
'In HTML, %s cannot be a descendant of <%s>.\n' +
'This will cause a hydration error.%s',
tagDisplayName,
ancestorTag,
ancestorDescription,
);
}
if (child) {
const parent = child.return;
if (
ancestor !== null &&
parent !== null &&
(ancestor !== parent || parent._debugOwner !== child._debugOwner)
) {
runWithFiberInDEV(ancestor, () => {
console.error(
'<%s> cannot contain a nested %s.\n' +
'See this log for the ancestor stack trace.',
ancestorTag,
tagDisplayName,
);
});
}
}
return false;
}
return true;
}
function validateTextNesting(
childText: string,
parentTag: string,
implicitRootScope: boolean,
): boolean {
if (__DEV__) {
if (implicitRootScope || isTagValidWithParent('#text', parentTag, false)) {
return true;
}
const warnKey = '#text|' + parentTag;
if (didWarn[warnKey]) {
return false;
}
didWarn[warnKey] = true;
const child = current;
const ancestor = child ? findAncestor(child, parentTag) : null;
const ancestorDescription =
child !== null && ancestor !== null
? describeAncestors(
ancestor,
child,
child.tag !== HostText ? {children: null} : null,
)
: '';
if (/\S/.test(childText)) {
console.error(
'In HTML, text nodes cannot be a child of <%s>.\n' +
'This will cause a hydration error.%s',
parentTag,
ancestorDescription,
);
} else {
console.error(
'In HTML, whitespace text nodes cannot be a child of <%s>. ' +
"Make sure you don't have any extra whitespace between tags on " +
'each line of your source code.\n' +
'This will cause a hydration error.%s',
parentTag,
ancestorDescription,
);
}
return false;
}
return true;
}
export {updatedAncestorInfoDev, validateDOMNesting, validateTextNesting};