@react-devtools-plus/scan 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/plugin.ts ADDED
@@ -0,0 +1,638 @@
1
+ /**
2
+ * React Scan Plugin for React DevTools
3
+ *
4
+ * This plugin integrates React Scan into the React DevTools plugin system,
5
+ * providing performance monitoring and analysis capabilities.
6
+ */
7
+
8
+ import type { ReactDevtoolsScanOptions, ScanInstance } from './types'
9
+ import { getDisplayName, getFiberId } from 'bippy'
10
+ import { getScanInstance, resetScanInstance } from './adapter'
11
+
12
+ /**
13
+ * React Scan plugin configuration
14
+ */
15
+ export interface ScanPluginConfig extends ReactDevtoolsScanOptions {
16
+ /**
17
+ * Whether to auto-start scan on plugin load
18
+ * @default true
19
+ */
20
+ autoStart?: boolean
21
+ }
22
+
23
+ /**
24
+ * Create React Scan plugin
25
+ *
26
+ * @param config - Plugin configuration
27
+ * @returns DevTools plugin instance
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { createScanPlugin } from '@react-devtools-plus/scan/plugin';
32
+ *
33
+ * const scanPlugin = createScanPlugin({
34
+ * enabled: true,
35
+ * showToolbar: true,
36
+ * autoStart: true,
37
+ * });
38
+ * ```
39
+ */
40
+ export function createScanPlugin(config: ScanPluginConfig = {}): any {
41
+ let scanInstance: ScanInstance | null = null
42
+ let context: any = null
43
+
44
+ const {
45
+ autoStart = true,
46
+ ...scanOptions
47
+ } = config
48
+
49
+ // Event emitter for plugin events
50
+ const eventHandlers: Map<string, Set<(data: any) => void>> = new Map()
51
+
52
+ const emit = (eventName: string, data: any) => {
53
+ const handlers = eventHandlers.get(eventName)
54
+ if (handlers) {
55
+ handlers.forEach((handler) => {
56
+ if (typeof handler === 'function') {
57
+ try {
58
+ handler(data)
59
+ }
60
+ catch {
61
+ // Ignore event handler errors
62
+ }
63
+ }
64
+ })
65
+ }
66
+ }
67
+
68
+ const subscribe = (eventName: string, handler: (data: any) => void) => {
69
+ if (!eventHandlers.has(eventName)) {
70
+ eventHandlers.set(eventName, new Set())
71
+ }
72
+ eventHandlers.get(eventName)!.add(handler)
73
+
74
+ // Return unsubscribe function
75
+ return () => {
76
+ const handlers = eventHandlers.get(eventName)
77
+ if (handlers) {
78
+ handlers.delete(handler)
79
+ }
80
+ }
81
+ }
82
+
83
+ return {
84
+ id: 'react-scan',
85
+ name: 'React Scan',
86
+ description: 'Performance monitoring and analysis for React applications',
87
+ version: '1.0.0',
88
+
89
+ // Expose subscribe method for event subscriptions
90
+ subscribe,
91
+
92
+ /**
93
+ * Plugin setup
94
+ */
95
+ async setup(ctx: any) {
96
+ context = ctx
97
+
98
+ // Always initialize the scan instance, and start by default
99
+ const initOptions = {
100
+ enabled: autoStart !== false,
101
+ ...scanOptions,
102
+ }
103
+
104
+ // Always create the scan instance so RPC methods work
105
+ scanInstance = getScanInstance(initOptions)
106
+
107
+ // Always call scan() to start scanning by default (unless autoStart explicitly false)
108
+ if (autoStart !== false) {
109
+ // Use adapter's start which handles globals correctly
110
+ scanInstance.start()
111
+ }
112
+
113
+ // Set up inspect state change listener
114
+ try {
115
+ const scan = getScanInstance()
116
+ if (scan) {
117
+ // Track the last hovered component during inspection
118
+ let lastInspectedComponent: { componentName: string, componentId?: string } | null = null
119
+
120
+ scan.onInspectStateChange((state: any) => {
121
+ // Emit inspect state change event
122
+ // Sanitize state for RPC
123
+ const sanitizedState = {
124
+ kind: state.kind,
125
+ // Include component name if available
126
+ componentName: state.fiber ? (state.fiber.type?.displayName || state.fiber.type?.name || 'Unknown') : undefined,
127
+ }
128
+ emit('inspect-state-changed', sanitizedState)
129
+
130
+ // Track component during inspecting state - save hovered component info
131
+ if (state.kind === 'inspecting' && state.fiber) {
132
+ const componentName = getDisplayName(state.fiber.type) || 'Unknown'
133
+ const componentId = String(getFiberId(state.fiber))
134
+ lastInspectedComponent = { componentName, componentId }
135
+ }
136
+
137
+ // If a component is focused, emit focused component info and set up tracking
138
+ if (state.kind === 'focused') {
139
+ const focusedComponent = scan.getFocusedComponent()
140
+ if (focusedComponent) {
141
+ // Sanitize for RPC - remove non-serializable fields
142
+ const { fiber, domElement, ...serializableComponent } = focusedComponent as any
143
+ emit('component-focused', serializableComponent)
144
+ // Set up render tracking
145
+ scan.setFocusedComponentByName(focusedComponent.componentName)
146
+ }
147
+ }
148
+
149
+ // When inspection ends (inspect-off), emit the last inspected component
150
+ if (state.kind === 'inspect-off' && lastInspectedComponent) {
151
+ emit('component-focused', lastInspectedComponent)
152
+ scan.setFocusedComponentByName(lastInspectedComponent.componentName)
153
+ lastInspectedComponent = null
154
+ }
155
+ })
156
+
157
+ // Subscribe to focused component render changes
158
+ scan.onFocusedComponentChange((info) => {
159
+ emit('focused-component-render', info)
160
+ })
161
+ }
162
+ }
163
+ catch {
164
+ // Ignore errors setting up inspect state listener
165
+ }
166
+
167
+ // Listen for component tree changes if context supports it
168
+ // Listen to component tree changes if supported
169
+ if (ctx.on) {
170
+ ctx.on('component-tree-changed', (_event: any) => {
171
+ // Component tree changed, could update UI here
172
+ })
173
+ }
174
+
175
+ // Register RPC functions if context supports it
176
+ if (ctx.registerRPC) {
177
+ ctx.registerRPC('getScanOptions', () => {
178
+ try {
179
+ const scan = getScanInstance()
180
+ return scan?.getOptions() || null
181
+ }
182
+ catch {
183
+ return null
184
+ }
185
+ })
186
+
187
+ ctx.registerRPC('setScanOptions', (options: Partial<ReactDevtoolsScanOptions>) => {
188
+ try {
189
+ const scan = getScanInstance()
190
+ if (scan) {
191
+ scan.setOptions(options)
192
+ return true
193
+ }
194
+ return false
195
+ }
196
+ catch {
197
+ return false
198
+ }
199
+ })
200
+
201
+ ctx.registerRPC('startScan', () => {
202
+ try {
203
+ const scan = getScanInstance()
204
+ if (scan) {
205
+ scan.start()
206
+ return true
207
+ }
208
+ return false
209
+ }
210
+ catch {
211
+ return false
212
+ }
213
+ })
214
+
215
+ ctx.registerRPC('stopScan', () => {
216
+ try {
217
+ const scan = getScanInstance()
218
+ if (scan) {
219
+ scan.stop()
220
+ return true
221
+ }
222
+ return false
223
+ }
224
+ catch {
225
+ return false
226
+ }
227
+ })
228
+
229
+ ctx.registerRPC('isScanActive', () => {
230
+ try {
231
+ const scan = getScanInstance()
232
+ return scan?.isActive() || false
233
+ }
234
+ catch {
235
+ return false
236
+ }
237
+ })
238
+ }
239
+ },
240
+
241
+ /**
242
+ * Plugin teardown
243
+ */
244
+ async teardown() {
245
+ if (scanInstance) {
246
+ scanInstance.stop()
247
+ scanInstance = null
248
+ }
249
+
250
+ resetScanInstance()
251
+ context = null
252
+ },
253
+
254
+ /**
255
+ * RPC methods exposed to other plugins
256
+ */
257
+ rpc: {
258
+ /**
259
+ * Get current scan options
260
+ */
261
+ getOptions: () => {
262
+ try {
263
+ const scan = getScanInstance()
264
+ return scan?.getOptions() || null
265
+ }
266
+ catch {
267
+ return null
268
+ }
269
+ },
270
+
271
+ /**
272
+ * Set scan options
273
+ */
274
+ setOptions: (options: Partial<ReactDevtoolsScanOptions>) => {
275
+ try {
276
+ const scan = getScanInstance()
277
+ if (scan) {
278
+ scan.setOptions(options)
279
+ return true
280
+ }
281
+ return false
282
+ }
283
+ catch {
284
+ return false
285
+ }
286
+ },
287
+
288
+ /**
289
+ * Start scan
290
+ */
291
+ start: () => {
292
+ try {
293
+ const scanInst = getScanInstance()
294
+ if (scanInst) {
295
+ scanInst.start()
296
+ return true
297
+ }
298
+ // Auto-initialize if not started
299
+ if (!scanInstance) {
300
+ scanInstance = getScanInstance(config)
301
+ scanInstance.start()
302
+ return true
303
+ }
304
+ return false
305
+ }
306
+ catch {
307
+ return false
308
+ }
309
+ },
310
+
311
+ /**
312
+ * Stop scan
313
+ */
314
+ stop: () => {
315
+ try {
316
+ const scan = getScanInstance()
317
+ if (scan) {
318
+ scan.stop()
319
+ return true
320
+ }
321
+ return false
322
+ }
323
+ catch {
324
+ return false
325
+ }
326
+ },
327
+
328
+ /**
329
+ * Check if scan is active
330
+ */
331
+ isActive: () => {
332
+ try {
333
+ const scan = getScanInstance()
334
+ return scan?.isActive() || false
335
+ }
336
+ catch {
337
+ return false
338
+ }
339
+ },
340
+
341
+ /**
342
+ * Hide the React Scan toolbar
343
+ */
344
+ hideToolbar: () => {
345
+ try {
346
+ const scan = getScanInstance()
347
+ if (scan) {
348
+ scan.hideToolbar()
349
+ return true
350
+ }
351
+ return false
352
+ }
353
+ catch {
354
+ return false
355
+ }
356
+ },
357
+
358
+ /**
359
+ * Show the React Scan toolbar
360
+ */
361
+ showToolbar: () => {
362
+ try {
363
+ const scan = getScanInstance()
364
+ if (scan) {
365
+ scan.showToolbar()
366
+ return true
367
+ }
368
+ return false
369
+ }
370
+ catch {
371
+ return false
372
+ }
373
+ },
374
+
375
+ /**
376
+ * Get toolbar visibility state
377
+ */
378
+ getToolbarVisibility: () => {
379
+ try {
380
+ const scan = getScanInstance()
381
+ return scan?.getToolbarVisibility() || false
382
+ }
383
+ catch {
384
+ return false
385
+ }
386
+ },
387
+
388
+ /**
389
+ * Get performance data for all components
390
+ */
391
+ getPerformanceData: () => {
392
+ try {
393
+ const scan = getScanInstance()
394
+ return scan?.getPerformanceData() || []
395
+ }
396
+ catch {
397
+ return []
398
+ }
399
+ },
400
+
401
+ /**
402
+ * Get aggregated performance summary
403
+ */
404
+ getPerformanceSummary: () => {
405
+ try {
406
+ const scan = getScanInstance()
407
+ if (!scan) {
408
+ return {
409
+ totalRenders: 0,
410
+ totalComponents: 0,
411
+ unnecessaryRenders: 0,
412
+ averageRenderTime: 0,
413
+ slowestComponents: [],
414
+ }
415
+ }
416
+ return scan.getPerformanceSummary()
417
+ }
418
+ catch {
419
+ return {
420
+ totalRenders: 0,
421
+ totalComponents: 0,
422
+ unnecessaryRenders: 0,
423
+ averageRenderTime: 0,
424
+ slowestComponents: [],
425
+ }
426
+ }
427
+ },
428
+
429
+ /**
430
+ * Clear all performance data
431
+ */
432
+ clearPerformanceData: () => {
433
+ try {
434
+ const scan = getScanInstance()
435
+ if (scan) {
436
+ scan.clearPerformanceData()
437
+ return true
438
+ }
439
+ return false
440
+ }
441
+ catch {
442
+ return false
443
+ }
444
+ },
445
+
446
+ /**
447
+ * Get current FPS
448
+ */
449
+ getFPS: () => {
450
+ try {
451
+ const scan = getScanInstance()
452
+ return scan?.getFPS() || 0
453
+ }
454
+ catch {
455
+ return 0
456
+ }
457
+ },
458
+
459
+ /**
460
+ * Start component inspection mode
461
+ */
462
+ startInspecting: () => {
463
+ try {
464
+ const scan = getScanInstance()
465
+ if (scan) {
466
+ scan.startInspecting()
467
+ return true
468
+ }
469
+ return false
470
+ }
471
+ catch {
472
+ return false
473
+ }
474
+ },
475
+
476
+ /**
477
+ * Stop component inspection mode
478
+ */
479
+ stopInspecting: () => {
480
+ try {
481
+ const scan = getScanInstance()
482
+ if (scan) {
483
+ scan.stopInspecting()
484
+ return true
485
+ }
486
+ return false
487
+ }
488
+ catch {
489
+ return false
490
+ }
491
+ },
492
+
493
+ /**
494
+ * Check if inspection mode is active
495
+ */
496
+ isInspecting: () => {
497
+ try {
498
+ const scan = getScanInstance()
499
+ return scan?.isInspecting() || false
500
+ }
501
+ catch {
502
+ return false
503
+ }
504
+ },
505
+
506
+ /**
507
+ * Focus on a specific component
508
+ */
509
+ focusComponent: (fiber: any) => {
510
+ try {
511
+ const scan = getScanInstance()
512
+ if (scan && fiber) {
513
+ scan.focusComponent(fiber)
514
+ return true
515
+ }
516
+ return false
517
+ }
518
+ catch {
519
+ return false
520
+ }
521
+ },
522
+
523
+ /**
524
+ * Get currently focused component
525
+ */
526
+ getFocusedComponent: () => {
527
+ try {
528
+ const scan = getScanInstance()
529
+ const component = scan?.getFocusedComponent() || null
530
+ if (component) {
531
+ // Sanitize for RPC - remove non-serializable fields
532
+ const { fiber, domElement, ...serializableComponent } = component as any
533
+ return serializableComponent
534
+ }
535
+ return null
536
+ }
537
+ catch {
538
+ return null
539
+ }
540
+ },
541
+
542
+ /**
543
+ * Get focused component render info with changes
544
+ */
545
+ getFocusedComponentRenderInfo: () => {
546
+ try {
547
+ const scan = getScanInstance()
548
+ return scan?.getFocusedComponentRenderInfo() || null
549
+ }
550
+ catch {
551
+ return null
552
+ }
553
+ },
554
+
555
+ /**
556
+ * Clear focused component changes
557
+ */
558
+ clearFocusedComponentChanges: () => {
559
+ try {
560
+ const scan = getScanInstance()
561
+ if (scan) {
562
+ scan.clearFocusedComponentChanges()
563
+ return true
564
+ }
565
+ return false
566
+ }
567
+ catch {
568
+ return false
569
+ }
570
+ },
571
+
572
+ /**
573
+ * Set focused component by name for render tracking
574
+ */
575
+ setFocusedComponentByName: (componentName: string) => {
576
+ try {
577
+ const scan = getScanInstance()
578
+ if (scan && componentName) {
579
+ scan.setFocusedComponentByName(componentName)
580
+ return true
581
+ }
582
+ return false
583
+ }
584
+ catch {
585
+ return false
586
+ }
587
+ },
588
+
589
+ /**
590
+ * Get the component tree with render counts
591
+ */
592
+ getComponentTree: () => {
593
+ try {
594
+ const scan = getScanInstance()
595
+ return scan?.getComponentTree() || []
596
+ }
597
+ catch {
598
+ return []
599
+ }
600
+ },
601
+
602
+ /**
603
+ * Clear component tree render counts
604
+ */
605
+ clearComponentTree: () => {
606
+ try {
607
+ const scan = getScanInstance()
608
+ if (scan) {
609
+ scan.clearComponentTree()
610
+ return true
611
+ }
612
+ return false
613
+ }
614
+ catch {
615
+ return false
616
+ }
617
+ },
618
+ },
619
+
620
+ /**
621
+ * Event handlers
622
+ */
623
+ on: {
624
+ 'component-mounted': (_event: any) => {
625
+ // React Scan automatically tracks component mounts
626
+ },
627
+
628
+ 'component-updated': (_event: any) => {
629
+ // React Scan automatically tracks component updates
630
+ },
631
+ },
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Default React Scan plugin instance
637
+ */
638
+ export const scanPlugin = createScanPlugin()