mirror of
https://github.com/facebook/react.git
synced 2026-02-25 13:13:03 +00:00
282 lines
7.9 KiB
JavaScript
282 lines
7.9 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import JSON5 from 'json5';
|
|
|
|
import type {ReactFunctionLocation} from 'shared/ReactTypes';
|
|
import type {
|
|
Element,
|
|
SuspenseNode,
|
|
} from 'react-devtools-shared/src/frontend/types';
|
|
import type {StateContext} from './views/Components/TreeContext';
|
|
import type Store from './store';
|
|
|
|
export function printElement(
|
|
element: Element,
|
|
includeWeight: boolean = false,
|
|
): string {
|
|
let prefix = ' ';
|
|
if (element.children.length > 0) {
|
|
prefix = element.isCollapsed ? '▸' : '▾';
|
|
}
|
|
|
|
let key = '';
|
|
if (element.key !== null) {
|
|
key = ` key="${element.key}"`;
|
|
}
|
|
|
|
let name = '';
|
|
if (element.nameProp !== null) {
|
|
name = ` name="${element.nameProp}"`;
|
|
}
|
|
|
|
let hocDisplayNames = null;
|
|
if (element.hocDisplayNames !== null) {
|
|
hocDisplayNames = [...element.hocDisplayNames];
|
|
}
|
|
|
|
const hocs =
|
|
hocDisplayNames === null ? '' : ` [${hocDisplayNames.join('][')}]`;
|
|
|
|
let suffix = '';
|
|
if (includeWeight) {
|
|
suffix = ` (${element.isCollapsed ? 1 : element.weight})`;
|
|
}
|
|
|
|
return `${' '.repeat(element.depth + 1)}${prefix} <${
|
|
element.displayName || 'null'
|
|
}${key}${name}>${hocs}${suffix}`;
|
|
}
|
|
|
|
function printRects(rects: SuspenseNode['rects']): string {
|
|
if (rects === null) {
|
|
return ' rects={null}';
|
|
} else {
|
|
return ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`;
|
|
}
|
|
}
|
|
|
|
function printSuspense(suspense: SuspenseNode): string {
|
|
let name = '';
|
|
if (suspense.name !== null) {
|
|
name = ` name="${suspense.name}"`;
|
|
}
|
|
|
|
const printedRects = printRects(suspense.rects);
|
|
|
|
return `<Suspense${name}${printedRects}>`;
|
|
}
|
|
|
|
function printSuspenseWithChildren(
|
|
store: Store,
|
|
suspense: SuspenseNode,
|
|
depth: number,
|
|
): Array<string> {
|
|
const lines = [' '.repeat(depth) + printSuspense(suspense)];
|
|
for (let i = 0; i < suspense.children.length; i++) {
|
|
const childID = suspense.children[i];
|
|
const child = store.getSuspenseByID(childID);
|
|
if (child === null) {
|
|
throw new Error(`Could not find Suspense node with ID "${childID}".`);
|
|
}
|
|
lines.push(...printSuspenseWithChildren(store, child, depth + 1));
|
|
}
|
|
|
|
return lines;
|
|
}
|
|
|
|
export function printOwnersList(
|
|
elements: Array<Element>,
|
|
includeWeight: boolean = false,
|
|
): string {
|
|
return elements
|
|
.map(element => printElement(element, includeWeight))
|
|
.join('\n');
|
|
}
|
|
|
|
export function printStore(
|
|
store: Store,
|
|
includeWeight: boolean = false,
|
|
state: StateContext | null = null,
|
|
includeSuspense: boolean = true,
|
|
): string {
|
|
const snapshotLines = [];
|
|
|
|
let rootWeight = 0;
|
|
|
|
function printSelectedMarker(index: number): string {
|
|
if (state === null) {
|
|
return '';
|
|
}
|
|
return state.inspectedElementIndex === index ? `→` : ' ';
|
|
}
|
|
|
|
function printErrorsAndWarnings(element: Element): string {
|
|
const {errorCount, warningCount} =
|
|
store.getErrorAndWarningCountForElementID(element.id);
|
|
if (errorCount === 0 && warningCount === 0) {
|
|
return '';
|
|
}
|
|
return ` ${errorCount > 0 ? '✕' : ''}${warningCount > 0 ? '⚠' : ''}`;
|
|
}
|
|
|
|
const ownerFlatTree = state !== null ? state.ownerFlatTree : null;
|
|
if (ownerFlatTree !== null) {
|
|
snapshotLines.push(
|
|
'[owners]' + (includeWeight ? ` (${ownerFlatTree.length})` : ''),
|
|
);
|
|
ownerFlatTree.forEach((element, index) => {
|
|
const printedSelectedMarker = printSelectedMarker(index);
|
|
const printedElement = printElement(element, false);
|
|
const printedErrorsAndWarnings = printErrorsAndWarnings(element);
|
|
snapshotLines.push(
|
|
`${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
|
|
);
|
|
});
|
|
} else {
|
|
const errorsAndWarnings = store._errorsAndWarnings;
|
|
if (errorsAndWarnings.size > 0) {
|
|
let errorCount = 0;
|
|
let warningCount = 0;
|
|
errorsAndWarnings.forEach(entry => {
|
|
errorCount += entry.errorCount;
|
|
warningCount += entry.warningCount;
|
|
});
|
|
|
|
snapshotLines.push(`✕ ${errorCount}, ⚠ ${warningCount}`);
|
|
}
|
|
|
|
store.roots.forEach(rootID => {
|
|
const {weight} = ((store.getElementByID(rootID): any): Element);
|
|
const maybeWeightLabel = includeWeight ? ` (${weight})` : '';
|
|
|
|
// Store does not (yet) expose a way to get errors/warnings per root.
|
|
snapshotLines.push(`[root]${maybeWeightLabel}`);
|
|
|
|
for (let i = rootWeight; i < rootWeight + weight; i++) {
|
|
const element = store.getElementAtIndex(i);
|
|
|
|
if (element == null) {
|
|
throw Error(`Could not find element at index "${i}"`);
|
|
}
|
|
|
|
const printedSelectedMarker = printSelectedMarker(i);
|
|
const printedElement = printElement(element, includeWeight);
|
|
const printedErrorsAndWarnings = printErrorsAndWarnings(element);
|
|
snapshotLines.push(
|
|
`${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
|
|
);
|
|
}
|
|
|
|
rootWeight += weight;
|
|
|
|
if (includeSuspense) {
|
|
const root = store.getSuspenseByID(rootID);
|
|
// Roots from legacy renderers don't have a separate Suspense tree
|
|
if (root !== null) {
|
|
if (root.children.length > 0) {
|
|
snapshotLines.push('[suspense-root] ' + printRects(root.rects));
|
|
for (let i = 0; i < root.children.length; i++) {
|
|
const childID = root.children[i];
|
|
const child = store.getSuspenseByID(childID);
|
|
if (child === null) {
|
|
throw new Error(
|
|
`Could not find Suspense node with ID "${childID}".`,
|
|
);
|
|
}
|
|
snapshotLines.push(...printSuspenseWithChildren(store, child, 1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Make sure the pretty-printed test align with the Store's reported number of total rows.
|
|
if (rootWeight !== store.numElements) {
|
|
throw Error(
|
|
`Inconsistent Store state. Individual root weights ("${rootWeight}") do not match total weight ("${store.numElements}")`,
|
|
);
|
|
}
|
|
|
|
// If roots have been unmounted, verify that they've been removed from maps.
|
|
// This helps ensure the Store doesn't leak memory.
|
|
store.assertExpectedRootMapSizes();
|
|
}
|
|
|
|
return snapshotLines.join('\n');
|
|
}
|
|
|
|
// We use JSON.parse to parse string values
|
|
// e.g. 'foo' is not valid JSON but it is a valid string
|
|
// so this method replaces e.g. 'foo' with "foo"
|
|
export function sanitizeForParse(value: any): any | string {
|
|
if (typeof value === 'string') {
|
|
if (
|
|
value.length >= 2 &&
|
|
value.charAt(0) === "'" &&
|
|
value.charAt(value.length - 1) === "'"
|
|
) {
|
|
return '"' + value.slice(1, value.length - 1) + '"';
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function smartParse(value: any): any | void | number {
|
|
switch (value) {
|
|
case 'Infinity':
|
|
return Infinity;
|
|
case 'NaN':
|
|
return NaN;
|
|
case 'undefined':
|
|
return undefined;
|
|
default:
|
|
return JSON5.parse(sanitizeForParse(value));
|
|
}
|
|
}
|
|
|
|
export function smartStringify(value: any): string {
|
|
if (typeof value === 'number') {
|
|
if (Number.isNaN(value)) {
|
|
return 'NaN';
|
|
} else if (!Number.isFinite(value)) {
|
|
return 'Infinity';
|
|
}
|
|
} else if (value === undefined) {
|
|
return 'undefined';
|
|
}
|
|
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
const STACK_DELIMETER = /\n\s+at /;
|
|
const STACK_SOURCE_LOCATION = /([^\s]+) \((.+):(.+):(.+)\)/;
|
|
|
|
export function stackToComponentLocations(
|
|
stack: string,
|
|
): Array<[string, ?ReactFunctionLocation]> {
|
|
const out: Array<[string, ?ReactFunctionLocation]> = [];
|
|
stack
|
|
.split(STACK_DELIMETER)
|
|
.slice(1)
|
|
.forEach(entry => {
|
|
const match = STACK_SOURCE_LOCATION.exec(entry);
|
|
if (match) {
|
|
const [, component, url, row, column] = match;
|
|
out.push([
|
|
component,
|
|
[component, url, parseInt(row, 10), parseInt(column, 10)],
|
|
]);
|
|
} else {
|
|
out.push([entry, null]);
|
|
}
|
|
});
|
|
return out;
|
|
}
|