In RSC and other stacks now we use a lot of `ReactFunctionLocation` type to represent the location of a function. I.e. the location of the beginning of the function (the enclosing line/col) that is represented by the "Source" of the function. This is also what the parent Component Stacks represents. As opposed to `ReactCallSite` which is what normal stack traces and owner stacks represent. I.e. the line/column number of the callsite into the next function. We can start sharing more code by using the `ReactFunctionLocation` type to represent the component source location and it also helps clarify which ones are function locations and which ones are callsites as we start adding more stack traces (e.g. for async debug info and owner stack traces).
216 lines
6.0 KiB
JavaScript
216 lines
6.0 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} 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 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}>${hocs}${suffix}`;
|
|
}
|
|
|
|
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,
|
|
): 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;
|
|
});
|
|
|
|
// 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;
|
|
}
|