import type {Fiber} from './ReactInternalTypes';
import {
HostComponent,
HostHoistable,
HostSingleton,
LazyComponent,
SuspenseComponent,
SuspenseListComponent,
FunctionComponent,
ForwardRef,
SimpleMemoComponent,
ClassComponent,
HostText,
} from './ReactWorkTags';
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
import assign from 'shared/assign';
import getComponentNameFromType from 'shared/getComponentNameFromType';
import isArray from 'shared/isArray';
export type HydrationDiffNode = {
fiber: Fiber,
children: Array<HydrationDiffNode>,
serverProps: void | null | $ReadOnly<{[propName: string]: mixed}> | string,
serverTail: Array<
| $ReadOnly<{type: string, props: $ReadOnly<{[propName: string]: mixed}>}>
| string,
>,
distanceFromLeaf: number,
};
const maxRowLength = 120;
const idealDepth = 15;
function findNotableNode(
node: HydrationDiffNode,
indent: number,
): HydrationDiffNode {
if (
node.serverProps === undefined &&
node.serverTail.length === 0 &&
node.children.length === 1 &&
node.distanceFromLeaf > 3 &&
node.distanceFromLeaf > idealDepth - indent
) {
const child = node.children[0];
return findNotableNode(child, indent);
}
return node;
}
function indentation(indent: number): string {
return ' ' + ' '.repeat(indent);
}
function added(indent: number): string {
return '+ ' + ' '.repeat(indent);
}
function removed(indent: number): string {
return '- ' + ' '.repeat(indent);
}
function describeFiberType(fiber: Fiber): null | string {
switch (fiber.tag) {
case HostHoistable:
case HostSingleton:
case HostComponent:
return fiber.type;
case LazyComponent:
return 'Lazy';
case SuspenseComponent:
return 'Suspense';
case SuspenseListComponent:
return 'SuspenseList';
case FunctionComponent:
case SimpleMemoComponent:
const fn = fiber.type;
return fn.displayName || fn.name || null;
case ForwardRef:
const render = fiber.type.render;
return render.displayName || render.name || null;
case ClassComponent:
const ctr = fiber.type;
return ctr.displayName || ctr.name || null;
default:
return null;
}
}
const needsEscaping = /["'&<>\n\t]|^\s|\s$/;
function describeTextNode(content: string, maxLength: number): string {
if (needsEscaping.test(content)) {
const encoded = JSON.stringify(content);
if (encoded.length > maxLength - 2) {
if (maxLength < 8) {
return '{"..."}';
}
return '{' + encoded.slice(0, maxLength - 7) + '..."}';
}
return '{' + encoded + '}';
} else {
if (content.length > maxLength) {
if (maxLength < 5) {
return '{"..."}';
}
return content.slice(0, maxLength - 3) + '...';
}
return content;
}
}
function describeTextDiff(
clientText: string,
serverProps: mixed,
indent: number,
): string {
const maxLength = maxRowLength - indent * 2;
if (serverProps === null) {
return added(indent) + describeTextNode(clientText, maxLength) + '\n';
} else if (typeof serverProps === 'string') {
let serverText: string = serverProps;
let firstDiff = 0;
for (
;
firstDiff < serverText.length && firstDiff < clientText.length;
firstDiff++
) {
if (
serverText.charCodeAt(firstDiff) !== clientText.charCodeAt(firstDiff)
) {
break;
}
}
if (firstDiff > maxLength - 8 && firstDiff > 10) {
clientText = '...' + clientText.slice(firstDiff - 8);
serverText = '...' + serverText.slice(firstDiff - 8);
}
return (
added(indent) +
describeTextNode(clientText, maxLength) +
'\n' +
removed(indent) +
describeTextNode(serverText, maxLength) +
'\n'
);
} else {
return indentation(indent) + describeTextNode(clientText, maxLength) + '\n';
}
}
function objectName(object: mixed): string {
const name = Object.prototype.toString.call(object);
return name.replace(/^\[object (.*)\]$/, function (m, p0) {
return p0;
});
}
function describeValue(value: mixed, maxLength: number): string {
switch (typeof value) {
case 'string': {
const encoded = JSON.stringify(value);
if (encoded.length > maxLength) {
if (maxLength < 5) {
return '"..."';
}
return encoded.slice(0, maxLength - 4) + '..."';
}
return encoded;
}
case 'object': {
if (value === null) {
return 'null';
}
if (isArray(value)) {
return '[...]';
}
if ((value: any).$$typeof === REACT_ELEMENT_TYPE) {
const type = getComponentNameFromType((value: any).type);
return type ? '<' + type + '>' : '<...>';
}
const name = objectName(value);
if (name === 'Object') {
let properties = '';
maxLength -= 2;
for (let propName in value) {
if (!value.hasOwnProperty(propName)) {
continue;
}
const jsonPropName = JSON.stringify(propName);
if (jsonPropName !== '"' + propName + '"') {
propName = jsonPropName;
}
maxLength -= propName.length - 2;
const propValue = describeValue(
value[propName],
maxLength < 15 ? maxLength : 15,
);
maxLength -= propValue.length;
if (maxLength < 0) {
properties += properties === '' ? '...' : ', ...';
break;
}
properties +=
(properties === '' ? '' : ',') + propName + ':' + propValue;
}
return '{' + properties + '}';
}
return name;
}
case 'function': {
const name = (value: any).displayName || value.name;
return name ? 'function ' + name : 'function';
}
default:
return String(value);
}
}
function describePropValue(value: mixed, maxLength: number): string {
if (typeof value === 'string' && !needsEscaping.test(value)) {
if (value.length > maxLength - 2) {
if (maxLength < 5) {
return '"..."';
}
return '"' + value.slice(0, maxLength - 5) + '..."';
}
return '"' + value + '"';
}
return '{' + describeValue(value, maxLength - 2) + '}';
}
function describeCollapsedElement(
type: string,
props: {[propName: string]: mixed},
indent: number,
): string {
let maxLength = maxRowLength - indent * 2 - type.length - 2;
let content = '';
for (const propName in props) {
if (!props.hasOwnProperty(propName)) {
continue;
}
if (propName === 'children') {
continue;
}
const propValue = describePropValue(props[propName], 15);
maxLength -= propName.length + propValue.length + 2;
if (maxLength < 0) {
content += ' ...';
break;
}
content += ' ' + propName + '=' + propValue;
}
return indentation(indent) + '<' + type + content + '>\n';
}
function describeExpandedElement(
type: string,
props: {+[propName: string]: mixed},
rowPrefix: string,
): string {
let remainingRowLength = maxRowLength - rowPrefix.length - type.length;
const properties = [];
for (const propName in props) {
if (!props.hasOwnProperty(propName)) {
continue;
}
if (propName === 'children') {
continue;
}
const maxLength = maxRowLength - rowPrefix.length - propName.length - 1;
const propValue = describePropValue(props[propName], maxLength);
remainingRowLength -= propName.length + propValue.length + 2;
properties.push(propName + '=' + propValue);
}
if (properties.length === 0) {
return rowPrefix + '<' + type + '>\n';
} else if (remainingRowLength > 0) {
return rowPrefix + '<' + type + ' ' + properties.join(' ') + '>\n';
} else {
return (
rowPrefix +
'<' +
type +
'\n' +
rowPrefix +
' ' +
properties.join('\n' + rowPrefix + ' ') +
'\n' +
rowPrefix +
'>\n'
);
}
}
function describePropertiesDiff(
clientObject: {+[propName: string]: mixed},
serverObject: {+[propName: string]: mixed},
indent: number,
): string {
let properties = '';
const remainingServerProperties = assign({}, serverObject);
for (const propName in clientObject) {
if (!clientObject.hasOwnProperty(propName)) {
continue;
}
delete remainingServerProperties[propName];
const maxLength = maxRowLength - indent * 2 - propName.length - 2;
const clientValue = clientObject[propName];
const clientPropValue = describeValue(clientValue, maxLength);
if (serverObject.hasOwnProperty(propName)) {
const serverValue = serverObject[propName];
const serverPropValue = describeValue(serverValue, maxLength);
properties += added(indent) + propName + ': ' + clientPropValue + '\n';
properties += removed(indent) + propName + ': ' + serverPropValue + '\n';
} else {
properties += added(indent) + propName + ': ' + clientPropValue + '\n';
}
}
for (const propName in remainingServerProperties) {
if (!remainingServerProperties.hasOwnProperty(propName)) {
continue;
}
const maxLength = maxRowLength - indent * 2 - propName.length - 2;
const serverValue = remainingServerProperties[propName];
const serverPropValue = describeValue(serverValue, maxLength);
properties += removed(indent) + propName + ': ' + serverPropValue + '\n';
}
return properties;
}
function describeElementDiff(
type: string,
clientProps: {+[propName: string]: mixed},
serverProps: {+[propName: string]: mixed},
indent: number,
): string {
let content = '';
const serverPropNames: Map<string, string> = new Map();
for (const propName in serverProps) {
if (!serverProps.hasOwnProperty(propName)) {
continue;
}
serverPropNames.set(propName.toLowerCase(), propName);
}
if (serverPropNames.size === 1 && serverPropNames.has('children')) {
content += describeExpandedElement(type, clientProps, indentation(indent));
} else {
for (const propName in clientProps) {
if (!clientProps.hasOwnProperty(propName)) {
continue;
}
if (propName === 'children') {
continue;
}
const maxLength = maxRowLength - (indent + 1) * 2 - propName.length - 1;
const serverPropName = serverPropNames.get(propName.toLowerCase());
if (serverPropName !== undefined) {
serverPropNames.delete(propName.toLowerCase());
const clientValue = clientProps[propName];
const serverValue = serverProps[serverPropName];
const clientPropValue = describePropValue(clientValue, maxLength);
const serverPropValue = describePropValue(serverValue, maxLength);
if (
typeof clientValue === 'object' &&
clientValue !== null &&
typeof serverValue === 'object' &&
serverValue !== null &&
objectName(clientValue) === 'Object' &&
objectName(serverValue) === 'Object' &&
(Object.keys(clientValue).length > 2 ||
Object.keys(serverValue).length > 2 ||
clientPropValue.indexOf('...') > -1 ||
serverPropValue.indexOf('...') > -1)
) {
content +=
indentation(indent + 1) +
propName +
'={{\n' +
describePropertiesDiff(clientValue, serverValue, indent + 2) +
indentation(indent + 1) +
'}}\n';
} else {
content +=
added(indent + 1) + propName + '=' + clientPropValue + '\n';
content +=
removed(indent + 1) + propName + '=' + serverPropValue + '\n';
}
} else {
content +=
indentation(indent + 1) +
propName +
'=' +
describePropValue(clientProps[propName], maxLength) +
'\n';
}
}
serverPropNames.forEach(propName => {
if (propName === 'children') {
return;
}
const maxLength = maxRowLength - (indent + 1) * 2 - propName.length - 1;
content +=
removed(indent + 1) +
propName +
'=' +
describePropValue(serverProps[propName], maxLength) +
'\n';
});
if (content === '') {
content = indentation(indent) + '<' + type + '>\n';
} else {
content =
indentation(indent) +
'<' +
type +
'\n' +
content +
indentation(indent) +
'>\n';
}
}
const serverChildren = serverProps.children;
const clientChildren = clientProps.children;
if (
typeof serverChildren === 'string' ||
typeof serverChildren === 'number' ||
typeof serverChildren === 'bigint'
) {
const serverText = '' + serverChildren;
let clientText = '';
if (
typeof clientChildren === 'string' ||
typeof clientChildren === 'number' ||
typeof clientChildren === 'bigint'
) {
clientText = '' + clientChildren;
}
content += describeTextDiff(clientText, serverText, indent + 1);
} else if (
typeof clientChildren === 'string' ||
typeof clientChildren === 'number' ||
typeof clientChildren === 'bigint'
) {
if (serverChildren == null) {
content += describeTextDiff('' + clientChildren, null, indent + 1);
} else {
content += describeTextDiff('' + clientChildren, undefined, indent + 1);
}
}
return content;
}
function describeSiblingFiber(fiber: Fiber, indent: number): string {
const type = describeFiberType(fiber);
if (type === null) {
let flatContent = '';
let childFiber = fiber.child;
while (childFiber) {
flatContent += describeSiblingFiber(childFiber, indent);
childFiber = childFiber.sibling;
}
return flatContent;
}
return indentation(indent) + '<' + type + '>' + '\n';
}
function describeNode(node: HydrationDiffNode, indent: number): string {
const skipToNode = findNotableNode(node, indent);
if (
skipToNode !== node &&
(node.children.length !== 1 || node.children[0] !== skipToNode)
) {
return indentation(indent) + '...\n' + describeNode(skipToNode, indent + 1);
}
let parentContent = '';
const debugInfo = node.fiber._debugInfo;
if (debugInfo) {
for (let i = 0; i < debugInfo.length; i++) {
const serverComponentName = debugInfo[i].name;
if (typeof serverComponentName === 'string') {
parentContent +=
indentation(indent) + '<' + serverComponentName + '>' + '\n';
indent++;
}
}
}
let selfContent = '';
const clientProps = node.fiber.pendingProps;
if (node.fiber.tag === HostText) {
selfContent = describeTextDiff(clientProps, node.serverProps, indent);
indent++;
} else {
const type = describeFiberType(node.fiber);
if (type !== null) {
if (node.serverProps === undefined) {
selfContent = describeCollapsedElement(type, clientProps, indent);
indent++;
} else if (node.serverProps === null) {
selfContent = describeExpandedElement(type, clientProps, added(indent));
indent++;
} else if (typeof node.serverProps === 'string') {
if (__DEV__) {
console.error(
'Should not have matched a non HostText fiber to a Text node. This is a bug in React.',
);
}
} else {
selfContent = describeElementDiff(
type,
clientProps,
node.serverProps,
indent,
);
indent++;
}
}
}
let childContent = '';
let childFiber = node.fiber.child;
let diffIdx = 0;
while (childFiber && diffIdx < node.children.length) {
const childNode = node.children[diffIdx];
if (childNode.fiber === childFiber) {
childContent += describeNode(childNode, indent);
diffIdx++;
} else {
childContent += describeSiblingFiber(childFiber, indent);
}
childFiber = childFiber.sibling;
}
if (childFiber && node.children.length > 0) {
childContent += indentation(indent) + '...' + '\n';
}
const serverTail = node.serverTail;
if (node.serverProps === null) {
indent--;
}
for (let i = 0; i < serverTail.length; i++) {
const tailNode = serverTail[i];
if (typeof tailNode === 'string') {
childContent +=
removed(indent) +
describeTextNode(tailNode, maxRowLength - indent * 2) +
'\n';
} else {
childContent += describeExpandedElement(
tailNode.type,
tailNode.props,
removed(indent),
);
}
}
return parentContent + selfContent + childContent;
}
export function describeDiff(rootNode: HydrationDiffNode): string {
try {
return '\n\n' + describeNode(rootNode, 0);
} catch (x) {
return '';
}
}