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
JavaScript
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