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}]}>
|
<Suspense name="three" rects={[{x:1,y:2,width:5,height:1}]}>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const rendererID = getRendererID();
|
|
||||||
const rootID = store.getRootIDForElement(store.getElementIDAtIndex(0));
|
|
||||||
await actAsync(() => {
|
await actAsync(() => {
|
||||||
agent.overrideSuspenseMilestone({
|
agent.overrideSuspenseMilestone({
|
||||||
rendererID,
|
|
||||||
rootID,
|
|
||||||
suspendedSet: [
|
suspendedSet: [
|
||||||
store.getElementIDAtIndex(4),
|
store.getElementIDAtIndex(4),
|
||||||
store.getElementIDAtIndex(8),
|
store.getElementIDAtIndex(8),
|
||||||
|
|
@ -1009,8 +1005,6 @@ describe('Store', () => {
|
||||||
|
|
||||||
await actAsync(() => {
|
await actAsync(() => {
|
||||||
agent.overrideSuspenseMilestone({
|
agent.overrideSuspenseMilestone({
|
||||||
rendererID,
|
|
||||||
rootID,
|
|
||||||
suspendedSet: [],
|
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 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 setupHighlighter from './views/Highlighter';
|
||||||
import {
|
import {
|
||||||
initialize as setupTraceUpdates,
|
initialize as setupTraceUpdates,
|
||||||
|
|
@ -26,8 +30,13 @@ import type {
|
||||||
RendererID,
|
RendererID,
|
||||||
RendererInterface,
|
RendererInterface,
|
||||||
DevToolsHookSettings,
|
DevToolsHookSettings,
|
||||||
|
InspectedElement,
|
||||||
} from './types';
|
} 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 type {GroupItem} from './views/TraceUpdates/canvas';
|
||||||
import {gte, isReactNativeEnvironment} from './utils';
|
import {gte, isReactNativeEnvironment} from './utils';
|
||||||
import {
|
import {
|
||||||
|
|
@ -73,6 +82,13 @@ type InspectElementParams = {
|
||||||
requestID: number,
|
requestID: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type InspectScreenParams = {
|
||||||
|
forceFullData: boolean,
|
||||||
|
id: number,
|
||||||
|
path: Array<string | number> | null,
|
||||||
|
requestID: number,
|
||||||
|
};
|
||||||
|
|
||||||
type OverrideHookParams = {
|
type OverrideHookParams = {
|
||||||
id: number,
|
id: number,
|
||||||
hookID: number,
|
hookID: number,
|
||||||
|
|
@ -131,8 +147,6 @@ type OverrideSuspenseParams = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type OverrideSuspenseMilestoneParams = {
|
type OverrideSuspenseMilestoneParams = {
|
||||||
rendererID: number,
|
|
||||||
rootID: number,
|
|
||||||
suspendedSet: Array<number>,
|
suspendedSet: Array<number>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -141,6 +155,111 @@ type PersistedSelection = {
|
||||||
path: Array<PathFrame>,
|
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<{
|
export default class Agent extends EventEmitter<{
|
||||||
hideNativeHighlight: [],
|
hideNativeHighlight: [],
|
||||||
showNativeHighlight: [HostInstance],
|
showNativeHighlight: [HostInstance],
|
||||||
|
|
@ -201,6 +320,7 @@ export default class Agent extends EventEmitter<{
|
||||||
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
|
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
|
||||||
bridge.addListener('getOwnersList', this.getOwnersList);
|
bridge.addListener('getOwnersList', this.getOwnersList);
|
||||||
bridge.addListener('inspectElement', this.inspectElement);
|
bridge.addListener('inspectElement', this.inspectElement);
|
||||||
|
bridge.addListener('inspectScreen', this.inspectScreen);
|
||||||
bridge.addListener('logElementToConsole', this.logElementToConsole);
|
bridge.addListener('logElementToConsole', this.logElementToConsole);
|
||||||
bridge.addListener('overrideError', this.overrideError);
|
bridge.addListener('overrideError', this.overrideError);
|
||||||
bridge.addListener('overrideSuspense', this.overrideSuspense);
|
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}) => {
|
logElementToConsole: ElementAndRendererID => void = ({id, rendererID}) => {
|
||||||
const renderer = this._rendererInterfaces[rendererID];
|
const renderer = this._rendererInterfaces[rendererID];
|
||||||
if (renderer == null) {
|
if (renderer == null) {
|
||||||
|
|
@ -567,17 +819,15 @@ export default class Agent extends EventEmitter<{
|
||||||
};
|
};
|
||||||
|
|
||||||
overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({
|
overrideSuspenseMilestone: OverrideSuspenseMilestoneParams => void = ({
|
||||||
rendererID,
|
|
||||||
rootID,
|
|
||||||
suspendedSet,
|
suspendedSet,
|
||||||
}) => {
|
}) => {
|
||||||
const renderer = this._rendererInterfaces[rendererID];
|
for (const rendererID in this._rendererInterfaces) {
|
||||||
if (renderer == null) {
|
const renderer = ((this._rendererInterfaces[
|
||||||
console.warn(
|
(rendererID: any)
|
||||||
`Invalid renderer id "${rendererID}" to override suspense milestone`,
|
]: any): RendererInterface);
|
||||||
);
|
if (renderer.supportsTogglingSuspense) {
|
||||||
} else {
|
renderer.overrideSuspenseMilestone(suspendedSet);
|
||||||
renderer.overrideSuspenseMilestone(rootID, suspendedSet);
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2420,7 +2420,6 @@ export function attach(
|
||||||
!isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0,
|
!isProductionBuildOfRenderer && StrictModeBits !== 0 ? 1 : 0,
|
||||||
);
|
);
|
||||||
pushOperation(hasOwnerMetadata ? 1 : 0);
|
pushOperation(hasOwnerMetadata ? 1 : 0);
|
||||||
pushOperation(supportsTogglingSuspense ? 1 : 0);
|
|
||||||
|
|
||||||
if (isProfiling) {
|
if (isProfiling) {
|
||||||
if (displayNamesByRootID !== null) {
|
if (displayNamesByRootID !== null) {
|
||||||
|
|
@ -4902,7 +4901,11 @@ export function attach(
|
||||||
fiberInstance.data = nextFiber;
|
fiberInstance.data = nextFiber;
|
||||||
if (
|
if (
|
||||||
mostRecentlyInspectedElement !== null &&
|
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)
|
didFiberRender(prevFiber, nextFiber)
|
||||||
) {
|
) {
|
||||||
// If this Fiber has updated, clear cached inspected data.
|
// If this Fiber has updated, clear cached inspected data.
|
||||||
|
|
@ -6422,7 +6425,10 @@ export function attach(
|
||||||
return inspectVirtualInstanceRaw(devtoolsInstance);
|
return inspectVirtualInstanceRaw(devtoolsInstance);
|
||||||
}
|
}
|
||||||
if (devtoolsInstance.kind === FIBER_INSTANCE) {
|
if (devtoolsInstance.kind === FIBER_INSTANCE) {
|
||||||
return inspectFiberInstanceRaw(devtoolsInstance);
|
const isRoot = devtoolsInstance.parent === null;
|
||||||
|
return isRoot
|
||||||
|
? inspectRootsRaw(devtoolsInstance.id)
|
||||||
|
: inspectFiberInstanceRaw(devtoolsInstance);
|
||||||
}
|
}
|
||||||
(devtoolsInstance: FilteredFiberInstance); // assert exhaustive
|
(devtoolsInstance: FilteredFiberInstance); // assert exhaustive
|
||||||
throw new Error('Unsupported instance kind');
|
throw new Error('Unsupported instance kind');
|
||||||
|
|
@ -6875,10 +6881,24 @@ export function attach(
|
||||||
let currentlyInspectedPaths: Object = {};
|
let currentlyInspectedPaths: Object = {};
|
||||||
|
|
||||||
function isMostRecentlyInspectedElement(id: number): boolean {
|
function isMostRecentlyInspectedElement(id: number): boolean {
|
||||||
return (
|
if (mostRecentlyInspectedElement === null) {
|
||||||
mostRecentlyInspectedElement !== null &&
|
return false;
|
||||||
mostRecentlyInspectedElement.id === id
|
}
|
||||||
);
|
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 {
|
function isMostRecentlyInspectedElementCurrent(id: number): boolean {
|
||||||
|
|
@ -7060,8 +7080,8 @@ export function attach(
|
||||||
if (!hasElementUpdatedSinceLastInspected) {
|
if (!hasElementUpdatedSinceLastInspected) {
|
||||||
if (path !== null) {
|
if (path !== null) {
|
||||||
let secondaryCategory: 'suspendedBy' | 'hooks' | null = null;
|
let secondaryCategory: 'suspendedBy' | 'hooks' | null = null;
|
||||||
if (path[0] === 'hooks') {
|
if (path[0] === 'hooks' || path[0] === 'suspendedBy') {
|
||||||
secondaryCategory = 'hooks';
|
secondaryCategory = path[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this element has not been updated since it was last inspected,
|
// 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) {
|
function logElementToConsole(id: number) {
|
||||||
const result = isMostRecentlyInspectedElementCurrent(id)
|
const result = isMostRecentlyInspectedElementCurrent(id)
|
||||||
? mostRecentlyInspectedElement
|
? mostRecentlyInspectedElement
|
||||||
|
|
@ -7867,13 +7980,9 @@ export function attach(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the all other roots of this renderer.
|
* Resets the all other roots of this renderer.
|
||||||
* @param rootID The root that contains this milestone
|
|
||||||
* @param suspendedSet List of IDs of SuspenseComponent Fibers
|
* @param suspendedSet List of IDs of SuspenseComponent Fibers
|
||||||
*/
|
*/
|
||||||
function overrideSuspenseMilestone(
|
function overrideSuspenseMilestone(suspendedSet: Array<FiberInstance['id']>) {
|
||||||
rootID: FiberInstance['id'],
|
|
||||||
suspendedSet: Array<FiberInstance['id']>,
|
|
||||||
) {
|
|
||||||
if (
|
if (
|
||||||
typeof setSuspenseHandler !== 'function' ||
|
typeof setSuspenseHandler !== 'function' ||
|
||||||
typeof scheduleUpdate !== '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);
|
const unsuspendedSet: Set<Fiber> = new Set(forceFallbackForFibers);
|
||||||
|
|
||||||
let resuspended = false;
|
let resuspended = false;
|
||||||
|
|
|
||||||
|
|
@ -412,7 +412,6 @@ export function attach(
|
||||||
pushOperation(0); // Profiling flag
|
pushOperation(0); // Profiling flag
|
||||||
pushOperation(0); // StrictMode supported?
|
pushOperation(0); // StrictMode supported?
|
||||||
pushOperation(hasOwnerMetadata ? 1 : 0);
|
pushOperation(hasOwnerMetadata ? 1 : 0);
|
||||||
pushOperation(supportsTogglingSuspense ? 1 : 0);
|
|
||||||
|
|
||||||
pushOperation(SUSPENSE_TREE_OPERATION_ADD);
|
pushOperation(SUSPENSE_TREE_OPERATION_ADD);
|
||||||
pushOperation(id);
|
pushOperation(id);
|
||||||
|
|
@ -800,6 +799,20 @@ export function attach(
|
||||||
return null;
|
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 {key} = getData(internalInstance);
|
||||||
const type = getElementType(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 {
|
function logElementToConsole(id: number): void {
|
||||||
const result = inspectElementRaw(id);
|
const result = inspectElementRaw(id);
|
||||||
if (result === null) {
|
if (result === null) {
|
||||||
|
|
|
||||||
|
|
@ -450,10 +450,7 @@ export type RendererInterface = {
|
||||||
onErrorOrWarning?: OnErrorOrWarning,
|
onErrorOrWarning?: OnErrorOrWarning,
|
||||||
overrideError: (id: number, forceError: boolean) => void,
|
overrideError: (id: number, forceError: boolean) => void,
|
||||||
overrideSuspense: (id: number, forceFallback: boolean) => void,
|
overrideSuspense: (id: number, forceFallback: boolean) => void,
|
||||||
overrideSuspenseMilestone: (
|
overrideSuspenseMilestone: (suspendedSet: Array<number>) => void,
|
||||||
rootID: number,
|
|
||||||
suspendedSet: Array<number>,
|
|
||||||
) => void,
|
|
||||||
overrideValueAtPath: (
|
overrideValueAtPath: (
|
||||||
type: Type,
|
type: Type,
|
||||||
id: number,
|
id: number,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
import Agent from 'react-devtools-shared/src/backend/agent';
|
import Agent from 'react-devtools-shared/src/backend/agent';
|
||||||
import {hideOverlay, showOverlay} from './Highlighter';
|
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 {BackendBridge} from 'react-devtools-shared/src/bridge';
|
||||||
import type {RendererInterface} from '../../types';
|
import type {RendererInterface} from '../../types';
|
||||||
|
|
||||||
|
|
@ -26,6 +27,7 @@ export default function setupHighlighter(
|
||||||
): void {
|
): void {
|
||||||
bridge.addListener('clearHostInstanceHighlight', clearHostInstanceHighlight);
|
bridge.addListener('clearHostInstanceHighlight', clearHostInstanceHighlight);
|
||||||
bridge.addListener('highlightHostInstance', highlightHostInstance);
|
bridge.addListener('highlightHostInstance', highlightHostInstance);
|
||||||
|
bridge.addListener('highlightHostInstances', highlightHostInstances);
|
||||||
bridge.addListener('scrollToHostInstance', scrollToHostInstance);
|
bridge.addListener('scrollToHostInstance', scrollToHostInstance);
|
||||||
bridge.addListener('shutdown', stopInspectingHost);
|
bridge.addListener('shutdown', stopInspectingHost);
|
||||||
bridge.addListener('startInspectingHost', startInspectingHost);
|
bridge.addListener('startInspectingHost', startInspectingHost);
|
||||||
|
|
@ -157,6 +159,52 @@ export default function setupHighlighter(
|
||||||
hideOverlay(agent);
|
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(
|
function attemptScrollToHostInstance(
|
||||||
renderer: RendererInterface,
|
renderer: RendererInterface,
|
||||||
id: number,
|
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,
|
id: number,
|
||||||
path: InspectedElementPath | null,
|
path: InspectedElementPath | null,
|
||||||
rendererID: number,
|
rendererID: number,
|
||||||
shouldListenToPauseEvents: boolean = false,
|
shouldListenToPauseEvents: boolean,
|
||||||
): Promise<InspectedElementPayload> {
|
): Promise<InspectedElementPayload> {
|
||||||
const requestID = requestCounter++;
|
const requestID = requestCounter++;
|
||||||
const promise = getPromiseForRequestID<InspectedElementPayload>(
|
const promise = getPromiseForRequestID<InspectedElementPayload>(
|
||||||
|
|
@ -117,6 +117,32 @@ export function inspectElement(
|
||||||
return promise;
|
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;
|
let storeAsGlobalCount = 0;
|
||||||
|
|
||||||
export function storeAsGlobal({
|
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,
|
version: 2,
|
||||||
minNpmVersion: '4.22.0',
|
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,
|
maxNpmVersion: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -92,6 +86,12 @@ type HighlightHostInstance = {
|
||||||
openBuiltinElementsPanel: boolean,
|
openBuiltinElementsPanel: boolean,
|
||||||
scrollIntoView: boolean,
|
scrollIntoView: boolean,
|
||||||
};
|
};
|
||||||
|
type HighlightHostInstances = {
|
||||||
|
elements: Array<ElementAndRendererID>,
|
||||||
|
displayName: string | null,
|
||||||
|
hideAfterTimeout: boolean,
|
||||||
|
scrollIntoView: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
type ScrollToHostInstance = {
|
type ScrollToHostInstance = {
|
||||||
...ElementAndRendererID,
|
...ElementAndRendererID,
|
||||||
|
|
@ -145,8 +145,6 @@ type OverrideSuspense = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type OverrideSuspenseMilestone = {
|
type OverrideSuspenseMilestone = {
|
||||||
rendererID: number,
|
|
||||||
rootID: number,
|
|
||||||
suspendedSet: Array<number>,
|
suspendedSet: Array<number>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -167,6 +165,13 @@ type InspectElementParams = {
|
||||||
requestID: number,
|
requestID: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type InspectScreenParams = {
|
||||||
|
requestID: number,
|
||||||
|
id: number,
|
||||||
|
forceFullData: boolean,
|
||||||
|
path: Array<number | string> | null,
|
||||||
|
};
|
||||||
|
|
||||||
type StoreAsGlobalParams = {
|
type StoreAsGlobalParams = {
|
||||||
...ElementAndRendererID,
|
...ElementAndRendererID,
|
||||||
count: number,
|
count: number,
|
||||||
|
|
@ -199,6 +204,7 @@ export type BackendEvents = {
|
||||||
fastRefreshScheduled: [],
|
fastRefreshScheduled: [],
|
||||||
getSavedPreferences: [],
|
getSavedPreferences: [],
|
||||||
inspectedElement: [InspectedElementPayload],
|
inspectedElement: [InspectedElementPayload],
|
||||||
|
inspectedScreen: [InspectedElementPayload],
|
||||||
isReloadAndProfileSupportedByBackend: [boolean],
|
isReloadAndProfileSupportedByBackend: [boolean],
|
||||||
operations: [Array<number>],
|
operations: [Array<number>],
|
||||||
ownersList: [OwnersList],
|
ownersList: [OwnersList],
|
||||||
|
|
@ -243,7 +249,9 @@ type FrontendEvents = {
|
||||||
getProfilingData: [{rendererID: RendererID}],
|
getProfilingData: [{rendererID: RendererID}],
|
||||||
getProfilingStatus: [],
|
getProfilingStatus: [],
|
||||||
highlightHostInstance: [HighlightHostInstance],
|
highlightHostInstance: [HighlightHostInstance],
|
||||||
|
highlightHostInstances: [HighlightHostInstances],
|
||||||
inspectElement: [InspectElementParams],
|
inspectElement: [InspectElementParams],
|
||||||
|
inspectScreen: [InspectScreenParams],
|
||||||
logElementToConsole: [ElementAndRendererID],
|
logElementToConsole: [ElementAndRendererID],
|
||||||
overrideError: [OverrideError],
|
overrideError: [OverrideError],
|
||||||
overrideSuspense: [OverrideSuspense],
|
overrideSuspense: [OverrideSuspense],
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,6 @@ export type Capabilities = {
|
||||||
supportsBasicProfiling: boolean,
|
supportsBasicProfiling: boolean,
|
||||||
hasOwnerMetadata: boolean,
|
hasOwnerMetadata: boolean,
|
||||||
supportsStrictMode: boolean,
|
supportsStrictMode: boolean,
|
||||||
supportsTogglingSuspense: boolean,
|
|
||||||
supportsAdvancedProfiling: AdvancedProfiling,
|
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 build of DevTools supports the Timeline profiler.
|
||||||
// This is a static flag, controlled by the Store config.
|
// This is a static flag, controlled by the Store config.
|
||||||
get supportsTimeline(): boolean {
|
get supportsTimeline(): boolean {
|
||||||
|
|
@ -898,38 +889,48 @@ export default class Store extends EventEmitter<{
|
||||||
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
|
* @param uniqueSuspendersOnly Filters out boundaries without unique suspenders
|
||||||
*/
|
*/
|
||||||
getSuspendableDocumentOrderSuspense(
|
getSuspendableDocumentOrderSuspense(
|
||||||
rootID: Element['id'] | void,
|
|
||||||
uniqueSuspendersOnly: boolean,
|
uniqueSuspendersOnly: boolean,
|
||||||
): $ReadOnlyArray<SuspenseNode['id']> {
|
): $ReadOnlyArray<SuspenseNode['id']> {
|
||||||
if (rootID === undefined) {
|
const roots = this.roots;
|
||||||
return [];
|
if (roots.length === 0) {
|
||||||
}
|
|
||||||
const root = this.getElementByID(rootID);
|
|
||||||
if (root === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (!this.supportsTogglingSuspense(rootID)) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const list: SuspenseNode['id'][] = [];
|
const list: SuspenseNode['id'][] = [];
|
||||||
const suspense = this.getSuspenseByID(rootID);
|
for (let i = 0; i < roots.length; i++) {
|
||||||
if (suspense !== null) {
|
const rootID = roots[i];
|
||||||
const stack = [suspense];
|
const root = this.getElementByID(rootID);
|
||||||
while (stack.length > 0) {
|
if (root === null) {
|
||||||
const current = stack.pop();
|
continue;
|
||||||
if (current === undefined) {
|
}
|
||||||
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.
|
const stack = [suspense];
|
||||||
if (!uniqueSuspendersOnly || current.hasUniqueSuspenders) {
|
while (stack.length > 0) {
|
||||||
list.push(current.id);
|
const current = stack.pop();
|
||||||
}
|
if (current === undefined) {
|
||||||
// Add children in reverse order to maintain document order
|
continue;
|
||||||
for (let j = current.children.length - 1; j >= 0; j--) {
|
}
|
||||||
const childSuspense = this.getSuspenseByID(current.children[j]);
|
if (
|
||||||
if (childSuspense !== null) {
|
(!uniqueSuspendersOnly || current.hasUniqueSuspenders) &&
|
||||||
stack.push(childSuspense);
|
// 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 supportsStrictMode = false;
|
||||||
let hasOwnerMetadata = 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 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.
|
// 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;
|
hasOwnerMetadata = operations[i] > 0;
|
||||||
i++;
|
i++;
|
||||||
|
|
||||||
supportsTogglingSuspense = operations[i] > 0;
|
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._roots = this._roots.concat(id);
|
this._roots = this._roots.concat(id);
|
||||||
|
|
@ -1215,7 +1212,6 @@ export default class Store extends EventEmitter<{
|
||||||
supportsBasicProfiling,
|
supportsBasicProfiling,
|
||||||
hasOwnerMetadata,
|
hasOwnerMetadata,
|
||||||
supportsStrictMode,
|
supportsStrictMode,
|
||||||
supportsTogglingSuspense,
|
|
||||||
supportsAdvancedProfiling,
|
supportsAdvancedProfiling,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1561,7 +1557,12 @@ export default class Store extends EventEmitter<{
|
||||||
if (name === null) {
|
if (name === null) {
|
||||||
// The boundary isn't explicitly named.
|
// The boundary isn't explicitly named.
|
||||||
// Pick a sensible default.
|
// 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++; // Profiling flag
|
||||||
i++; // supportsStrictMode flag
|
i++; // supportsStrictMode flag
|
||||||
i++; // hasOwnerMetadata flag
|
i++; // hasOwnerMetadata flag
|
||||||
i++; // supportsTogglingSuspense flag
|
|
||||||
|
|
||||||
if (__DEBUG__) {
|
if (__DEBUG__) {
|
||||||
debug('Add', `new root fiber ${id}`);
|
debug('Add', `new root fiber ${id}`);
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export default function SuspenseBreadcrumbs(): React$Node {
|
||||||
const store = useContext(StoreContext);
|
const store = useContext(StoreContext);
|
||||||
const treeDispatch = useContext(TreeDispatcherContext);
|
const treeDispatch = useContext(TreeDispatcherContext);
|
||||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||||
const {selectedSuspenseID, selectedRootID, lineage} = useContext(
|
const {selectedSuspenseID, lineage, roots} = useContext(
|
||||||
SuspenseTreeStateContext,
|
SuspenseTreeStateContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -45,13 +45,13 @@ export default function SuspenseBreadcrumbs(): React$Node {
|
||||||
// that rendered the whole screen. In laymans terms this is really "Initial Paint".
|
// 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
|
// 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.
|
// "Transition" since in that case it's really about a Transition within the page.
|
||||||
selectedRootID !== null ? (
|
roots.length > 0 ? (
|
||||||
<li
|
<li
|
||||||
className={styles.SuspenseBreadcrumbsListItem}
|
className={styles.SuspenseBreadcrumbsListItem}
|
||||||
aria-current={selectedSuspenseID === selectedRootID}>
|
aria-current="true">
|
||||||
<button
|
<button
|
||||||
className={styles.SuspenseBreadcrumbsButton}
|
className={styles.SuspenseBreadcrumbsButton}
|
||||||
onClick={handleClick.bind(null, selectedRootID)}
|
onClick={handleClick.bind(null, roots[0])}
|
||||||
type="button">
|
type="button">
|
||||||
Initial Paint
|
Initial Paint
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -278,11 +278,7 @@ function getDocumentBoundingRect(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function SuspenseRectsShell({
|
function SuspenseRectsRoot({rootID}: {rootID: SuspenseNode['id']}): React$Node {
|
||||||
rootID,
|
|
||||||
}: {
|
|
||||||
rootID: SuspenseNode['id'],
|
|
||||||
}): React$Node {
|
|
||||||
const store = useContext(StoreContext);
|
const store = useContext(StoreContext);
|
||||||
const root = store.getSuspenseByID(rootID);
|
const root = store.getSuspenseByID(rootID);
|
||||||
if (root === null) {
|
if (root === null) {
|
||||||
|
|
@ -299,6 +295,8 @@ const ViewBox = createContext<Rect>((null: any));
|
||||||
|
|
||||||
function SuspenseRectsContainer(): React$Node {
|
function SuspenseRectsContainer(): React$Node {
|
||||||
const store = useContext(StoreContext);
|
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.
|
// TODO: This relies on a full re-render of all children when the Suspense tree changes.
|
||||||
const {roots} = useContext(SuspenseTreeStateContext);
|
const {roots} = useContext(SuspenseTreeStateContext);
|
||||||
|
|
||||||
|
|
@ -312,14 +310,33 @@ function SuspenseRectsContainer(): React$Node {
|
||||||
const width = '100%';
|
const width = '100%';
|
||||||
const aspectRatio = `1 / ${heightScale}`;
|
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 (
|
return (
|
||||||
<div className={styles.SuspenseRectsContainer}>
|
<div className={styles.SuspenseRectsContainer} onClick={handleClick}>
|
||||||
<ViewBox.Provider value={boundingBox}>
|
<ViewBox.Provider value={boundingBox}>
|
||||||
<div
|
<div
|
||||||
className={styles.SuspenseRectsViewBox}
|
className={styles.SuspenseRectsViewBox}
|
||||||
style={{aspectRatio, width}}>
|
style={{aspectRatio, width}}>
|
||||||
{roots.map(rootID => {
|
{roots.map(rootID => {
|
||||||
return <SuspenseRectsShell key={rootID} rootID={rootID} />;
|
return <SuspenseRectsRoot key={rootID} rootID={rootID} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</ViewBox.Provider>
|
</ViewBox.Provider>
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,9 @@ import {
|
||||||
SuspenseTreeStateContext,
|
SuspenseTreeStateContext,
|
||||||
} from './SuspenseTreeContext';
|
} from './SuspenseTreeContext';
|
||||||
import {StoreContext, OptionsContext} from '../context';
|
import {StoreContext, OptionsContext} from '../context';
|
||||||
import {TreeDispatcherContext} from '../Components/TreeContext';
|
|
||||||
import Button from '../Button';
|
import Button from '../Button';
|
||||||
import Toggle from '../Toggle';
|
import Toggle from '../Toggle';
|
||||||
import typeof {
|
import typeof {SyntheticPointerEvent} from 'react-dom-bindings/src/events/SyntheticEvent';
|
||||||
SyntheticEvent,
|
|
||||||
SyntheticPointerEvent,
|
|
||||||
} from 'react-dom-bindings/src/events/SyntheticEvent';
|
|
||||||
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
|
import SettingsModal from 'react-devtools-shared/src/devtools/views/Settings/SettingsModal';
|
||||||
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
|
import SettingsModalContextToggle from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContextToggle';
|
||||||
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
|
import {SettingsModalContextController} from 'react-devtools-shared/src/devtools/views/Settings/SettingsModalContext';
|
||||||
|
|
@ -71,20 +67,14 @@ function ToggleUniqueSuspenders() {
|
||||||
const store = useContext(StoreContext);
|
const store = useContext(StoreContext);
|
||||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||||
|
|
||||||
const {selectedRootID: rootID, uniqueSuspendersOnly} = useContext(
|
const {uniqueSuspendersOnly} = useContext(SuspenseTreeStateContext);
|
||||||
SuspenseTreeStateContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleToggleUniqueSuspenders() {
|
function handleToggleUniqueSuspenders() {
|
||||||
const nextUniqueSuspendersOnly = !uniqueSuspendersOnly;
|
const nextUniqueSuspendersOnly = !uniqueSuspendersOnly;
|
||||||
const nextTimeline =
|
// TODO: Handle different timeline modes (e.g. random order)
|
||||||
rootID === null
|
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
||||||
? []
|
nextUniqueSuspendersOnly,
|
||||||
: // TODO: Handle different timeline modes (e.g. random order)
|
);
|
||||||
store.getSuspendableDocumentOrderSuspense(
|
|
||||||
rootID,
|
|
||||||
nextUniqueSuspendersOnly,
|
|
||||||
);
|
|
||||||
suspenseTreeDispatch({
|
suspenseTreeDispatch({
|
||||||
type: 'SET_SUSPENSE_TIMELINE',
|
type: 'SET_SUSPENSE_TIMELINE',
|
||||||
payload: [nextTimeline, null, nextUniqueSuspendersOnly],
|
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({
|
function ToggleTreeList({
|
||||||
dispatch,
|
dispatch,
|
||||||
state,
|
state,
|
||||||
|
|
@ -427,7 +368,6 @@ function SuspenseTab(_: {}) {
|
||||||
<div className={styles.SuspenseBreadcrumbs}>
|
<div className={styles.SuspenseBreadcrumbs}>
|
||||||
<SuspenseBreadcrumbs />
|
<SuspenseBreadcrumbs />
|
||||||
</div>
|
</div>
|
||||||
<SelectRoot />
|
|
||||||
<div className={styles.VRule} />
|
<div className={styles.VRule} />
|
||||||
<ToggleUniqueSuspenders />
|
<ToggleUniqueSuspenders />
|
||||||
{!hideSettings && <SettingsModalContextToggle />}
|
{!hideSettings && <SettingsModalContextToggle />}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {useContext, useEffect, useRef} from 'react';
|
import {useContext, useEffect, useRef} from 'react';
|
||||||
import {BridgeContext, StoreContext} from '../context';
|
import {BridgeContext} from '../context';
|
||||||
import {TreeDispatcherContext} from '../Components/TreeContext';
|
import {TreeDispatcherContext} from '../Components/TreeContext';
|
||||||
import {useHighlightHostInstance, useScrollToHostInstance} from '../hooks';
|
import {useHighlightHostInstance, useScrollToHostInstance} from '../hooks';
|
||||||
import {
|
import {
|
||||||
|
|
@ -23,20 +23,15 @@ import ButtonIcon from '../ButtonIcon';
|
||||||
|
|
||||||
function SuspenseTimelineInput() {
|
function SuspenseTimelineInput() {
|
||||||
const bridge = useContext(BridgeContext);
|
const bridge = useContext(BridgeContext);
|
||||||
const store = useContext(StoreContext);
|
|
||||||
const treeDispatch = useContext(TreeDispatcherContext);
|
const treeDispatch = useContext(TreeDispatcherContext);
|
||||||
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
const suspenseTreeDispatch = useContext(SuspenseTreeDispatcherContext);
|
||||||
const {highlightHostInstance, clearHighlightHostInstance} =
|
const {highlightHostInstance, clearHighlightHostInstance} =
|
||||||
useHighlightHostInstance();
|
useHighlightHostInstance();
|
||||||
const scrollToHostInstance = useScrollToHostInstance();
|
const scrollToHostInstance = useScrollToHostInstance();
|
||||||
|
|
||||||
const {
|
const {timeline, timelineIndex, hoveredTimelineIndex, playing} = useContext(
|
||||||
selectedRootID: rootID,
|
SuspenseTreeStateContext,
|
||||||
timeline,
|
);
|
||||||
timelineIndex,
|
|
||||||
hoveredTimelineIndex,
|
|
||||||
playing,
|
|
||||||
} = useContext(SuspenseTreeStateContext);
|
|
||||||
|
|
||||||
const min = 0;
|
const min = 0;
|
||||||
const max = timeline.length > 0 ? timeline.length - 1 : 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.
|
// For now we just exclude it from deps since we don't lint those anyway.
|
||||||
function changeTimelineIndex(newIndex: number) {
|
function changeTimelineIndex(newIndex: number) {
|
||||||
// Synchronize timeline index with what is resuspended.
|
// 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
|
// 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
|
// anything suspended in the root. The step after that should have one less
|
||||||
// thing suspended. I.e. the first suspense boundary should be unsuspended
|
// 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.
|
// when it's selected. This also lets you show everything in the last step.
|
||||||
const suspendedSet = timeline.slice(timelineIndex + 1);
|
const suspendedSet = timeline.slice(timelineIndex + 1);
|
||||||
bridge.send('overrideSuspenseMilestone', {
|
bridge.send('overrideSuspenseMilestone', {
|
||||||
rendererID,
|
|
||||||
rootID,
|
|
||||||
suspendedSet,
|
suspendedSet,
|
||||||
});
|
});
|
||||||
if (isInitialMount.current) {
|
if (isInitialMount.current) {
|
||||||
|
|
@ -164,20 +147,6 @@ function SuspenseTimelineInput() {
|
||||||
};
|
};
|
||||||
}, [playing]);
|
}, [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) {
|
if (timeline.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.SuspenseTimelineInput}>
|
<div className={styles.SuspenseTimelineInput}>
|
||||||
|
|
@ -226,10 +195,9 @@ function SuspenseTimelineInput() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SuspenseTimeline(): React$Node {
|
export default function SuspenseTimeline(): React$Node {
|
||||||
const {selectedRootID} = useContext(SuspenseTreeStateContext);
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.SuspenseTimelineContainer}>
|
<div className={styles.SuspenseTimelineContainer}>
|
||||||
<SuspenseTimelineInput key={selectedRootID} />
|
<SuspenseTimelineInput />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,7 @@
|
||||||
* @flow
|
* @flow
|
||||||
*/
|
*/
|
||||||
import type {ReactContext} from 'shared/ReactTypes';
|
import type {ReactContext} from 'shared/ReactTypes';
|
||||||
import type {
|
import type {SuspenseNode} from 'react-devtools-shared/src/frontend/types';
|
||||||
Element,
|
|
||||||
SuspenseNode,
|
|
||||||
} from 'react-devtools-shared/src/frontend/types';
|
|
||||||
import type Store from '../../store';
|
import type Store from '../../store';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
@ -27,7 +24,6 @@ import {StoreContext} from '../context';
|
||||||
export type SuspenseTreeState = {
|
export type SuspenseTreeState = {
|
||||||
lineage: $ReadOnlyArray<SuspenseNode['id']> | null,
|
lineage: $ReadOnlyArray<SuspenseNode['id']> | null,
|
||||||
roots: $ReadOnlyArray<SuspenseNode['id']>,
|
roots: $ReadOnlyArray<SuspenseNode['id']>,
|
||||||
selectedRootID: SuspenseNode['id'] | null,
|
|
||||||
selectedSuspenseID: SuspenseNode['id'] | null,
|
selectedSuspenseID: SuspenseNode['id'] | null,
|
||||||
timeline: $ReadOnlyArray<SuspenseNode['id']>,
|
timeline: $ReadOnlyArray<SuspenseNode['id']>,
|
||||||
timelineIndex: number | -1,
|
timelineIndex: number | -1,
|
||||||
|
|
@ -107,60 +103,27 @@ type Props = {
|
||||||
children: React$Node,
|
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 {
|
function getInitialState(store: Store): SuspenseTreeState {
|
||||||
let initialState: SuspenseTreeState;
|
|
||||||
const uniqueSuspendersOnly = true;
|
const uniqueSuspendersOnly = true;
|
||||||
const selectedRootID = getDefaultRootID(store);
|
const timeline =
|
||||||
// TODO: Default to nearest from inspected
|
store.getSuspendableDocumentOrderSuspense(uniqueSuspendersOnly);
|
||||||
if (selectedRootID === null) {
|
const timelineIndex = timeline.length - 1;
|
||||||
initialState = {
|
const selectedSuspenseID =
|
||||||
selectedSuspenseID: null,
|
timelineIndex === -1 ? null : timeline[timelineIndex];
|
||||||
lineage: null,
|
const lineage =
|
||||||
roots: store.roots,
|
selectedSuspenseID !== null
|
||||||
selectedRootID,
|
? store.getSuspenseLineage(selectedSuspenseID)
|
||||||
timeline: [],
|
: [];
|
||||||
timelineIndex: -1,
|
const initialState: SuspenseTreeState = {
|
||||||
hoveredTimelineIndex: -1,
|
selectedSuspenseID,
|
||||||
uniqueSuspendersOnly,
|
lineage,
|
||||||
playing: false,
|
roots: store.roots,
|
||||||
};
|
timeline,
|
||||||
} else {
|
timelineIndex,
|
||||||
const timeline = store.getSuspendableDocumentOrderSuspense(
|
hoveredTimelineIndex: -1,
|
||||||
selectedRootID,
|
uniqueSuspendersOnly,
|
||||||
uniqueSuspendersOnly,
|
playing: false,
|
||||||
);
|
};
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return initialState;
|
return initialState;
|
||||||
}
|
}
|
||||||
|
|
@ -209,23 +172,10 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
||||||
selectedTimelineID = removedIDs.get(selectedTimelineID);
|
selectedTimelineID = removedIDs.get(selectedTimelineID);
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextRootID = state.selectedRootID;
|
// TODO: Handle different timeline modes (e.g. random order)
|
||||||
if (selectedTimelineID !== null && selectedTimelineID !== 0) {
|
const nextTimeline = store.getSuspendableDocumentOrderSuspense(
|
||||||
nextRootID =
|
state.uniqueSuspendersOnly,
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
let nextTimelineIndex =
|
let nextTimelineIndex =
|
||||||
selectedTimelineID === null || nextTimeline.length === 0
|
selectedTimelineID === null || nextTimeline.length === 0
|
||||||
|
|
@ -250,7 +200,6 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
||||||
...state,
|
...state,
|
||||||
lineage: nextLineage,
|
lineage: nextLineage,
|
||||||
roots: store.roots,
|
roots: store.roots,
|
||||||
selectedRootID: nextRootID,
|
|
||||||
selectedSuspenseID,
|
selectedSuspenseID,
|
||||||
timeline: nextTimeline,
|
timeline: nextTimeline,
|
||||||
timelineIndex: nextTimelineIndex,
|
timelineIndex: nextTimelineIndex,
|
||||||
|
|
@ -258,27 +207,21 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
||||||
}
|
}
|
||||||
case 'SELECT_SUSPENSE_BY_ID': {
|
case 'SELECT_SUSPENSE_BY_ID': {
|
||||||
const selectedSuspenseID = action.payload;
|
const selectedSuspenseID = action.payload;
|
||||||
const selectedRootID =
|
|
||||||
store.getSuspenseRootIDForSuspense(selectedSuspenseID);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
selectedSuspenseID,
|
selectedSuspenseID,
|
||||||
selectedRootID,
|
|
||||||
playing: false, // pause
|
playing: false, // pause
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'SET_SUSPENSE_LINEAGE': {
|
case 'SET_SUSPENSE_LINEAGE': {
|
||||||
const suspenseID = action.payload;
|
const suspenseID = action.payload;
|
||||||
const lineage = store.getSuspenseLineage(suspenseID);
|
const lineage = store.getSuspenseLineage(suspenseID);
|
||||||
const selectedRootID =
|
|
||||||
store.getSuspenseRootIDForSuspense(suspenseID);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
lineage,
|
lineage,
|
||||||
selectedSuspenseID: suspenseID,
|
selectedSuspenseID: suspenseID,
|
||||||
selectedRootID,
|
|
||||||
playing: false, // pause
|
playing: false, // pause
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -316,8 +259,6 @@ function SuspenseTreeContextController({children}: Props): React.Node {
|
||||||
...state,
|
...state,
|
||||||
selectedSuspenseID: nextSelectedSuspenseID,
|
selectedSuspenseID: nextSelectedSuspenseID,
|
||||||
lineage: nextLineage,
|
lineage: nextLineage,
|
||||||
selectedRootID:
|
|
||||||
nextRootID === null ? state.selectedRootID : nextRootID,
|
|
||||||
timeline: nextTimeline,
|
timeline: nextTimeline,
|
||||||
timelineIndex: nextMilestoneIndex,
|
timelineIndex: nextMilestoneIndex,
|
||||||
uniqueSuspendersOnly: nextUniqueSuspendersOnly,
|
uniqueSuspendersOnly: nextUniqueSuspendersOnly,
|
||||||
|
|
|
||||||
|
|
@ -353,20 +353,44 @@ export function useHighlightHostInstance(): {
|
||||||
const highlightHostInstance = useCallback(
|
const highlightHostInstance = useCallback(
|
||||||
(id: number, scrollIntoView?: boolean = false) => {
|
(id: number, scrollIntoView?: boolean = false) => {
|
||||||
const element = store.getElementByID(id);
|
const element = store.getElementByID(id);
|
||||||
const rendererID = store.getRendererIDForElement(id);
|
if (element !== null) {
|
||||||
if (element !== null && rendererID !== null) {
|
const isRoot = element.parentID === 0;
|
||||||
let displayName = element.displayName;
|
let displayName = element.displayName;
|
||||||
if (displayName !== null && element.nameProp !== null) {
|
if (displayName !== null && element.nameProp !== null) {
|
||||||
displayName += ` name="${element.nameProp}"`;
|
displayName += ` name="${element.nameProp}"`;
|
||||||
}
|
}
|
||||||
bridge.send('highlightHostInstance', {
|
if (isRoot) {
|
||||||
displayName,
|
// Inspect screen
|
||||||
hideAfterTimeout: false,
|
const elements: Array<{rendererID: number, id: number}> = [];
|
||||||
id,
|
|
||||||
openBuiltinElementsPanel: false,
|
for (let i = 0; i < store.roots.length; i++) {
|
||||||
rendererID,
|
const rootID = store.roots[i];
|
||||||
scrollIntoView: scrollIntoView,
|
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],
|
[store, bridge],
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
convertInspectedElementBackendToFrontend,
|
convertInspectedElementBackendToFrontend,
|
||||||
hydrateHelper,
|
hydrateHelper,
|
||||||
inspectElement as inspectElementAPI,
|
inspectElement as inspectElementAPI,
|
||||||
|
inspectScreen as inspectScreenAPI,
|
||||||
} from 'react-devtools-shared/src/backendAPI';
|
} from 'react-devtools-shared/src/backendAPI';
|
||||||
import {fillInPath} from 'react-devtools-shared/src/hydration';
|
import {fillInPath} from 'react-devtools-shared/src/hydration';
|
||||||
|
|
||||||
|
|
@ -57,21 +58,31 @@ export function inspectElement(
|
||||||
rendererID: number,
|
rendererID: number,
|
||||||
shouldListenToPauseEvents: boolean = false,
|
shouldListenToPauseEvents: boolean = false,
|
||||||
): Promise<InspectElementReturnType> {
|
): Promise<InspectElementReturnType> {
|
||||||
const {id} = element;
|
const {id, parentID} = element;
|
||||||
|
|
||||||
// This could indicate that the DevTools UI has been closed and reopened.
|
// 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.
|
// 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.
|
// In this case, we need to tell it to resend the full data.
|
||||||
const forceFullData = !inspectedElementCache.has(id);
|
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(
|
return promisedElement.then((data: any) => {
|
||||||
bridge,
|
|
||||||
forceFullData,
|
|
||||||
id,
|
|
||||||
path,
|
|
||||||
rendererID,
|
|
||||||
shouldListenToPauseEvents,
|
|
||||||
).then((data: any) => {
|
|
||||||
const {type} = data;
|
const {type} = data;
|
||||||
|
|
||||||
let inspectedElement;
|
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++; // supportsProfiling
|
||||||
i++; // supportsStrictMode
|
i++; // supportsStrictMode
|
||||||
i++; // hasOwnerMetadata
|
i++; // hasOwnerMetadata
|
||||||
i++; // supportsTogglingSuspense
|
|
||||||
} else {
|
} else {
|
||||||
const parentID = ((operations[i]: any): number);
|
const parentID = ((operations[i]: any): number);
|
||||||
i++;
|
i++;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user