mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[DevTools] Inspect the Initial Paint when inspecting a Root (#34454)
This commit is contained in:
parent
e4a27db283
commit
4a28227960
|
|
@ -974,12 +974,8 @@ describe('Store', () => {
|
|||
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||
`);
|
||||
|
||||
const rendererID = getRendererID();
|
||||
const rootID = store.getRootIDForElement(store.getElementIDAtIndex(0));
|
||||
await actAsync(() => {
|
||||
agent.overrideSuspenseMilestone({
|
||||
rendererID,
|
||||
rootID,
|
||||
suspendedSet: [
|
||||
store.getElementIDAtIndex(4),
|
||||
store.getElementIDAtIndex(8),
|
||||
|
|
@ -1009,8 +1005,6 @@ describe('Store', () => {
|
|||
|
||||
await actAsync(() => {
|
||||
agent.overrideSuspenseMilestone({
|
||||
rendererID,
|
||||
rootID,
|
||||
suspendedSet: [],
|
||||
});
|
||||
});
|
||||
|
|
|
|||
276
packages/react-devtools-shared/src/backend/agent.js
vendored
276
packages/react-devtools-shared/src/backend/agent.js
vendored
|
|
@ -8,7 +8,11 @@
|
|||
*/
|
||||
|
||||
import EventEmitter from '../events';
|
||||
import {SESSION_STORAGE_LAST_SELECTION_KEY, __DEBUG__} from '../constants';
|
||||
import {
|
||||
SESSION_STORAGE_LAST_SELECTION_KEY,
|
||||
UNKNOWN_SUSPENDERS_NONE,
|
||||
__DEBUG__,
|
||||
} from '../constants';
|
||||
import setupHighlighter from './views/Highlighter';
|
||||
import {
|
||||
initialize as setupTraceUpdates,
|
||||
|
|
@ -26,8 +30,13 @@ import type {
|
|||
RendererID,
|
||||
RendererInterface,
|
||||
DevToolsHookSettings,
|
||||
InspectedElement,
|
||||
} from './types';
|
||||
import type {ComponentFilter} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {
|
||||
ComponentFilter,
|
||||
DehydratedData,
|
||||
ElementType,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {GroupItem} from './views/TraceUpdates/canvas';
|
||||
import {gte, isReactNativeEnvironment} from './utils';
|
||||
import {
|
||||
|
|
@ -73,6 +82,13 @@ type InspectElementParams = {
|
|||
requestID: number,
|
||||
};
|
||||
|
||||
type InspectScreenParams = {
|
||||
forceFullData: boolean,
|
||||
id: number,
|
||||
path: Array<string | number> | null,
|
||||
requestID: number,
|
||||
};
|
||||
|
||||
type OverrideHookParams = {
|
||||
id: number,
|
||||
hookID: number,
|
||||
|
|
@ -131,8 +147,6 @@ type OverrideSuspenseParams = {
|
|||
};
|
||||
|
||||
type OverrideSuspenseMilestoneParams = {
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
suspendedSet: Array<number>,
|
||||
};
|
||||
|
||||
|
|
@ -141,6 +155,111 @@ type PersistedSelection = {
|
|||
path: Array<PathFrame>,
|
||||
};
|
||||
|
||||
function createEmptyInspectedScreen(
|
||||
arbitraryRootID: number,
|
||||
type: ElementType,
|
||||
): InspectedElement {
|
||||
const suspendedBy: DehydratedData = {
|
||||
cleaned: [],
|
||||
data: [],
|
||||
unserializable: [],
|
||||
};
|
||||
return {
|
||||
// invariants
|
||||
id: arbitraryRootID,
|
||||
type: type,
|
||||
// Properties we merge
|
||||
isErrored: false,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
suspendedBy,
|
||||
suspendedByRange: null,
|
||||
// TODO: How to merge these?
|
||||
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
|
||||
// Properties where merging doesn't make sense so we ignore them entirely in the UI
|
||||
rootType: null,
|
||||
plugins: {stylex: null},
|
||||
nativeTag: null,
|
||||
env: null,
|
||||
source: null,
|
||||
stack: null,
|
||||
rendererPackageName: null,
|
||||
rendererVersion: null,
|
||||
// These don't make sense for a Root. They're just bottom values.
|
||||
key: null,
|
||||
canEditFunctionProps: false,
|
||||
canEditHooks: false,
|
||||
canEditFunctionPropsDeletePaths: false,
|
||||
canEditFunctionPropsRenamePaths: false,
|
||||
canEditHooksAndDeletePaths: false,
|
||||
canEditHooksAndRenamePaths: false,
|
||||
canToggleError: false,
|
||||
canToggleSuspense: false,
|
||||
isSuspended: false,
|
||||
hasLegacyContext: false,
|
||||
context: null,
|
||||
hooks: null,
|
||||
props: null,
|
||||
state: null,
|
||||
owners: null,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeRoots(
|
||||
left: InspectedElement,
|
||||
right: InspectedElement,
|
||||
suspendedByOffset: number,
|
||||
): void {
|
||||
const leftSuspendedByRange = left.suspendedByRange;
|
||||
const rightSuspendedByRange = right.suspendedByRange;
|
||||
|
||||
if (right.isErrored) {
|
||||
left.isErrored = true;
|
||||
}
|
||||
for (let i = 0; i < right.errors.length; i++) {
|
||||
left.errors.push(right.errors[i]);
|
||||
}
|
||||
for (let i = 0; i < right.warnings.length; i++) {
|
||||
left.warnings.push(right.warnings[i]);
|
||||
}
|
||||
|
||||
const leftSuspendedBy: DehydratedData = left.suspendedBy;
|
||||
const {data, cleaned, unserializable} = (right.suspendedBy: DehydratedData);
|
||||
const leftSuspendedByData = ((leftSuspendedBy.data: any): Array<mixed>);
|
||||
const rightSuspendedByData = ((data: any): Array<mixed>);
|
||||
for (let i = 0; i < rightSuspendedByData.length; i++) {
|
||||
leftSuspendedByData.push(rightSuspendedByData[i]);
|
||||
}
|
||||
for (let i = 0; i < cleaned.length; i++) {
|
||||
leftSuspendedBy.cleaned.push(
|
||||
[suspendedByOffset + cleaned[i][0]].concat(cleaned[i].slice(1)),
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < unserializable.length; i++) {
|
||||
leftSuspendedBy.unserializable.push(
|
||||
[suspendedByOffset + unserializable[i][0]].concat(
|
||||
unserializable[i].slice(1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (rightSuspendedByRange !== null) {
|
||||
if (leftSuspendedByRange === null) {
|
||||
left.suspendedByRange = [
|
||||
rightSuspendedByRange[0],
|
||||
rightSuspendedByRange[1],
|
||||
];
|
||||
} else {
|
||||
if (rightSuspendedByRange[0] < leftSuspendedByRange[0]) {
|
||||
leftSuspendedByRange[0] = rightSuspendedByRange[0];
|
||||
}
|
||||
if (rightSuspendedByRange[1] > leftSuspendedByRange[1]) {
|
||||
leftSuspendedByRange[1] = rightSuspendedByRange[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class Agent extends EventEmitter<{
|
||||
hideNativeHighlight: [],
|
||||
showNativeHighlight: [HostInstance],
|
||||
|
|
@ -201,6 +320,7 @@ export default class Agent extends EventEmitter<{
|
|||
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
|
||||
bridge.addListener('getOwnersList', this.getOwnersList);
|
||||
bridge.addListener('inspectElement', this.inspectElement);
|
||||
bridge.addListener('inspectScreen', this.inspectScreen);
|
||||
bridge.addListener('logElementToConsole', this.logElementToConsole);
|
||||
bridge.addListener('overrideError', this.overrideError);
|
||||
bridge.addListener('overrideSuspense', this.overrideSuspense);
|
||||
|
|
@ -531,6 +651,138 @@ export default class Agent extends EventEmitter<{
|
|||
}
|
||||
};
|
||||
|
||||
inspectScreen: InspectScreenParams => void = ({
|
||||
requestID,
|
||||
id,
|
||||
forceFullData,
|
||||
path: screenPath,
|
||||
}) => {
|
||||
let inspectedScreen: InspectedElement | null = null;
|
||||
let found = false;
|
||||
// the suspendedBy index will be from the previously merged roots.
|
||||
// We need to keep track of how many suspendedBy we've already seen to know
|
||||
// to which renderer the index belongs.
|
||||
let suspendedByOffset = 0;
|
||||
let suspendedByPathIndex: number | null = null;
|
||||
// The path to hydrate for a specific renderer
|
||||
let rendererPath: InspectElementParams['path'] = null;
|
||||
if (screenPath !== null && screenPath.length > 1) {
|
||||
const secondaryCategory = screenPath[0];
|
||||
if (secondaryCategory !== 'suspendedBy') {
|
||||
throw new Error(
|
||||
'Only hydrating suspendedBy paths is supported. This is a bug.',
|
||||
);
|
||||
}
|
||||
if (typeof screenPath[1] !== 'number') {
|
||||
throw new Error(
|
||||
`Expected suspendedBy index to be a number. Received '${screenPath[1]}' instead. This is a bug.`,
|
||||
);
|
||||
}
|
||||
suspendedByPathIndex = screenPath[1];
|
||||
rendererPath = screenPath.slice(2);
|
||||
}
|
||||
|
||||
for (const rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
let path: InspectElementParams['path'] = null;
|
||||
if (suspendedByPathIndex !== null && rendererPath !== null) {
|
||||
const suspendedByPathRendererIndex =
|
||||
suspendedByPathIndex - suspendedByOffset;
|
||||
const rendererHasRequestedSuspendedByPath =
|
||||
renderer.getElementAttributeByPath(id, [
|
||||
'suspendedBy',
|
||||
suspendedByPathRendererIndex,
|
||||
]) !== undefined;
|
||||
if (rendererHasRequestedSuspendedByPath) {
|
||||
path = ['suspendedBy', suspendedByPathRendererIndex].concat(
|
||||
rendererPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const inspectedRootsPayload = renderer.inspectElement(
|
||||
requestID,
|
||||
id,
|
||||
path,
|
||||
forceFullData,
|
||||
);
|
||||
switch (inspectedRootsPayload.type) {
|
||||
case 'hydrated-path':
|
||||
// The path will be relative to the Roots of this renderer. We adjust it
|
||||
// to be relative to all Roots of this implementation.
|
||||
inspectedRootsPayload.path[1] += suspendedByOffset;
|
||||
// TODO: Hydration logic is flawed since the Frontend path is not based
|
||||
// on the original backend data but rather its own representation of it (e.g. due to reorder).
|
||||
// So we can receive null here instead when hydration fails
|
||||
if (inspectedRootsPayload.value !== null) {
|
||||
for (
|
||||
let i = 0;
|
||||
i < inspectedRootsPayload.value.cleaned.length;
|
||||
i++
|
||||
) {
|
||||
inspectedRootsPayload.value.cleaned[i][1] += suspendedByOffset;
|
||||
}
|
||||
}
|
||||
this._bridge.send('inspectedScreen', inspectedRootsPayload);
|
||||
// If we hydrated a path, it must've been in a specific renderer so we can stop here.
|
||||
return;
|
||||
case 'full-data':
|
||||
const inspectedRoots = inspectedRootsPayload.value;
|
||||
if (inspectedScreen === null) {
|
||||
inspectedScreen = createEmptyInspectedScreen(
|
||||
inspectedRoots.id,
|
||||
inspectedRoots.type,
|
||||
);
|
||||
}
|
||||
mergeRoots(inspectedScreen, inspectedRoots, suspendedByOffset);
|
||||
const dehydratedSuspendedBy: DehydratedData =
|
||||
inspectedRoots.suspendedBy;
|
||||
const suspendedBy = ((dehydratedSuspendedBy.data: any): Array<mixed>);
|
||||
suspendedByOffset += suspendedBy.length;
|
||||
found = true;
|
||||
break;
|
||||
case 'no-change':
|
||||
found = true;
|
||||
const rootsSuspendedBy: Array<mixed> =
|
||||
(renderer.getElementAttributeByPath(id, ['suspendedBy']): any);
|
||||
suspendedByOffset += rootsSuspendedBy.length;
|
||||
break;
|
||||
case 'not-found':
|
||||
break;
|
||||
case 'error':
|
||||
// bail out and show the error
|
||||
// TODO: aggregate errors
|
||||
this._bridge.send('inspectedScreen', inspectedRootsPayload);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (inspectedScreen === null) {
|
||||
if (found) {
|
||||
this._bridge.send('inspectedScreen', {
|
||||
type: 'no-change',
|
||||
responseID: requestID,
|
||||
id,
|
||||
});
|
||||
} else {
|
||||
this._bridge.send('inspectedScreen', {
|
||||
type: 'not-found',
|
||||
responseID: requestID,
|
||||
id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this._bridge.send('inspectedScreen', {
|
||||
type: 'full-data',
|
||||
responseID: requestID,
|
||||
id,
|
||||
value: inspectedScreen,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
logElementToConsole: ElementAndRendererID => void = ({id, rendererID}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
|
|
@ -567,17 +819,15 @@ export default class Agent extends EventEmitter<{
|
|||
};
|
||||
|
||||
overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({
|
||||
rendererID,
|
||||
rootID,
|
||||
suspendedSet,
|
||||
}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(
|
||||
`Invalid renderer id "${rendererID}" to override suspense milestone`,
|
||||
);
|
||||
} else {
|
||||
renderer.overrideSuspenseMilestone(rootID, suspendedSet);
|
||||
for (const rendererID in this._rendererInterfaces) {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
if (renderer.supportsTogglingSuspense) {
|
||||
renderer.overrideSuspenseMilestone(suspendedSet);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2420,7 +2420,6 @@ export function attach(
|
|||
!isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0,
|
||||
);
|
||||
pushOperation(hasOwnerMetadata ? 1 : 0);
|
||||
pushOperation(supportsTogglingSuspense ? 1 : 0);
|
||||
|
||||
if (isProfiling) {
|
||||
if (displayNamesByRootID !== null) {
|
||||
|
|
@ -4902,7 +4901,11 @@ export function attach(
|
|||
fiberInstance.data = nextFiber;
|
||||
if (
|
||||
mostRecentlyInspectedElement !== null &&
|
||||
mostRecentlyInspectedElement.id === fiberInstance.id &&
|
||||
(mostRecentlyInspectedElement.id === fiberInstance.id ||
|
||||
// If we're inspecting a Root, we inspect the Screen.
|
||||
// Invalidating any Root invalidates the Screen too.
|
||||
(mostRecentlyInspectedElement.type === ElementTypeRoot &&
|
||||
nextFiber.tag === HostRoot)) &&
|
||||
didFiberRender(prevFiber, nextFiber)
|
||||
) {
|
||||
// If this Fiber has updated, clear cached inspected data.
|
||||
|
|
@ -6422,7 +6425,10 @@ export function attach(
|
|||
return inspectVirtualInstanceRaw(devtoolsInstance);
|
||||
}
|
||||
if (devtoolsInstance.kind === FIBER_INSTANCE) {
|
||||
return inspectFiberInstanceRaw(devtoolsInstance);
|
||||
const isRoot = devtoolsInstance.parent === null;
|
||||
return isRoot
|
||||
? inspectRootsRaw(devtoolsInstance.id)
|
||||
: inspectFiberInstanceRaw(devtoolsInstance);
|
||||
}
|
||||
(devtoolsInstance: FilteredFiberInstance); // assert exhaustive
|
||||
throw new Error('Unsupported instance kind');
|
||||
|
|
@ -6875,10 +6881,24 @@ export function attach(
|
|||
let currentlyInspectedPaths: Object = {};
|
||||
|
||||
function isMostRecentlyInspectedElement(id: number): boolean {
|
||||
return (
|
||||
mostRecentlyInspectedElement !== null &&
|
||||
mostRecentlyInspectedElement.id === id
|
||||
);
|
||||
if (mostRecentlyInspectedElement === null) {
|
||||
return false;
|
||||
}
|
||||
if (mostRecentlyInspectedElement.id === id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mostRecentlyInspectedElement.type === ElementTypeRoot) {
|
||||
// we inspected the screen recently. If we're inspecting another root, we're
|
||||
// still inspecting the screen.
|
||||
const instance = idToDevToolsInstanceMap.get(id);
|
||||
return (
|
||||
instance !== undefined &&
|
||||
instance.kind === FIBER_INSTANCE &&
|
||||
instance.parent === null
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isMostRecentlyInspectedElementCurrent(id: number): boolean {
|
||||
|
|
@ -7060,8 +7080,8 @@ export function attach(
|
|||
if (!hasElementUpdatedSinceLastInspected) {
|
||||
if (path !== null) {
|
||||
let secondaryCategory: 'suspendedBy' | 'hooks' | null = null;
|
||||
if (path[0] === 'hooks') {
|
||||
secondaryCategory = 'hooks';
|
||||
if (path[0] === 'hooks' || path[0] === 'suspendedBy') {
|
||||
secondaryCategory = path[0];
|
||||
}
|
||||
|
||||
// If this element has not been updated since it was last inspected,
|
||||
|
|
@ -7209,6 +7229,99 @@ export function attach(
|
|||
};
|
||||
}
|
||||
|
||||
function inspectRootsRaw(arbitraryRootID: number): InspectedElement | null {
|
||||
const roots = hook.getFiberRoots(rendererID);
|
||||
if (roots.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inspectedRoots: InspectedElement = {
|
||||
// invariants
|
||||
id: arbitraryRootID,
|
||||
type: ElementTypeRoot,
|
||||
// Properties we merge
|
||||
isErrored: false,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
suspendedBy: [],
|
||||
suspendedByRange: null,
|
||||
// TODO: How to merge these?
|
||||
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
|
||||
// Properties where merging doesn't make sense so we ignore them entirely in the UI
|
||||
rootType: null,
|
||||
plugins: {stylex: null},
|
||||
nativeTag: null,
|
||||
env: null,
|
||||
source: null,
|
||||
stack: null,
|
||||
rendererPackageName: null,
|
||||
rendererVersion: null,
|
||||
// These don't make sense for a Root. They're just bottom values.
|
||||
key: null,
|
||||
canEditFunctionProps: false,
|
||||
canEditHooks: false,
|
||||
canEditFunctionPropsDeletePaths: false,
|
||||
canEditFunctionPropsRenamePaths: false,
|
||||
canEditHooksAndDeletePaths: false,
|
||||
canEditHooksAndRenamePaths: false,
|
||||
canToggleError: false,
|
||||
canToggleSuspense: false,
|
||||
isSuspended: false,
|
||||
hasLegacyContext: false,
|
||||
context: null,
|
||||
hooks: null,
|
||||
props: null,
|
||||
state: null,
|
||||
owners: null,
|
||||
};
|
||||
|
||||
let minSuspendedByRange = Infinity;
|
||||
let maxSuspendedByRange = -Infinity;
|
||||
roots.forEach(root => {
|
||||
const rootInstance = rootToFiberInstanceMap.get(root);
|
||||
if (rootInstance === undefined) {
|
||||
throw new Error(
|
||||
'Expected a root instance to exist for this Fiber root',
|
||||
);
|
||||
}
|
||||
const inspectedRoot = inspectFiberInstanceRaw(rootInstance);
|
||||
if (inspectedRoot === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inspectedRoot.isErrored) {
|
||||
inspectedRoots.isErrored = true;
|
||||
}
|
||||
for (let i = 0; i < inspectedRoot.errors.length; i++) {
|
||||
inspectedRoots.errors.push(inspectedRoot.errors[i]);
|
||||
}
|
||||
for (let i = 0; i < inspectedRoot.warnings.length; i++) {
|
||||
inspectedRoots.warnings.push(inspectedRoot.warnings[i]);
|
||||
}
|
||||
for (let i = 0; i < inspectedRoot.suspendedBy.length; i++) {
|
||||
inspectedRoots.suspendedBy.push(inspectedRoot.suspendedBy[i]);
|
||||
}
|
||||
const suspendedByRange = inspectedRoot.suspendedByRange;
|
||||
if (suspendedByRange !== null) {
|
||||
if (suspendedByRange[0] < minSuspendedByRange) {
|
||||
minSuspendedByRange = suspendedByRange[0];
|
||||
}
|
||||
if (suspendedByRange[1] > maxSuspendedByRange) {
|
||||
maxSuspendedByRange = suspendedByRange[1];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (minSuspendedByRange !== Infinity || maxSuspendedByRange !== -Infinity) {
|
||||
inspectedRoots.suspendedByRange = [
|
||||
minSuspendedByRange,
|
||||
maxSuspendedByRange,
|
||||
];
|
||||
}
|
||||
|
||||
return inspectedRoots;
|
||||
}
|
||||
|
||||
function logElementToConsole(id: number) {
|
||||
const result = isMostRecentlyInspectedElementCurrent(id)
|
||||
? mostRecentlyInspectedElement
|
||||
|
|
@ -7867,13 +7980,9 @@ export function attach(
|
|||
|
||||
/**
|
||||
* Resets the all other roots of this renderer.
|
||||
* @param rootID The root that contains this milestone
|
||||
* @param suspendedSet List of IDs of SuspenseComponent Fibers
|
||||
*/
|
||||
function overrideSuspenseMilestone(
|
||||
rootID: FiberInstance['id'],
|
||||
suspendedSet: Array<FiberInstance['id']>,
|
||||
) {
|
||||
function overrideSuspenseMilestone(suspendedSet: Array<FiberInstance['id']>) {
|
||||
if (
|
||||
typeof setSuspenseHandler !== 'function' ||
|
||||
typeof scheduleUpdate !== 'function'
|
||||
|
|
@ -7883,8 +7992,6 @@ export function attach(
|
|||
);
|
||||
}
|
||||
|
||||
// TODO: Allow overriding the timeline for the specified root.
|
||||
|
||||
const unsuspendedSet: Set<Fiber> = new Set(forceFallbackForFibers);
|
||||
|
||||
let resuspended = false;
|
||||
|
|
|
|||
|
|
@ -412,7 +412,6 @@ export function attach(
|
|||
pushOperation(0); // Profiling flag
|
||||
pushOperation(0); // StrictMode supported?
|
||||
pushOperation(hasOwnerMetadata ? 1 : 0);
|
||||
pushOperation(supportsTogglingSuspense ? 1 : 0);
|
||||
|
||||
pushOperation(SUSPENSE_TREE_OPERATION_ADD);
|
||||
pushOperation(id);
|
||||
|
|
@ -800,6 +799,20 @@ export function attach(
|
|||
return null;
|
||||
}
|
||||
|
||||
const rootID = internalInstanceToRootIDMap.get(internalInstance);
|
||||
if (rootID === undefined) {
|
||||
throw new Error('Expected to find root ID.');
|
||||
}
|
||||
const isRoot = rootID === id;
|
||||
return isRoot
|
||||
? inspectRootsRaw(rootID)
|
||||
: inspectInternalInstanceRaw(id, internalInstance);
|
||||
}
|
||||
|
||||
function inspectInternalInstanceRaw(
|
||||
id: number,
|
||||
internalInstance: InternalInstance,
|
||||
): InspectedElement | null {
|
||||
const {key} = getData(internalInstance);
|
||||
const type = getElementType(internalInstance);
|
||||
|
||||
|
|
@ -903,6 +916,98 @@ export function attach(
|
|||
};
|
||||
}
|
||||
|
||||
function inspectRootsRaw(arbitraryRootID: number): InspectedElement | null {
|
||||
const roots =
|
||||
renderer.Mount._instancesByReactRootID ||
|
||||
renderer.Mount._instancesByContainerID;
|
||||
|
||||
const inspectedRoots: InspectedElement = {
|
||||
// invariants
|
||||
id: arbitraryRootID,
|
||||
type: ElementTypeRoot,
|
||||
// Properties we merge
|
||||
isErrored: false,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
suspendedBy: [],
|
||||
suspendedByRange: null,
|
||||
// TODO: How to merge these?
|
||||
unknownSuspenders: UNKNOWN_SUSPENDERS_NONE,
|
||||
// Properties where merging doesn't make sense so we ignore them entirely in the UI
|
||||
rootType: null,
|
||||
plugins: {stylex: null},
|
||||
nativeTag: null,
|
||||
env: null,
|
||||
source: null,
|
||||
stack: null,
|
||||
// TODO: We could make the Frontend accept an array to display
|
||||
// a list of unique renderers contributing to this Screen.
|
||||
rendererPackageName: null,
|
||||
rendererVersion: null,
|
||||
// These don't make sense for a Root. They're just bottom values.
|
||||
key: null,
|
||||
canEditFunctionProps: false,
|
||||
canEditHooks: false,
|
||||
canEditFunctionPropsDeletePaths: false,
|
||||
canEditFunctionPropsRenamePaths: false,
|
||||
canEditHooksAndDeletePaths: false,
|
||||
canEditHooksAndRenamePaths: false,
|
||||
canToggleError: false,
|
||||
canToggleSuspense: false,
|
||||
isSuspended: false,
|
||||
hasLegacyContext: false,
|
||||
context: null,
|
||||
hooks: null,
|
||||
props: null,
|
||||
state: null,
|
||||
owners: null,
|
||||
};
|
||||
|
||||
let minSuspendedByRange = Infinity;
|
||||
let maxSuspendedByRange = -Infinity;
|
||||
|
||||
for (const rootKey in roots) {
|
||||
const internalInstance = roots[rootKey];
|
||||
const id = getID(internalInstance);
|
||||
const inspectedRoot = inspectInternalInstanceRaw(id, internalInstance);
|
||||
|
||||
if (inspectedRoot === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inspectedRoot.isErrored) {
|
||||
inspectedRoots.isErrored = true;
|
||||
}
|
||||
for (let i = 0; i < inspectedRoot.errors.length; i++) {
|
||||
inspectedRoots.errors.push(inspectedRoot.errors[i]);
|
||||
}
|
||||
for (let i = 0; i < inspectedRoot.warnings.length; i++) {
|
||||
inspectedRoots.warnings.push(inspectedRoot.warnings[i]);
|
||||
}
|
||||
for (let i = 0; i < inspectedRoot.suspendedBy.length; i++) {
|
||||
inspectedRoots.suspendedBy.push(inspectedRoot.suspendedBy[i]);
|
||||
}
|
||||
const suspendedByRange = inspectedRoot.suspendedByRange;
|
||||
if (suspendedByRange !== null) {
|
||||
if (suspendedByRange[0] < minSuspendedByRange) {
|
||||
minSuspendedByRange = suspendedByRange[0];
|
||||
}
|
||||
if (suspendedByRange[1] > maxSuspendedByRange) {
|
||||
maxSuspendedByRange = suspendedByRange[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (minSuspendedByRange !== Infinity || maxSuspendedByRange !== -Infinity) {
|
||||
inspectedRoots.suspendedByRange = [
|
||||
minSuspendedByRange,
|
||||
maxSuspendedByRange,
|
||||
];
|
||||
}
|
||||
|
||||
return inspectedRoots;
|
||||
}
|
||||
|
||||
function logElementToConsole(id: number): void {
|
||||
const result = inspectElementRaw(id);
|
||||
if (result === null) {
|
||||
|
|
|
|||
|
|
@ -450,10 +450,7 @@ export type RendererInterface = {
|
|||
onErrorOrWarning?: OnErrorOrWarning,
|
||||
overrideError: (id: number, forceError: boolean) => void,
|
||||
overrideSuspense: (id: number, forceFallback: boolean) => void,
|
||||
overrideSuspenseMilestone: (
|
||||
rootID: number,
|
||||
suspendedSet: Array<number>,
|
||||
) => void,
|
||||
overrideSuspenseMilestone: (suspendedSet: Array<number>) => void,
|
||||
overrideValueAtPath: (
|
||||
type: Type,
|
||||
id: number,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import Agent from 'react-devtools-shared/src/backend/agent';
|
||||
import {hideOverlay, showOverlay} from './Highlighter';
|
||||
|
||||
import type {HostInstance} from 'react-devtools-shared/src/backend/types';
|
||||
import type {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||
import type {RendererInterface} from '../../types';
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ export default function setupHighlighter(
|
|||
): void {
|
||||
bridge.addListener('clearHostInstanceHighlight', clearHostInstanceHighlight);
|
||||
bridge.addListener('highlightHostInstance', highlightHostInstance);
|
||||
bridge.addListener('highlightHostInstances', highlightHostInstances);
|
||||
bridge.addListener('scrollToHostInstance', scrollToHostInstance);
|
||||
bridge.addListener('shutdown', stopInspectingHost);
|
||||
bridge.addListener('startInspectingHost', startInspectingHost);
|
||||
|
|
@ -157,6 +159,52 @@ export default function setupHighlighter(
|
|||
hideOverlay(agent);
|
||||
}
|
||||
|
||||
function highlightHostInstances({
|
||||
displayName,
|
||||
hideAfterTimeout,
|
||||
elements,
|
||||
scrollIntoView,
|
||||
}: {
|
||||
displayName: string | null,
|
||||
hideAfterTimeout: boolean,
|
||||
elements: Array<{rendererID: number, id: number}>,
|
||||
scrollIntoView: boolean,
|
||||
}) {
|
||||
const nodes: Array<HostInstance> = [];
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const {id, rendererID} = elements[i];
|
||||
const renderer = agent.rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// In some cases fiber may already be unmounted
|
||||
if (!renderer.hasElementWithId(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hostInstances = renderer.findHostInstancesForElementID(id);
|
||||
if (hostInstances !== null) {
|
||||
for (let j = 0; j < hostInstances.length; j++) {
|
||||
nodes.push(hostInstances[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodes.length > 0) {
|
||||
const node = nodes[0];
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (scrollIntoView && typeof node.scrollIntoView === 'function') {
|
||||
// If the node isn't visible show it before highlighting it.
|
||||
// We may want to reconsider this; it might be a little disruptive.
|
||||
node.scrollIntoView({block: 'nearest', inline: 'nearest'});
|
||||
}
|
||||
}
|
||||
|
||||
showOverlay(nodes, displayName, agent, hideAfterTimeout);
|
||||
}
|
||||
|
||||
function attemptScrollToHostInstance(
|
||||
renderer: RendererInterface,
|
||||
id: number,
|
||||
|
|
|
|||
28
packages/react-devtools-shared/src/backendAPI.js
vendored
28
packages/react-devtools-shared/src/backendAPI.js
vendored
|
|
@ -95,7 +95,7 @@ export function inspectElement(
|
|||
id: number,
|
||||
path: InspectedElementPath | null,
|
||||
rendererID: number,
|
||||
shouldListenToPauseEvents: boolean = false,
|
||||
shouldListenToPauseEvents: boolean,
|
||||
): Promise<InspectedElementPayload> {
|
||||
const requestID = requestCounter++;
|
||||
const promise = getPromiseForRequestID<InspectedElementPayload>(
|
||||
|
|
@ -117,6 +117,32 @@ export function inspectElement(
|
|||
return promise;
|
||||
}
|
||||
|
||||
export function inspectScreen(
|
||||
bridge: FrontendBridge,
|
||||
forceFullData: boolean,
|
||||
arbitraryRootID: number,
|
||||
path: InspectedElementPath | null,
|
||||
shouldListenToPauseEvents: boolean,
|
||||
): Promise<InspectedElementPayload> {
|
||||
const requestID = requestCounter++;
|
||||
const promise = getPromiseForRequestID<InspectedElementPayload>(
|
||||
requestID,
|
||||
'inspectedScreen',
|
||||
bridge,
|
||||
`Timed out while inspecting screen.`,
|
||||
shouldListenToPauseEvents,
|
||||
);
|
||||
|
||||
bridge.send('inspectScreen', {
|
||||
requestID,
|
||||
id: arbitraryRootID,
|
||||
path,
|
||||
forceFullData,
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
let storeAsGlobalCount = 0;
|
||||
|
||||
export function storeAsGlobal({
|
||||
|
|
|
|||
24
packages/react-devtools-shared/src/bridge.js
vendored
24
packages/react-devtools-shared/src/bridge.js
vendored
|
|
@ -65,12 +65,6 @@ export const BRIDGE_PROTOCOL: Array<BridgeProtocol> = [
|
|||
{
|
||||
version: 2,
|
||||
minNpmVersion: '4.22.0',
|
||||
maxNpmVersion: '6.2.0',
|
||||
},
|
||||
// Version 3 adds supports-toggling-suspense bit to add-root
|
||||
{
|
||||
version: 3,
|
||||
minNpmVersion: '6.2.0',
|
||||
maxNpmVersion: null,
|
||||
},
|
||||
];
|
||||
|
|
@ -92,6 +86,12 @@ type HighlightHostInstance = {
|
|||
openBuiltinElementsPanel: boolean,
|
||||
scrollIntoView: boolean,
|
||||
};
|
||||
type HighlightHostInstances = {
|
||||
elements: Array<ElementAndRendererID>,
|
||||
displayName: string | null,
|
||||
hideAfterTimeout: boolean,
|
||||
scrollIntoView: boolean,
|
||||
};
|
||||
|
||||
type ScrollToHostInstance = {
|
||||
...ElementAndRendererID,
|
||||
|
|
@ -145,8 +145,6 @@ type OverrideSuspense = {
|
|||
};
|
||||
|
||||
type OverrideSuspenseMilestone = {
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
suspendedSet: Array<number>,
|
||||
};
|
||||
|
||||
|
|
@ -167,6 +165,13 @@ type InspectElementParams = {
|
|||
requestID: number,
|
||||
};
|
||||
|
||||
type InspectScreenParams = {
|
||||
requestID: number,
|
||||
id: number,
|
||||
forceFullData: boolean,
|
||||
path: Array<number | string> | null,
|
||||
};
|
||||
|
||||
type StoreAsGlobalParams = {
|
||||
...ElementAndRendererID,
|
||||
count: number,
|
||||
|
|
@ -199,6 +204,7 @@ export type BackendEvents = {
|
|||
fastRefreshScheduled: [],
|
||||
getSavedPreferences: [],
|
||||
inspectedElement: [InspectedElementPayload],
|
||||
inspectedScreen: [InspectedElementPayload],
|
||||
isReloadAndProfileSupportedByBackend: [boolean],
|
||||
operations: [Array<number>],
|
||||
ownersList: [OwnersList],
|
||||
|
|
@ -243,7 +249,9 @@ type FrontendEvents = {
|
|||
getProfilingData: [{rendererID: RendererID}],
|
||||
getProfilingStatus: [],
|
||||
highlightHostInstance: [HighlightHostInstance],
|
||||
highlightHostInstances: [HighlightHostInstances],
|
||||
inspectElement: [InspectElementParams],
|
||||
inspectScreen: [InspectScreenParams],
|
||||
logElementToConsole: [ElementAndRendererID],
|
||||
overrideError: [OverrideError],
|
||||
overrideSuspense: [OverrideSuspense],
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ export type Capabilities = {
|
|||
supportsBasicProfiling: boolean,
|
||||
hasOwnerMetadata: boolean,
|
||||
supportsStrictMode: boolean,
|
||||
supportsTogglingSuspense: boolean,
|
||||
supportsAdvancedProfiling: AdvancedProfiling,
|
||||
};
|
||||
|
||||
|
|
@ -506,14 +505,6 @@ export default class Store extends EventEmitter<{
|
|||
);
|
||||
}
|
||||
|
||||
supportsTogglingSuspense(rootID: Element['id']): boolean {
|
||||
const capabilities = this._rootIDToCapabilities.get(rootID);
|
||||
if (capabilities === undefined) {
|
||||
throw new Error(`No capabilities registered for root ${rootID}`);
|
||||
}
|
||||
return capabilities.supportsTogglingSuspense;
|
||||
}
|
||||
|
||||
// This build of DevTools supports the Timeline profiler.
|
||||
// This is a static flag, controlled by the Store config.
|
||||
get supportsTimeline(): boolean {
|
||||
|
|
@ -898,38 +889,48 @@ export default class Store extends EventEmitter<{
|
|||
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
|
||||
*/
|
||||
getSuspendableDocumentOrderSuspense(
|
||||
rootID: Element['id'] | void,
|
||||
uniqueSuspendersOnly: boolean,
|
||||
): $ReadOnlyArray<SuspenseNode['id']> {
|
||||
if (rootID === undefined) {
|
||||
return [];
|
||||
}
|
||||
const root = this.getElementByID(rootID);
|
||||
if (root === null) {
|
||||
return [];
|
||||
}
|
||||
if (!this.supportsTogglingSuspense(rootID)) {
|
||||
const roots = this.roots;
|
||||
if (roots.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const list: SuspenseNode['id'][] = [];
|
||||
const suspense = this.getSuspenseByID(rootID);
|
||||
if (suspense !== null) {
|
||||
const stack = [suspense];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (current === undefined) {
|
||||
continue;
|
||||
for (let i = 0; i < roots.length; i++) {
|
||||
const rootID = roots[i];
|
||||
const root = this.getElementByID(rootID);
|
||||
if (root === null) {
|
||||
continue;
|
||||
}
|
||||
// TODO: This includes boundaries that can't be suspended due to no support from the renderer.
|
||||
|
||||
const suspense = this.getSuspenseByID(rootID);
|
||||
if (suspense !== null) {
|
||||
if (list.length === 0) {
|
||||
// start with an arbitrary root that will allow inspection of the Screen
|
||||
list.push(suspense.id);
|
||||
}
|
||||
// Include the root even if we won't show it suspended (because that's just blank).
|
||||
// You should be able to see what suspended the shell.
|
||||
if (!uniqueSuspendersOnly || current.hasUniqueSuspenders) {
|
||||
list.push(current.id);
|
||||
}
|
||||
// Add children in reverse order to maintain document order
|
||||
for (let j = current.children.length - 1; j >= 0; j--) {
|
||||
const childSuspense = this.getSuspenseByID(current.children[j]);
|
||||
if (childSuspense !== null) {
|
||||
stack.push(childSuspense);
|
||||
|
||||
const stack = [suspense];
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
if (current === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(!uniqueSuspendersOnly || current.hasUniqueSuspenders) &&
|
||||
// Roots are already included as part of the Screen
|
||||
current.id !== rootID
|
||||
) {
|
||||
list.push(current.id);
|
||||
}
|
||||
// Add children in reverse order to maintain document order
|
||||
for (let j = current.children.length - 1; j >= 0; j--) {
|
||||
const childSuspense = this.getSuspenseByID(current.children[j]);
|
||||
if (childSuspense !== null) {
|
||||
stack.push(childSuspense);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1191,7 +1192,6 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
let supportsStrictMode = false;
|
||||
let hasOwnerMetadata = false;
|
||||
let supportsTogglingSuspense = false;
|
||||
|
||||
// If we don't know the bridge protocol, guess that we're dealing with the latest.
|
||||
// If we do know it, we can take it into consideration when parsing operations.
|
||||
|
|
@ -1204,9 +1204,6 @@ export default class Store extends EventEmitter<{
|
|||
|
||||
hasOwnerMetadata = operations[i] > 0;
|
||||
i++;
|
||||
|
||||
supportsTogglingSuspense = operations[i] > 0;
|
||||
i++;
|
||||
}
|
||||
|
||||
this._roots = this._roots.concat(id);
|
||||
|
|
@ -1215,7 +1212,6 @@ export default class Store extends EventEmitter<{
|
|||
supportsBasicProfiling,
|
||||
hasOwnerMetadata,
|
||||
supportsStrictMode,
|
||||
supportsTogglingSuspense,
|
||||
supportsAdvancedProfiling,
|
||||
});
|
||||
|
||||
|
|
@ -1561,7 +1557,12 @@ export default class Store extends EventEmitter<{
|
|||
if (name === null) {
|
||||
// The boundary isn't explicitly named.
|
||||
// Pick a sensible default.
|
||||
name = this._guessSuspenseName(element);
|
||||
if (parentID === 0) {
|
||||
// For Roots we use their display name.
|
||||
name = element.displayName;
|
||||
} else {
|
||||
name = this._guessSuspenseName(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -209,7 +209,6 @@ function updateTree(
|
|||
i++; // Profiling flag
|
||||
i++; // supportsStrictMode flag
|
||||
i++; // hasOwnerMetadata flag
|
||||
i++; // supportsTogglingSuspense flag
|
||||
|
||||
if (__DEBUG__) {
|
||||
debug('Add', `new root fiber ${id}`);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default function SuspenseBreadcrumbs(): React$Node {
|
|||
const store = useContext(StoreContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||
const {selectedSuspenseID, selectedRootID, lineage} = useContext(
|
||||
const {selectedSuspenseID, lineage, roots} = useContext(
|
||||
SuspenseTreeStateContext,
|
||||
);
|
||||
|
||||
|
|
@ -45,13 +45,13 @@ export default function SuspenseBreadcrumbs(): React$Node {
|
|||
// that rendered the whole screen. In laymans terms this is really "Initial Paint".
|
||||
// TODO: Once we add subtree selection, then the equivalent should be called
|
||||
// "Transition" since in that case it's really about a Transition within the page.
|
||||
selectedRootID !== null ? (
|
||||
roots.length > 0 ? (
|
||||
<li
|
||||
className={styles.SuspenseBreadcrumbsListItem}
|
||||
aria-current={selectedSuspenseID === selectedRootID}>
|
||||
aria-current="true">
|
||||
<button
|
||||
className={styles.SuspenseBreadcrumbsButton}
|
||||
onClick={handleClick.bind(null, selectedRootID)}
|
||||
onClick={handleClick.bind(null, roots[0])}
|
||||
type="button">
|
||||
Initial Paint
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -278,11 +278,7 @@ function getDocumentBoundingRect(
|
|||
};
|
||||
}
|
||||
|
||||
function SuspenseRectsShell({
|
||||
rootID,
|
||||
}: {
|
||||
rootID: SuspenseNode['id'],
|
||||
}): React$Node {
|
||||
function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
const root = store.getSuspenseByID(rootID);
|
||||
if (root === null) {
|
||||
|
|
@ -299,6 +295,8 @@ const ViewBox = createContext<Rect>((null: any));
|
|||
|
||||
function SuspenseRectsContainer(): React$Node {
|
||||
const store = useContext(StoreContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
|
||||
const {roots} = useContext(SuspenseTreeStateContext);
|
||||
|
||||
|
|
@ -312,14 +310,33 @@ function SuspenseRectsContainer(): React$Node {
|
|||
const width = '100%';
|
||||
const aspectRatio = `1 / ${heightScale}`;
|
||||
|
||||
function handleClick(event: SyntheticMouseEvent) {
|
||||
if (event.defaultPrevented) {
|
||||
// Already clicked on an inner rect
|
||||
return;
|
||||
}
|
||||
if (roots.length === 0) {
|
||||
// Nothing to select
|
||||
return;
|
||||
}
|
||||
const arbitraryRootID = roots[0];
|
||||
|
||||
event.preventDefault();
|
||||
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: arbitraryRootID});
|
||||
suspenseTreeDispatch({
|
||||
type: 'SET_SUSPENSE_LINEAGE',
|
||||
payload: arbitraryRootID,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.SuspenseRectsContainer}>
|
||||
<div className={styles.SuspenseRectsContainer} onClick={handleClick}>
|
||||
<ViewBox.Provider value={boundingBox}>
|
||||
<div
|
||||
className={styles.SuspenseRectsViewBox}
|
||||
style={{aspectRatio, width}}>
|
||||
{roots.map(rootID => {
|
||||
return <SuspenseRectsShell key={rootID} rootID={rootID} />;
|
||||
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
|
||||
})}
|
||||
</div>
|
||||
</ViewBox.Provider>
|
||||
|
|
|
|||
|
|
@ -34,13 +34,9 @@ import {
|
|||
SuspenseTreeStateContext,
|
||||
} from './SuspenseTreeContext';
|
||||
import {StoreContext, OptionsContext} from '../context';
|
||||
import {TreeDispatcherContext} from '../Components/TreeContext';
|
||||
import Button from '../Button';
|
||||
import Toggle from '../Toggle';
|
||||
import typeof {
|
||||
SyntheticEvent,
|
||||
SyntheticPointerEvent,
|
||||
} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
|
||||
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
|
||||
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
|
||||
|
|
@ -71,20 +67,14 @@ function ToggleUniqueSuspenders() {
|
|||
const store = useContext(StoreContext);
|
||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||
|
||||
const {selectedRootID: rootID, uniqueSuspendersOnly} = useContext(
|
||||
SuspenseTreeStateContext,
|
||||
);
|
||||
const {uniqueSuspendersOnly} = useContext(SuspenseTreeStateContext);
|
||||
|
||||
function handleToggleUniqueSuspenders() {
|
||||
const nextUniqueSuspendersOnly = !uniqueSuspendersOnly;
|
||||
const nextTimeline =
|
||||
rootID === null
|
||||
? []
|
||||
: // TODO: Handle different timeline modes (e.g. random order)
|
||||
store.getSuspendableDocumentOrderSuspense(
|
||||
rootID,
|
||||
nextUniqueSuspendersOnly,
|
||||
);
|
||||
// TODO: Handle different timeline modes (e.g. random order)
|
||||
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
||||
nextUniqueSuspendersOnly,
|
||||
);
|
||||
suspenseTreeDispatch({
|
||||
type: 'SET_SUSPENSE_TIMELINE',
|
||||
payload: [nextTimeline, null, nextUniqueSuspendersOnly],
|
||||
|
|
@ -101,55 +91,6 @@ function ToggleUniqueSuspenders() {
|
|||
);
|
||||
}
|
||||
|
||||
function SelectRoot() {
|
||||
const store = useContext(StoreContext);
|
||||
const {roots, selectedRootID, uniqueSuspendersOnly} = useContext(
|
||||
SuspenseTreeStateContext,
|
||||
);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||
|
||||
function handleChange(event: SyntheticEvent) {
|
||||
const newRootID = +event.currentTarget.value;
|
||||
// TODO: scrollIntoView both suspense rects and host instance.
|
||||
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
||||
newRootID,
|
||||
uniqueSuspendersOnly,
|
||||
);
|
||||
suspenseTreeDispatch({
|
||||
type: 'SET_SUSPENSE_TIMELINE',
|
||||
payload: [nextTimeline, newRootID, uniqueSuspendersOnly],
|
||||
});
|
||||
if (nextTimeline.length > 0) {
|
||||
const milestone = nextTimeline[nextTimeline.length - 1];
|
||||
treeDispatch({type: 'SELECT_ELEMENT_BY_ID', payload: milestone});
|
||||
}
|
||||
}
|
||||
return (
|
||||
roots.length > 0 && (
|
||||
<select
|
||||
aria-label="Select Suspense Root"
|
||||
className={styles.SuspenseTimelineRootSwitcher}
|
||||
onChange={handleChange}
|
||||
value={selectedRootID === null ? -1 : selectedRootID}>
|
||||
<option disabled={true} value={-1}>
|
||||
----
|
||||
</option>
|
||||
{roots.map(rootID => {
|
||||
// TODO: Use name
|
||||
const name = '#' + rootID;
|
||||
// TODO: Highlight host on hover
|
||||
return (
|
||||
<option key={rootID} value={rootID}>
|
||||
{name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleTreeList({
|
||||
dispatch,
|
||||
state,
|
||||
|
|
@ -427,7 +368,6 @@ function SuspenseTab(_: {}) {
|
|||
<div className={styles.SuspenseBreadcrumbs}>
|
||||
<SuspenseBreadcrumbs />
|
||||
</div>
|
||||
<SelectRoot />
|
||||
<div className={styles.VRule} />
|
||||
<ToggleUniqueSuspenders />
|
||||
{!hideSettings && <SettingsModalContextToggle />}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {useContext, useEffect, useRef} from 'react';
|
||||
import {BridgeContext, StoreContext} from '../context';
|
||||
import {BridgeContext} from '../context';
|
||||
import {TreeDispatcherContext} from '../Components/TreeContext';
|
||||
import {useHighlightHostInstance, useScrollToHostInstance} from '../hooks';
|
||||
import {
|
||||
|
|
@ -23,20 +23,15 @@ import ButtonIcon from '../ButtonIcon';
|
|||
|
||||
function SuspenseTimelineInput() {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||
const {highlightHostInstance, clearHighlightHostInstance} =
|
||||
useHighlightHostInstance();
|
||||
const scrollToHostInstance = useScrollToHostInstance();
|
||||
|
||||
const {
|
||||
selectedRootID: rootID,
|
||||
timeline,
|
||||
timelineIndex,
|
||||
hoveredTimelineIndex,
|
||||
playing,
|
||||
} = useContext(SuspenseTreeStateContext);
|
||||
const {timeline, timelineIndex, hoveredTimelineIndex, playing} = useContext(
|
||||
SuspenseTreeStateContext,
|
||||
);
|
||||
|
||||
const min = 0;
|
||||
const max = timeline.length > 0 ? timeline.length - 1 : 0;
|
||||
|
|
@ -112,24 +107,12 @@ function SuspenseTimelineInput() {
|
|||
// For now we just exclude it from deps since we don't lint those anyway.
|
||||
function changeTimelineIndex(newIndex: number) {
|
||||
// Synchronize timeline index with what is resuspended.
|
||||
if (rootID === null) {
|
||||
return;
|
||||
}
|
||||
const rendererID = store.getRendererIDForElement(rootID);
|
||||
if (rendererID === null) {
|
||||
console.error(
|
||||
`No renderer ID found for root element ${rootID} in suspense timeline.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
// We suspend everything after the current selection. The root isn't showing
|
||||
// anything suspended in the root. The step after that should have one less
|
||||
// thing suspended. I.e. the first suspense boundary should be unsuspended
|
||||
// when it's selected. This also lets you show everything in the last step.
|
||||
const suspendedSet = timeline.slice(timelineIndex + 1);
|
||||
bridge.send('overrideSuspenseMilestone', {
|
||||
rendererID,
|
||||
rootID,
|
||||
suspendedSet,
|
||||
});
|
||||
if (isInitialMount.current) {
|
||||
|
|
@ -164,20 +147,6 @@ function SuspenseTimelineInput() {
|
|||
};
|
||||
}, [playing]);
|
||||
|
||||
if (rootID === null) {
|
||||
return (
|
||||
<div className={styles.SuspenseTimelineInput}>No root selected.</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!store.supportsTogglingSuspense(rootID)) {
|
||||
return (
|
||||
<div className={styles.SuspenseTimelineInput}>
|
||||
Can't step through Suspense in production apps.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (timeline.length === 0) {
|
||||
return (
|
||||
<div className={styles.SuspenseTimelineInput}>
|
||||
|
|
@ -226,10 +195,9 @@ function SuspenseTimelineInput() {
|
|||
}
|
||||
|
||||
export default function SuspenseTimeline(): React$Node {
|
||||
const {selectedRootID} = useContext(SuspenseTreeStateContext);
|
||||
return (
|
||||
<div className={styles.SuspenseTimelineContainer}>
|
||||
<SuspenseTimelineInput key={selectedRootID} />
|
||||
<SuspenseTimelineInput />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,7 @@
|
|||
* @flow
|
||||
*/
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import type {
|
||||
Element,
|
||||
SuspenseNode,
|
||||
} from 'react-devtools-shared/src/frontend/types';
|
||||
import type {SuspenseNode} from 'react-devtools-shared/src/frontend/types';
|
||||
import type Store from '../../store';
|
||||
|
||||
import * as React from 'react';
|
||||
|
|
@ -27,7 +24,6 @@ import {StoreContext} from '../context';
|
|||
export type SuspenseTreeState = {
|
||||
lineage: $ReadOnlyArray<SuspenseNode['id']> | null,
|
||||
roots: $ReadOnlyArray<SuspenseNode['id']>,
|
||||
selectedRootID: SuspenseNode['id'] | null,
|
||||
selectedSuspenseID: SuspenseNode['id'] | null,
|
||||
timeline: $ReadOnlyArray<SuspenseNode['id']>,
|
||||
timelineIndex: number | -1,
|
||||
|
|
@ -107,60 +103,27 @@ type Props = {
|
|||
children: React$Node,
|
||||
};
|
||||
|
||||
function getDefaultRootID(store: Store): Element['id'] | null {
|
||||
const designatedRootID = store.roots.find(rootID => {
|
||||
const suspense = store.getSuspenseByID(rootID);
|
||||
return (
|
||||
store.supportsTogglingSuspense(rootID) &&
|
||||
suspense !== null &&
|
||||
suspense.children.length > 1
|
||||
);
|
||||
});
|
||||
|
||||
return designatedRootID === undefined ? null : designatedRootID;
|
||||
}
|
||||
|
||||
function getInitialState(store: Store): SuspenseTreeState {
|
||||
let initialState: SuspenseTreeState;
|
||||
const uniqueSuspendersOnly = true;
|
||||
const selectedRootID = getDefaultRootID(store);
|
||||
// TODO: Default to nearest from inspected
|
||||
if (selectedRootID === null) {
|
||||
initialState = {
|
||||
selectedSuspenseID: null,
|
||||
lineage: null,
|
||||
roots: store.roots,
|
||||
selectedRootID,
|
||||
timeline: [],
|
||||
timelineIndex: -1,
|
||||
hoveredTimelineIndex: -1,
|
||||
uniqueSuspendersOnly,
|
||||
playing: false,
|
||||
};
|
||||
} else {
|
||||
const timeline = store.getSuspendableDocumentOrderSuspense(
|
||||
selectedRootID,
|
||||
uniqueSuspendersOnly,
|
||||
);
|
||||
const timelineIndex = timeline.length - 1;
|
||||
const selectedSuspenseID =
|
||||
timelineIndex === -1 ? null : timeline[timelineIndex];
|
||||
const lineage =
|
||||
selectedSuspenseID !== null
|
||||
? store.getSuspenseLineage(selectedSuspenseID)
|
||||
: [];
|
||||
initialState = {
|
||||
selectedSuspenseID,
|
||||
lineage,
|
||||
roots: store.roots,
|
||||
selectedRootID,
|
||||
timeline,
|
||||
timelineIndex,
|
||||
hoveredTimelineIndex: -1,
|
||||
uniqueSuspendersOnly,
|
||||
playing: false,
|
||||
};
|
||||
}
|
||||
const timeline =
|
||||
store.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
|
||||
const timelineIndex = timeline.length - 1;
|
||||
const selectedSuspenseID =
|
||||
timelineIndex === -1 ? null : timeline[timelineIndex];
|
||||
const lineage =
|
||||
selectedSuspenseID !== null
|
||||
? store.getSuspenseLineage(selectedSuspenseID)
|
||||
: [];
|
||||
const initialState: SuspenseTreeState = {
|
||||
selectedSuspenseID,
|
||||
lineage,
|
||||
roots: store.roots,
|
||||
timeline,
|
||||
timelineIndex,
|
||||
hoveredTimelineIndex: -1,
|
||||
uniqueSuspendersOnly,
|
||||
playing: false,
|
||||
};
|
||||
|
||||
return initialState;
|
||||
}
|
||||
|
|
@ -209,23 +172,10 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
selectedTimelineID = removedIDs.get(selectedTimelineID);
|
||||
}
|
||||
|
||||
let nextRootID = state.selectedRootID;
|
||||
if (selectedTimelineID !== null && selectedTimelineID !== 0) {
|
||||
nextRootID =
|
||||
store.getSuspenseRootIDForSuspense(selectedTimelineID);
|
||||
}
|
||||
if (nextRootID === null) {
|
||||
nextRootID = getDefaultRootID(store);
|
||||
}
|
||||
|
||||
const nextTimeline =
|
||||
nextRootID === null
|
||||
? []
|
||||
: // TODO: Handle different timeline modes (e.g. random order)
|
||||
store.getSuspendableDocumentOrderSuspense(
|
||||
nextRootID,
|
||||
state.uniqueSuspendersOnly,
|
||||
);
|
||||
// TODO: Handle different timeline modes (e.g. random order)
|
||||
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
||||
state.uniqueSuspendersOnly,
|
||||
);
|
||||
|
||||
let nextTimelineIndex =
|
||||
selectedTimelineID === null || nextTimeline.length === 0
|
||||
|
|
@ -250,7 +200,6 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
...state,
|
||||
lineage: nextLineage,
|
||||
roots: store.roots,
|
||||
selectedRootID: nextRootID,
|
||||
selectedSuspenseID,
|
||||
timeline: nextTimeline,
|
||||
timelineIndex: nextTimelineIndex,
|
||||
|
|
@ -258,27 +207,21 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
}
|
||||
case 'SELECT_SUSPENSE_BY_ID': {
|
||||
const selectedSuspenseID = action.payload;
|
||||
const selectedRootID =
|
||||
store.getSuspenseRootIDForSuspense(selectedSuspenseID);
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedSuspenseID,
|
||||
selectedRootID,
|
||||
playing: false, // pause
|
||||
};
|
||||
}
|
||||
case 'SET_SUSPENSE_LINEAGE': {
|
||||
const suspenseID = action.payload;
|
||||
const lineage = store.getSuspenseLineage(suspenseID);
|
||||
const selectedRootID =
|
||||
store.getSuspenseRootIDForSuspense(suspenseID);
|
||||
|
||||
return {
|
||||
...state,
|
||||
lineage,
|
||||
selectedSuspenseID: suspenseID,
|
||||
selectedRootID,
|
||||
playing: false, // pause
|
||||
};
|
||||
}
|
||||
|
|
@ -316,8 +259,6 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
|||
...state,
|
||||
selectedSuspenseID: nextSelectedSuspenseID,
|
||||
lineage: nextLineage,
|
||||
selectedRootID:
|
||||
nextRootID === null ? state.selectedRootID : nextRootID,
|
||||
timeline: nextTimeline,
|
||||
timelineIndex: nextMilestoneIndex,
|
||||
uniqueSuspendersOnly: nextUniqueSuspendersOnly,
|
||||
|
|
|
|||
|
|
@ -353,20 +353,44 @@ export function useHighlightHostInstance(): {
|
|||
const highlightHostInstance = useCallback(
|
||||
(id: number, scrollIntoView?: boolean = false) => {
|
||||
const element = store.getElementByID(id);
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (element !== null && rendererID !== null) {
|
||||
if (element !== null) {
|
||||
const isRoot = element.parentID === 0;
|
||||
let displayName = element.displayName;
|
||||
if (displayName !== null && element.nameProp !== null) {
|
||||
displayName += ` name="${element.nameProp}"`;
|
||||
}
|
||||
bridge.send('highlightHostInstance', {
|
||||
displayName,
|
||||
hideAfterTimeout: false,
|
||||
id,
|
||||
openBuiltinElementsPanel: false,
|
||||
rendererID,
|
||||
scrollIntoView: scrollIntoView,
|
||||
});
|
||||
if (isRoot) {
|
||||
// Inspect screen
|
||||
const elements: Array<{rendererID: number, id: number}> = [];
|
||||
|
||||
for (let i = 0; i < store.roots.length; i++) {
|
||||
const rootID = store.roots[i];
|
||||
const rendererID = store.getRendererIDForElement(rootID);
|
||||
if (rendererID === null) {
|
||||
continue;
|
||||
}
|
||||
elements.push({rendererID, id: rootID});
|
||||
}
|
||||
|
||||
bridge.send('highlightHostInstances', {
|
||||
displayName,
|
||||
hideAfterTimeout: false,
|
||||
elements,
|
||||
scrollIntoView: scrollIntoView,
|
||||
});
|
||||
} else {
|
||||
const rendererID = store.getRendererIDForElement(id);
|
||||
if (rendererID !== null) {
|
||||
bridge.send('highlightHostInstance', {
|
||||
displayName,
|
||||
hideAfterTimeout: false,
|
||||
id,
|
||||
openBuiltinElementsPanel: false,
|
||||
rendererID,
|
||||
scrollIntoView: scrollIntoView,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[store, bridge],
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
convertInspectedElementBackendToFrontend,
|
||||
hydrateHelper,
|
||||
inspectElement as inspectElementAPI,
|
||||
inspectScreen as inspectScreenAPI,
|
||||
} from 'react-devtools-shared/src/backendAPI';
|
||||
import {fillInPath} from 'react-devtools-shared/src/hydration';
|
||||
|
||||
|
|
@ -57,21 +58,31 @@ export function inspectElement(
|
|||
rendererID: number,
|
||||
shouldListenToPauseEvents: boolean = false,
|
||||
): Promise<InspectElementReturnType> {
|
||||
const {id} = element;
|
||||
const {id, parentID} = element;
|
||||
|
||||
// This could indicate that the DevTools UI has been closed and reopened.
|
||||
// The in-memory cache will be clear but the backend still thinks we have cached data.
|
||||
// In this case, we need to tell it to resend the full data.
|
||||
const forceFullData = !inspectedElementCache.has(id);
|
||||
const isRoot = parentID === 0;
|
||||
const promisedElement = isRoot
|
||||
? inspectScreenAPI(
|
||||
bridge,
|
||||
forceFullData,
|
||||
id,
|
||||
path,
|
||||
shouldListenToPauseEvents,
|
||||
)
|
||||
: inspectElementAPI(
|
||||
bridge,
|
||||
forceFullData,
|
||||
id,
|
||||
path,
|
||||
rendererID,
|
||||
shouldListenToPauseEvents,
|
||||
);
|
||||
|
||||
return inspectElementAPI(
|
||||
bridge,
|
||||
forceFullData,
|
||||
id,
|
||||
path,
|
||||
rendererID,
|
||||
shouldListenToPauseEvents,
|
||||
).then((data: any) => {
|
||||
return promisedElement.then((data: any) => {
|
||||
const {type} = data;
|
||||
|
||||
let inspectedElement;
|
||||
|
|
|
|||
1
packages/react-devtools-shared/src/utils.js
vendored
1
packages/react-devtools-shared/src/utils.js
vendored
|
|
@ -262,7 +262,6 @@ export function printOperationsArray(operations: Array<number>) {
|
|||
i++; // supportsProfiling
|
||||
i++; // supportsStrictMode
|
||||
i++; // hasOwnerMetadata
|
||||
i++; // supportsTogglingSuspense
|
||||
} else {
|
||||
const parentID = ((operations[i]: any): number);
|
||||
i++;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user