mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
4a58b63865
This collects the ReactAsyncInfo between instances. It associates it
with the parent. Typically this would be a Server Component's Promise
return value but it can also be Promises in a fragment. It can also be
associated with a client component when you pass a Promise into the
child position e.g. `<div>{promise}</div>` then it's associated with the
div. If an instance is filtered, then it gets associated with the parent
of that's unfiltered.
The stack trace currently isn't source mapped. I'll do that in a follow
up.
We also need to add a "short name" from the Promise for the description
(e.g. url). I'll also add a little marker showing the relative time span
of each entry.
<img width="447" height="591" alt="Screenshot 2025-07-26 at 7 56 00 PM"
src="https://github.com/user-attachments/assets/7c966540-7b1b-4568-8cb9-f25cefd5a918"
/>
<img width="446" height="570" alt="Screenshot 2025-07-26 at 7 55 23 PM"
src="https://github.com/user-attachments/assets/4eac235b-e735-41e8-9c6e-a7633af64e4b"
/>
201 lines
5.0 KiB
JavaScript
201 lines
5.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 escapeStringRegExp from 'escape-string-regexp';
|
|
import {meta} from '../../hydration';
|
|
import {formatDataForPreview} from '../../utils';
|
|
import isArray from 'react-devtools-shared/src/isArray';
|
|
|
|
import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';
|
|
|
|
// $FlowFixMe[method-unbinding]
|
|
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
|
|
export function alphaSortEntries(
|
|
entryA: [string, mixed],
|
|
entryB: [string, mixed],
|
|
): number {
|
|
const a = entryA[0];
|
|
const b = entryB[0];
|
|
if (String(+a) === a) {
|
|
if (String(+b) !== b) {
|
|
return -1;
|
|
}
|
|
return +a < +b ? -1 : 1;
|
|
}
|
|
return a < b ? -1 : 1;
|
|
}
|
|
|
|
export function createRegExp(string: string): RegExp {
|
|
// Allow /regex/ syntax with optional last /
|
|
if (string[0] === '/') {
|
|
// Cut off first slash
|
|
string = string.slice(1);
|
|
// Cut off last slash, but only if it's there
|
|
if (string[string.length - 1] === '/') {
|
|
string = string.slice(0, string.length - 1);
|
|
}
|
|
try {
|
|
return new RegExp(string, 'i');
|
|
} catch (err) {
|
|
// Bad regex. Make it not match anything.
|
|
// TODO: maybe warn in console?
|
|
return new RegExp('.^');
|
|
}
|
|
}
|
|
|
|
function isLetter(char: string) {
|
|
return char.toLowerCase() !== char.toUpperCase();
|
|
}
|
|
|
|
function matchAnyCase(char: string) {
|
|
if (!isLetter(char)) {
|
|
// Don't mess with special characters like [.
|
|
return char;
|
|
}
|
|
return '[' + char.toLowerCase() + char.toUpperCase() + ']';
|
|
}
|
|
|
|
// 'item' should match 'Item' and 'ListItem', but not 'InviteMom'.
|
|
// To do this, we'll slice off 'tem' and check first letter separately.
|
|
const escaped = escapeStringRegExp(string);
|
|
const firstChar = escaped[0];
|
|
let restRegex = '';
|
|
// For 'item' input, restRegex becomes '[tT][eE][mM]'
|
|
// We can't simply make it case-insensitive because first letter case matters.
|
|
for (let i = 1; i < escaped.length; i++) {
|
|
restRegex += matchAnyCase(escaped[i]);
|
|
}
|
|
|
|
if (!isLetter(firstChar)) {
|
|
// We can't put a non-character like [ in a group
|
|
// so we fall back to the simple case.
|
|
return new RegExp(firstChar + restRegex);
|
|
}
|
|
|
|
// Construct a smarter regex.
|
|
return new RegExp(
|
|
// For example:
|
|
// (^[iI]|I)[tT][eE][mM]
|
|
// Matches:
|
|
// 'Item'
|
|
// 'ListItem'
|
|
// but not 'InviteMom'
|
|
'(^' +
|
|
matchAnyCase(firstChar) +
|
|
'|' +
|
|
firstChar.toUpperCase() +
|
|
')' +
|
|
restRegex,
|
|
);
|
|
}
|
|
|
|
export function getMetaValueLabel(data: Object): string | null {
|
|
if (hasOwnProperty.call(data, meta.preview_long)) {
|
|
return data[meta.preview_long];
|
|
} else {
|
|
return formatDataForPreview(data, true);
|
|
}
|
|
}
|
|
|
|
function sanitize(data: Object): void {
|
|
for (const key in data) {
|
|
const value = data[key];
|
|
|
|
if (value && value[meta.type]) {
|
|
data[key] = getMetaValueLabel(value);
|
|
} else if (value != null) {
|
|
if (isArray(value)) {
|
|
sanitize(value);
|
|
} else if (typeof value === 'object') {
|
|
sanitize(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function serializeDataForCopy(props: Object): string {
|
|
const cloned = isArray(props) ? props.slice(0) : Object.assign({}, props);
|
|
|
|
sanitize(cloned);
|
|
|
|
try {
|
|
return JSON.stringify(cloned, null, 2);
|
|
} catch (error) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
export function serializeHooksForCopy(hooks: HooksTree | null): string {
|
|
// $FlowFixMe[not-an-object] "HooksTree is not an object"
|
|
const cloned = Object.assign(([]: Array<any>), hooks);
|
|
|
|
const queue = [...cloned];
|
|
|
|
while (queue.length > 0) {
|
|
const current = queue.pop();
|
|
|
|
// These aren't meaningful
|
|
// $FlowFixMe[incompatible-use]
|
|
delete current.id;
|
|
// $FlowFixMe[incompatible-use]
|
|
delete current.isStateEditable;
|
|
|
|
// $FlowFixMe[incompatible-use]
|
|
if (current.subHooks.length > 0) {
|
|
// $FlowFixMe[incompatible-use]
|
|
queue.push(...current.subHooks);
|
|
}
|
|
}
|
|
|
|
sanitize(cloned);
|
|
|
|
try {
|
|
return JSON.stringify(cloned, null, 2);
|
|
} catch (error) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
// Keeping this in memory seems to be enough to enable the browser to download larger profiles.
|
|
// Without this, we would see a "Download failed: network error" failure.
|
|
let downloadUrl = null;
|
|
|
|
export function downloadFile(
|
|
element: HTMLAnchorElement,
|
|
filename: string,
|
|
text: string,
|
|
): void {
|
|
const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
|
|
|
|
if (downloadUrl !== null) {
|
|
URL.revokeObjectURL(downloadUrl);
|
|
}
|
|
|
|
downloadUrl = URL.createObjectURL(blob);
|
|
|
|
element.setAttribute('href', downloadUrl);
|
|
element.setAttribute('download', filename);
|
|
|
|
element.click();
|
|
}
|
|
|
|
export function truncateText(text: string, maxLength: number): string {
|
|
const {length} = text;
|
|
if (length > maxLength) {
|
|
return (
|
|
text.slice(0, Math.floor(maxLength / 2)) +
|
|
'…' +
|
|
text.slice(length - Math.ceil(maxLength / 2) - 1)
|
|
);
|
|
} else {
|
|
return text;
|
|
}
|
|
}
|