UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

292 lines • 12.2 kB
export class ReactStateEngine { page; stateSnapshots = []; componentMap = new Map(); async attach(page) { this.page = page; // Inject React DevTools hook enhancer await page.addInitScript(() => { // Store original React DevTools hook const originalHook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; if (originalHook) { // Wrap the hook to capture component updates const wrappedHook = { ...originalHook, onCommitFiberRoot: (id, root, ...args) => { // Capture state changes window.__REACT_STATE_CHANGES__ = window.__REACT_STATE_CHANGES__ || []; window.__REACT_STATE_CHANGES__.push({ timestamp: Date.now(), rootId: id, root: root }); // Call original if exists if (originalHook.onCommitFiberRoot) { originalHook.onCommitFiberRoot(id, root, ...args); } } }; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = wrappedHook; } }); } async captureComponentTree() { if (!this.page) throw new Error('No page attached'); return await this.page.evaluate(() => { const components = []; const visited = new WeakSet(); // Find React root const findReactRoot = () => { // Try React 18+ root const root = document.querySelector('#root') || document.querySelector('[id^="__next"]') || document.body; const rootKey = Object.keys(root).find(key => key.startsWith('_reactRootContainer') || key.startsWith('__reactContainer')); return rootKey ? root[rootKey] : null; }; // Extract component info from fiber const extractComponentInfo = (fiber) => { if (!fiber || visited.has(fiber)) return null; visited.add(fiber); const isComponent = fiber.elementType && (typeof fiber.elementType === 'function' || (typeof fiber.elementType === 'object' && fiber.elementType.$$typeof)); if (!isComponent) return null; const component = { name: fiber.elementType.displayName || fiber.elementType.name || 'Anonymous', props: fiber.memoizedProps || {}, state: fiber.memoizedState, hooks: [], fiber: fiber, element: fiber.stateNode?.tagName?.toLowerCase() }; // Extract hooks for function components if (fiber.memoizedState && fiber.elementType && typeof fiber.elementType === 'function') { let hookState = fiber.memoizedState; const hooks = []; while (hookState) { hooks.push({ type: hookState.tag, value: hookState.memoizedState }); hookState = hookState.next; } component.hooks = hooks; } // Process children component.children = []; let child = fiber.child; while (child) { const childComponent = extractComponentInfo(child); if (childComponent) { component.children.push(childComponent); } child = child.sibling; } return component; }; // Start from React root const reactRoot = findReactRoot(); if (reactRoot && reactRoot._internalRoot) { const rootFiber = reactRoot._internalRoot.current; const rootComponent = extractComponentInfo(rootFiber); if (rootComponent) { components.push(rootComponent); } } return components; }); } async getCurrentState() { const components = await this.captureComponentTree(); const stateTree = await this.buildStateTree(components); const propFlows = await this.tracePropFlows(components); const snapshot = { timestamp: new Date(), components, stateTree, propFlows }; this.stateSnapshots.push(snapshot); return snapshot; } async buildStateTree(components) { const tree = {}; const processComponent = (comp, path) => { const fullPath = path ? `${path}.${comp.name}` : comp.name; // Store component state if (comp.state) { tree[fullPath] = { state: comp.state, props: comp.props, hooks: comp.hooks }; } // Process children if (comp.children) { comp.children.forEach(child => processComponent(child, fullPath)); } }; components.forEach(comp => processComponent(comp, '')); return tree; } async tracePropFlows(components) { const flows = []; const processComponent = (comp, parentPath = '') => { const currentPath = parentPath ? `${parentPath}.${comp.name}` : comp.name; // Check if props are passed to children if (comp.children && comp.props) { comp.children.forEach(child => { Object.keys(child.props || {}).forEach(propKey => { if (comp.props[propKey] !== undefined) { flows.push({ from: currentPath, to: `${currentPath}.${child.name}`, prop: propKey, value: child.props[propKey] }); } }); processComponent(child, currentPath); }); } }; components.forEach(comp => processComponent(comp)); return flows; } async detectStatePropMismatches(apiData) { const mismatches = []; const currentState = await this.getCurrentState(); // Helper to find data in component tree const findDataInComponents = (data, components) => { const matches = []; const searchComponent = (comp, path) => { // Check props if (comp.props) { Object.entries(comp.props).forEach(([key, value]) => { if (this.isDataMatch(value, data)) { matches.push({ component: comp.name, path: `${path}.props.${key}`, value: value, type: 'props' }); } }); } // Check state if (comp.state) { if (this.isDataMatch(comp.state, data)) { matches.push({ component: comp.name, path: `${path}.state`, value: comp.state, type: 'state' }); } } // Check hooks if (comp.hooks) { comp.hooks.forEach((hook, i) => { if (this.isDataMatch(hook.value, data)) { matches.push({ component: comp.name, path: `${path}.hooks[${i}]`, value: hook.value, type: 'hook' }); } }); } // Search children if (comp.children) { comp.children.forEach(child => searchComponent(child, `${path}.${child.name}`)); } }; components.forEach(comp => searchComponent(comp, comp.name)); return matches; }; // Compare API data with component state if (apiData && typeof apiData === 'object') { const componentMatches = findDataInComponents(apiData, currentState.components); // Check for mismatches if (Array.isArray(apiData)) { const displayedCount = componentMatches.filter(m => Array.isArray(m.value) && m.value.length !== apiData.length); displayedCount.forEach(match => { mismatches.push({ type: 'count_mismatch', component: match.component, path: match.path, apiCount: apiData.length, displayedCount: match.value.length, message: `Component ${match.component} shows ${match.value.length} items but API returned ${apiData.length}` }); }); } } return mismatches; } isDataMatch(componentData, apiData) { // Simple matching logic - can be enhanced if (componentData === apiData) return true; if (Array.isArray(componentData) && Array.isArray(apiData)) { // Check if arrays have similar structure if (componentData.length > 0 && apiData.length > 0) { const firstCompItem = componentData[0]; const firstApiItem = apiData[0]; if (typeof firstCompItem === 'object' && typeof firstApiItem === 'object') { // Check if they have similar keys const compKeys = Object.keys(firstCompItem); const apiKeys = Object.keys(firstApiItem); const commonKeys = compKeys.filter(k => apiKeys.includes(k)); return commonKeys.length > Math.min(compKeys.length, apiKeys.length) * 0.5; } } } return false; } async getComponentByName(name) { const components = await this.captureComponentTree(); const findComponent = (comps) => { for (const comp of comps) { if (comp.name === name) return comp; if (comp.children) { const found = findComponent(comp.children); if (found) return found; } } return null; }; return findComponent(components); } async monitorStateChanges(duration) { if (!this.page) throw new Error('No page attached'); const startSnapshot = await this.getCurrentState(); await this.page.waitForTimeout(duration); const endSnapshot = await this.getCurrentState(); // Compare snapshots to find changes const changes = this.compareSnapshots(startSnapshot, endSnapshot); return changes; } compareSnapshots(before, after) { const changes = []; // Simple comparison - can be enhanced const beforeTree = JSON.stringify(before.stateTree); const afterTree = JSON.stringify(after.stateTree); if (beforeTree !== afterTree) { changes.push({ type: 'state_change', before: before.stateTree, after: after.stateTree, timestamp: after.timestamp }); } return changes; } } //# sourceMappingURL=react-state-engine.js.map