88 */
99
1010import EventEmitter from '../events' ;
11- import { SESSION_STORAGE_LAST_SELECTION_KEY , __DEBUG__ } from '../constants' ;
11+ import {
12+ SESSION_STORAGE_LAST_SELECTION_KEY ,
13+ UNKNOWN_SUSPENDERS_NONE ,
14+ __DEBUG__ ,
15+ } from '../constants' ;
1216import setupHighlighter from './views/Highlighter' ;
1317import {
1418 initialize as setupTraceUpdates ,
@@ -26,9 +30,13 @@ import type {
2630 RendererID ,
2731 RendererInterface ,
2832 DevToolsHookSettings ,
29- InspectedElementPayload ,
33+ InspectedElement ,
3034} from './types' ;
31- import type { ComponentFilter } from 'react-devtools-shared/src/frontend/types' ;
35+ import type {
36+ ComponentFilter ,
37+ DehydratedData ,
38+ ElementType ,
39+ } from 'react-devtools-shared/src/frontend/types' ;
3240import type { GroupItem } from './views/TraceUpdates/canvas' ;
3341import { gte , isReactNativeEnvironment } from './utils' ;
3442import {
@@ -147,6 +155,111 @@ type PersistedSelection = {
147155 path : Array < PathFrame > ,
148156} ;
149157
158+ function createEmptyInspectedScreen (
159+ arbitraryRootID : number ,
160+ type : ElementType ,
161+ ) : InspectedElement {
162+ const suspendedBy : DehydratedData = {
163+ cleaned : [ ] ,
164+ data : [ ] ,
165+ unserializable : [ ] ,
166+ } ;
167+ return {
168+ // invariants
169+ id : arbitraryRootID ,
170+ type : type ,
171+ // Properties we merge
172+ isErrored : false ,
173+ errors : [ ] ,
174+ warnings : [ ] ,
175+ suspendedBy,
176+ suspendedByRange : null ,
177+ // TODO: How to merge these?
178+ unknownSuspenders : UNKNOWN_SUSPENDERS_NONE ,
179+ // Properties where merging doesn't make sense so we ignore them entirely in the UI
180+ rootType : null ,
181+ plugins : { stylex : null } ,
182+ nativeTag : null ,
183+ env : null ,
184+ source : null ,
185+ stack : null ,
186+ rendererPackageName : null ,
187+ rendererVersion : null ,
188+ // These don't make sense for a Root. They're just bottom values.
189+ key : null ,
190+ canEditFunctionProps : false ,
191+ canEditHooks : false ,
192+ canEditFunctionPropsDeletePaths : false ,
193+ canEditFunctionPropsRenamePaths : false ,
194+ canEditHooksAndDeletePaths : false ,
195+ canEditHooksAndRenamePaths : false ,
196+ canToggleError : false ,
197+ canToggleSuspense : false ,
198+ isSuspended : false ,
199+ hasLegacyContext : false ,
200+ context : null ,
201+ hooks : null ,
202+ props : null ,
203+ state : null ,
204+ owners : null ,
205+ } ;
206+ }
207+
208+ function mergeRoots (
209+ left : InspectedElement ,
210+ right : InspectedElement ,
211+ suspendedByOffset : number ,
212+ ) : void {
213+ const leftSuspendedByRange = left . suspendedByRange ;
214+ const rightSuspendedByRange = right . suspendedByRange ;
215+
216+ if ( right . isErrored ) {
217+ left . isErrored = true ;
218+ }
219+ for ( let i = 0 ; i < right . errors . length ; i ++ ) {
220+ left . errors . push ( right . errors [ i ] ) ;
221+ }
222+ for ( let i = 0 ; i < right . warnings . length ; i ++ ) {
223+ left . warnings . push ( right . warnings [ i ] ) ;
224+ }
225+
226+ const leftSuspendedBy : DehydratedData = left . suspendedBy ;
227+ const { data , cleaned , unserializable } = ( right . suspendedBy : DehydratedData ) ;
228+ const leftSuspendedByData = ( ( leftSuspendedBy . data : any ) : Array < mixed > ) ;
229+ const rightSuspendedByData = ( ( data : any ) : Array < mixed > ) ;
230+ for ( let i = 0 ; i < rightSuspendedByData . length ; i ++ ) {
231+ leftSuspendedByData . push ( rightSuspendedByData [ i ] ) ;
232+ }
233+ for ( let i = 0 ; i < cleaned . length ; i ++ ) {
234+ leftSuspendedBy . cleaned . push (
235+ [ suspendedByOffset + cleaned [ i ] [ 0 ] ] . concat ( cleaned [ i ] . slice ( 1 ) ) ,
236+ ) ;
237+ }
238+ for ( let i = 0 ; i < unserializable . length ; i ++ ) {
239+ leftSuspendedBy . unserializable . push (
240+ [ suspendedByOffset + unserializable [ i ] [ 0 ] ] . concat (
241+ unserializable [ i ] . slice ( 1 ) ,
242+ ) ,
243+ ) ;
244+ }
245+
246+ if ( rightSuspendedByRange !== null ) {
247+ if ( leftSuspendedByRange === null ) {
248+ left . suspendedByRange = [
249+ rightSuspendedByRange [ 0 ] ,
250+ rightSuspendedByRange [ 1 ] ,
251+ ] ;
252+ } else {
253+ if ( rightSuspendedByRange [ 0 ] < leftSuspendedByRange [ 0 ] ) {
254+ leftSuspendedByRange [ 0 ] = rightSuspendedByRange [ 0 ] ;
255+ }
256+ if ( rightSuspendedByRange [ 1 ] > leftSuspendedByRange [ 1 ] ) {
257+ leftSuspendedByRange [ 1 ] = rightSuspendedByRange [ 1 ] ;
258+ }
259+ }
260+ }
261+ }
262+
150263export default class Agent extends EventEmitter < {
151264 hideNativeHighlight : [ ] ,
152265 showNativeHighlight : [ HostInstance ] ,
@@ -542,43 +655,132 @@ export default class Agent extends EventEmitter<{
542655 requestID,
543656 id,
544657 forceFullData,
545- path,
658+ path : screenPath ,
546659 } ) = > {
547- const payload : InspectedElementPayload = {
548- type : 'no-change' ,
549- id,
550- responseID : requestID ,
551- } ;
660+ let inspectedScreen : InspectedElement | null = null ;
661+ let found = false ;
662+ // the suspendedBy index will be from the previously merged roots.
663+ // We need to keep track of how many suspendedBy we've already seen to know
664+ // to which renderer the index belongs.
665+ let suspendedByOffset = 0 ;
666+ let suspendedByPathIndex : number | null = null ;
667+ // The path to hydrate for a specific renderer
668+ let rendererPath : InspectElementParams [ 'path' ] = null ;
669+ if ( screenPath !== null && screenPath . length > 1 ) {
670+ const secondaryCategory = screenPath [ 0 ] ;
671+ if ( secondaryCategory !== 'suspendedBy' ) {
672+ throw new Error (
673+ 'Only hydrating suspendedBy paths is supported. This is a bug.' ,
674+ ) ;
675+ }
676+ if ( typeof screenPath [ 1 ] !== 'number' ) {
677+ throw new Error (
678+ `Expected suspendedBy index to be a number. Received '${ screenPath [ 1 ] } ' instead. This is a bug.` ,
679+ ) ;
680+ }
681+ suspendedByPathIndex = screenPath [ 1 ] ;
682+ rendererPath = screenPath . slice ( 2 ) ;
683+ }
684+
552685 for ( const rendererID in this . _rendererInterfaces ) {
553686 const renderer = ( ( this . _rendererInterfaces [
554687 ( rendererID : any )
555688 ] : any ) : RendererInterface ) ;
556- const inspectedRoots = renderer . inspectElement (
689+ let path : InspectElementParams [ 'path' ] = null ;
690+ if ( suspendedByPathIndex !== null && rendererPath !== null ) {
691+ const suspendedByPathRendererIndex =
692+ suspendedByPathIndex - suspendedByOffset ;
693+ const rendererHasRequestedSuspendedByPath =
694+ renderer . getElementAttributeByPath ( id , [
695+ 'suspendedBy' ,
696+ suspendedByPathRendererIndex ,
697+ ] ) !== undefined ;
698+ if ( rendererHasRequestedSuspendedByPath ) {
699+ path = [ 'suspendedBy' , suspendedByPathRendererIndex ] . concat (
700+ rendererPath ,
701+ ) ;
702+ }
703+ }
704+
705+ const inspectedRootsPayload = renderer . inspectElement (
557706 requestID ,
558707 id ,
559708 path ,
560709 forceFullData ,
561710 ) ;
562- switch ( inspectedRoots . type ) {
711+ switch ( inspectedRootsPayload . type ) {
563712 case 'hydrated - path ':
564- this . _bridge . send ( 'inspectedScreen' , inspectedRoots ) ;
713+ // The path will be relative to the Roots of this renderer. We adjust it
714+ // to be relative to all Roots of this implementation.
715+ inspectedRootsPayload . path [ 1 ] += suspendedByOffset ;
716+ // TODO: Hydration logic is flawed since the Frontend path is not based
717+ // on the original backend data but rather its own representation of it (e.g. due to reorder).
718+ // So we can receive null here instead when hydration fails
719+ if ( inspectedRootsPayload . value !== null ) {
720+ for (
721+ let i = 0 ;
722+ i < inspectedRootsPayload . value . cleaned . length ;
723+ i ++
724+ ) {
725+ inspectedRootsPayload . value . cleaned [ i ] [ 1 ] += suspendedByOffset ;
726+ }
727+ }
728+ this . _bridge . send ( 'inspectedScreen' , inspectedRootsPayload ) ;
565729 // If we hydrated a path, it must've been in a specific renderer so we can stop here.
566730 return ;
567731 case 'full - data ':
568- // TODO: Handle merging of roots from different renderer implementations.
569- this . _bridge . send ( 'inspectedScreen' , inspectedRoots ) ;
570- return ;
732+ const inspectedRoots = inspectedRootsPayload . value ;
733+ if ( inspectedScreen === null ) {
734+ inspectedScreen = createEmptyInspectedScreen (
735+ inspectedRoots . id ,
736+ inspectedRoots . type ,
737+ ) ;
738+ }
739+ mergeRoots ( inspectedScreen , inspectedRoots , suspendedByOffset ) ;
740+ const dehydratedSuspendedBy : DehydratedData =
741+ inspectedRoots . suspendedBy ;
742+ const suspendedBy = ( ( dehydratedSuspendedBy . data : any ) : Array < mixed > ) ;
743+ suspendedByOffset += suspendedBy . length ;
744+ found = true ;
745+ break ;
746+ case 'no-change' :
747+ found = true ;
748+ const rootsSuspendedBy : Array < mixed > =
749+ ( renderer . getElementAttributeByPath ( id , [ 'suspendedBy' ] ) : any ) ;
750+ suspendedByOffset += rootsSuspendedBy . length ;
751+ break ;
571752 case 'not-found' :
572- continue ;
753+ break ;
573754 case 'error' :
574755 // bail out and show the error
575756 // TODO: aggregate errors
576- this . _bridge . send ( 'inspectedScreen' , inspectedRoots ) ;
757+ this . _bridge . send ( 'inspectedScreen' , inspectedRootsPayload ) ;
577758 return ;
578759 }
579760 }
580761
581- this . _bridge . send ( 'inspectedScreen' , payload ) ;
762+ if ( inspectedScreen === null ) {
763+ if ( found ) {
764+ this . _bridge . send ( 'inspectedScreen' , {
765+ type : 'no-change' ,
766+ responseID : requestID ,
767+ id,
768+ } ) ;
769+ } else {
770+ this . _bridge . send ( 'inspectedScreen' , {
771+ type : 'not-found' ,
772+ responseID : requestID ,
773+ id,
774+ } ) ;
775+ }
776+ } else {
777+ this . _bridge . send ( 'inspectedScreen' , {
778+ type : 'full-data' ,
779+ responseID : requestID ,
780+ id,
781+ value : inspectedScreen ,
782+ } ) ;
783+ }
582784 } ;
583785
584786 logElementToConsole : ElementAndRendererID => void = ( { id, rendererID} ) => {
0 commit comments