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,8 +30,13 @@ import type {
2630 RendererID ,
2731 RendererInterface ,
2832 DevToolsHookSettings ,
33+ InspectedElement ,
2934} from './types' ;
30- 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' ;
3140import type { GroupItem } from './views/TraceUpdates/canvas' ;
3241import { gte , isReactNativeEnvironment } from './utils' ;
3342import {
@@ -73,6 +82,13 @@ type InspectElementParams = {
7382 requestID : number ,
7483} ;
7584
85+ type InspectScreenParams = {
86+ forceFullData : boolean ,
87+ id : number ,
88+ path : Array < string | number > | null ,
89+ requestID : number ,
90+ } ;
91+
7692type OverrideHookParams = {
7793 id : number ,
7894 hookID : number ,
@@ -131,8 +147,6 @@ type OverrideSuspenseParams = {
131147} ;
132148
133149type OverrideSuspenseMilestoneParams = {
134- rendererID : number ,
135- rootID : number ,
136150 suspendedSet : Array < number > ,
137151} ;
138152
@@ -141,6 +155,111 @@ type PersistedSelection = {
141155 path : Array < PathFrame > ,
142156} ;
143157
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+
144263export default class Agent extends EventEmitter < {
145264 hideNativeHighlight : [ ] ,
146265 showNativeHighlight : [ HostInstance ] ,
@@ -201,6 +320,7 @@ export default class Agent extends EventEmitter<{
201320 bridge . addListener ( 'getProfilingStatus' , this . getProfilingStatus ) ;
202321 bridge . addListener ( 'getOwnersList' , this . getOwnersList ) ;
203322 bridge . addListener ( 'inspectElement' , this . inspectElement ) ;
323+ bridge . addListener ( 'inspectScreen' , this . inspectScreen ) ;
204324 bridge . addListener ( 'logElementToConsole' , this . logElementToConsole ) ;
205325 bridge . addListener ( 'overrideError' , this . overrideError ) ;
206326 bridge . addListener ( 'overrideSuspense' , this . overrideSuspense ) ;
@@ -531,6 +651,138 @@ export default class Agent extends EventEmitter<{
531651 }
532652 } ;
533653
654+ inspectScreen : InspectScreenParams => void = ( {
655+ requestID,
656+ id,
657+ forceFullData,
658+ path : screenPath ,
659+ } ) = > {
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+
685+ for ( const rendererID in this . _rendererInterfaces ) {
686+ const renderer = ( ( this . _rendererInterfaces [
687+ ( rendererID : any )
688+ ] : any ) : RendererInterface ) ;
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 (
706+ requestID ,
707+ id ,
708+ path ,
709+ forceFullData ,
710+ ) ;
711+ switch ( inspectedRootsPayload . type ) {
712+ case 'hydrated - path ':
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 ) ;
729+ // If we hydrated a path, it must've been in a specific renderer so we can stop here.
730+ return ;
731+ case 'full - data ':
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 ;
752+ case 'not-found' :
753+ break ;
754+ case 'error' :
755+ // bail out and show the error
756+ // TODO: aggregate errors
757+ this . _bridge . send ( 'inspectedScreen' , inspectedRootsPayload ) ;
758+ return ;
759+ }
760+ }
761+
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+ }
784+ } ;
785+
534786 logElementToConsole : ElementAndRendererID => void = ( { id, rendererID} ) => {
535787 const renderer = this . _rendererInterfaces [ rendererID ] ;
536788 if ( renderer == null ) {
@@ -567,17 +819,15 @@ export default class Agent extends EventEmitter<{
567819 } ;
568820
569821 overrideSuspenseMilestone : OverrideSuspenseMilestoneParams => void = ( {
570- rendererID,
571- rootID,
572822 suspendedSet,
573823 } ) = > {
574- const renderer = this . _rendererInterfaces [ rendererID ] ;
575- if ( renderer == null ) {
576- console . warn (
577- `Invalid renderer id " ${ rendererID } " to override suspense milestone` ,
578- ) ;
579- } else {
580- renderer . overrideSuspenseMilestone ( rootID , suspendedSet ) ;
824+ for ( const rendererID in this . _rendererInterfaces ) {
825+ const renderer = ( ( this . _rendererInterfaces [
826+ ( rendererID : any )
827+ ] : any ) : RendererInterface ) ;
828+ if ( renderer . supportsTogglingSuspense ) {
829+ renderer . overrideSuspenseMilestone ( suspendedSet ) ;
830+ }
581831 }
582832 } ;
583833
0 commit comments