Files
react/packages/react-devtools-shared/src/devtools/utils.js
Ruslan Lesiutin 77ec61885f fix[devtools/inspectElement]: dont pause initial inspectElement call when user switches tabs (#27488)
There are not so many changes, most of them are changing imports,
because I've moved types for UI in a single file.

In https://github.com/facebook/react/pull/27357 I've added support for
pausing polling events: when user inspects an element, we start polling
React DevTools backend for updates in props / state. If user switches
tabs, extension's service worker can be killed by browser and this
polling will start spamming errors.

What I've missed is that we also have a separate call for this API, but
which is executed only once when user selects an element. We don't
handle promise rejection here and this can lead to some errors when user
selects an element and switches tabs right after it.

The only change here is that this API now has
`shouldListenToPauseEvents` param, which is `true` for polling, so we
will pause polling once user switches tabs. It is `false` by default, so
we won't pause initial call by accident.


af8beeebf6/packages/react-devtools-shared/src/backendAPI.js (L96)
2023-10-10 18:10:17 +01:00

215 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 {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.selectedElementIndex === 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);
}
// [url, row, column]
export type Stack = [string, number, number];
const STACK_DELIMETER = /\n\s+at /;
const STACK_SOURCE_LOCATION = /([^\s]+) \((.+):(.+):(.+)\)/;
export function stackToComponentSources(
stack: string,
): Array<[string, ?Stack]> {
const out: Array<[string, ?Stack]> = [];
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, [url, parseInt(row, 10), parseInt(column, 10)]]);
} else {
out.push([entry, null]);
}
});
return out;
}