/** * React Scan Plugin for React DevTools * * This plugin integrates React Scan into the React DevTools plugin system, * providing performance monitoring and analysis capabilities. */ import type { ReactDevtoolsScanOptions, ScanInstance } from './types' import { getDisplayName, getFiberId } from 'bippy' import { getScanInstance, resetScanInstance } from './scan-facade' const STORAGE_KEY = 'react-scan-options' function getPersistedEnabledState(): boolean | null { if (typeof window === 'undefined') { return null } try { const stored = localStorage.getItem(STORAGE_KEY) if (!stored) { return null } const parsed = JSON.parse(stored) return typeof parsed?.enabled === 'boolean' ? parsed.enabled : null } catch { return null } } /** * React Scan plugin configuration */ export interface ScanPluginConfig extends ReactDevtoolsScanOptions { /** * Whether to auto-start scan on plugin load * @default true */ autoStart?: boolean } /** * Create React Scan plugin * * @param config - Plugin configuration * @returns DevTools plugin instance * * @example * ```typescript * import { createScanPlugin } from '@react-devtools-plus/scan/plugin'; * * const scanPlugin = createScanPlugin({ * enabled: true, * showToolbar: true, * autoStart: true, * }); * ``` */ export function createScanPlugin(config: ScanPluginConfig = {}): any { let scanInstance: ScanInstance | null = null let context: any = null const { autoStart = true, ...scanOptions } = config // Event emitter for plugin events const eventHandlers: Map void>> = new Map() const emit = (eventName: string, data: any) => { const handlers = eventHandlers.get(eventName) if (handlers) { handlers.forEach((handler) => { if (typeof handler === 'function') { try { handler(data) } catch { // Ignore event handler errors } } }) } } const subscribe = (eventName: string, handler: (data: any) => void) => { if (!eventHandlers.has(eventName)) { eventHandlers.set(eventName, new Set()) } eventHandlers.get(eventName)!.add(handler) // Return unsubscribe function return () => { const handlers = eventHandlers.get(eventName) if (handlers) { handlers.delete(handler) } } } return { id: 'react-scan', name: 'React Scan', description: 'Performance monitoring and analysis for React applications', version: '1.0.0', // Expose subscribe method for event subscriptions subscribe, /** * Plugin setup */ async setup(ctx: any) { context = ctx scanInstance = getScanInstance({ ...scanOptions }) const shouldStart = typeof scanOptions.enabled === 'boolean' ? scanOptions.enabled : getPersistedEnabledState() ?? scanInstance.isActive() if (autoStart && shouldStart) { scanInstance.start() } // Set up inspect state change listener try { const scan = getScanInstance() if (scan) { // Track the last hovered component during inspection let lastInspectedComponent: { componentName: string, componentId?: string } | null = null scan.onInspectStateChange((state: any) => { // Emit inspect state change event // Sanitize state for RPC const sanitizedState = { kind: state.kind, // Include component name if available componentName: state.fiber ? (state.fiber.type?.displayName || state.fiber.type?.name || 'Unknown') : undefined, } emit('inspect-state-changed', sanitizedState) // Track component during inspecting state - save hovered component info if (state.kind === 'inspecting' && state.fiber) { const componentName = getDisplayName(state.fiber.type) || 'Unknown' const componentId = String(getFiberId(state.fiber)) lastInspectedComponent = { componentName, componentId } } // If a component is focused, emit focused component info and set up tracking if (state.kind === 'focused') { const focusedComponent = scan.getFocusedComponent() if (focusedComponent) { // Sanitize for RPC - remove non-serializable fields const { fiber, domElement, ...serializableComponent } = focusedComponent as any emit('component-focused', serializableComponent) // Set up render tracking scan.setFocusedComponentByName(focusedComponent.componentName) } } // When inspection ends (inspect-off), emit the last inspected component if (state.kind === 'inspect-off' && lastInspectedComponent) { emit('component-focused', lastInspectedComponent) scan.setFocusedComponentByName(lastInspectedComponent.componentName) lastInspectedComponent = null } }) // Subscribe to focused component render changes scan.onFocusedComponentChange((info) => { emit('focused-component-render', info) }) } } catch { // Ignore errors setting up inspect state listener } // Listen for component tree changes if context supports it // Listen to component tree changes if supported if (ctx.on) { ctx.on('component-tree-changed', (_event: any) => { // Component tree changed, could update UI here }) } // Register RPC functions if context supports it if (ctx.registerRPC) { ctx.registerRPC('getScanOptions', () => { try { const scan = getScanInstance() return scan?.getOptions() || null } catch { return null } }) ctx.registerRPC('setScanOptions', (options: Partial) => { try { const scan = getScanInstance() if (scan) { scan.setOptions(options) return true } return false } catch { return false } }) ctx.registerRPC('startScan', () => { try { const scan = getScanInstance() if (scan) { scan.start() return true } return false } catch { return false } }) ctx.registerRPC('stopScan', () => { try { const scan = getScanInstance() if (scan) { scan.stop() return true } return false } catch { return false } }) ctx.registerRPC('isScanActive', () => { try { const scan = getScanInstance() return scan?.isActive() || false } catch { return false } }) } }, /** * Plugin teardown */ async teardown() { if (scanInstance) { scanInstance.stop() scanInstance = null } resetScanInstance() context = null }, /** * RPC methods exposed to other plugins */ rpc: { /** * Get current scan options */ getOptions: () => { try { const scan = getScanInstance() return scan?.getOptions() || null } catch { return null } }, /** * Set scan options */ setOptions: (options: Partial) => { try { const scan = getScanInstance() if (scan) { scan.setOptions(options) return true } return false } catch { return false } }, /** * Start scan */ start: () => { try { const scanInst = getScanInstance() if (scanInst) { scanInst.start() return true } // Auto-initialize if not started if (!scanInstance) { scanInstance = getScanInstance(config) scanInstance.start() return true } return false } catch { return false } }, /** * Stop scan */ stop: () => { try { const scan = getScanInstance() if (scan) { scan.stop() return true } return false } catch { return false } }, /** * Check if scan is active */ isActive: () => { try { const scan = getScanInstance() return scan?.isActive() || false } catch { return false } }, /** * Hide the React Scan toolbar */ hideToolbar: () => { try { const scan = getScanInstance() if (scan) { scan.hideToolbar() return true } return false } catch { return false } }, /** * Show the React Scan toolbar */ showToolbar: () => { try { const scan = getScanInstance() if (scan) { scan.showToolbar() return true } return false } catch { return false } }, /** * Get toolbar visibility state */ getToolbarVisibility: () => { try { const scan = getScanInstance() return scan?.getToolbarVisibility() || false } catch { return false } }, /** * Get performance data for all components */ getPerformanceData: () => { try { const scan = getScanInstance() return scan?.getPerformanceData() || [] } catch { return [] } }, /** * Get aggregated performance summary */ getPerformanceSummary: () => { try { const scan = getScanInstance() if (!scan) { return { totalRenders: 0, totalComponents: 0, unnecessaryRenders: 0, averageRenderTime: 0, slowestComponents: [], } } return scan.getPerformanceSummary() } catch { return { totalRenders: 0, totalComponents: 0, unnecessaryRenders: 0, averageRenderTime: 0, slowestComponents: [], } } }, /** * Clear all performance data */ clearPerformanceData: () => { try { const scan = getScanInstance() if (scan) { scan.clearPerformanceData() return true } return false } catch { return false } }, /** * Get current FPS */ getFPS: () => { try { const scan = getScanInstance() return scan?.getFPS() || 0 } catch { return 0 } }, /** * Start component inspection mode */ startInspecting: () => { try { const scan = getScanInstance() if (scan) { scan.startInspecting() return true } return false } catch { return false } }, /** * Stop component inspection mode */ stopInspecting: () => { try { const scan = getScanInstance() if (scan) { scan.stopInspecting() return true } return false } catch { return false } }, /** * Check if inspection mode is active */ isInspecting: () => { try { const scan = getScanInstance() return scan?.isInspecting() || false } catch { return false } }, /** * Focus on a specific component */ focusComponent: (fiber: any) => { try { const scan = getScanInstance() if (scan && fiber) { scan.focusComponent(fiber) return true } return false } catch { return false } }, /** * Get currently focused component */ getFocusedComponent: () => { try { const scan = getScanInstance() const component = scan?.getFocusedComponent() || null if (component) { // Sanitize for RPC - remove non-serializable fields const { fiber, domElement, ...serializableComponent } = component as any return serializableComponent } return null } catch { return null } }, /** * Get focused component render info with changes */ getFocusedComponentRenderInfo: () => { try { const scan = getScanInstance() return scan?.getFocusedComponentRenderInfo() || null } catch { return null } }, /** * Clear focused component changes */ clearFocusedComponentChanges: () => { try { const scan = getScanInstance() if (scan) { scan.clearFocusedComponentChanges() return true } return false } catch { return false } }, /** * Set focused component by name for render tracking */ setFocusedComponentByName: (componentName: string) => { try { const scan = getScanInstance() if (scan && componentName) { scan.setFocusedComponentByName(componentName) return true } return false } catch { return false } }, /** * Get the component tree with render counts */ getComponentTree: () => { try { const scan = getScanInstance() return scan?.getComponentTree() || [] } catch { return [] } }, /** * Clear component tree render counts */ clearComponentTree: () => { try { const scan = getScanInstance() if (scan) { scan.clearComponentTree() return true } return false } catch { return false } }, }, /** * Event handlers */ on: { 'component-mounted': (_event: any) => { // React Scan automatically tracks component mounts }, 'component-updated': (_event: any) => { // React Scan automatically tracks component updates }, }, } } /** * Default React Scan plugin instance */ export const scanPlugin = createScanPlugin()