component-tracker.js•16.3 kB
export class ComponentTracker {
page = null;
componentCache = new Map();
domToComponentMap = new Map();
async initialize(page) {
this.page = page;
this.componentCache.clear();
this.domToComponentMap.clear();
// Inject tracking hooks into the page
await this.injectTrackingHooks();
}
async injectTrackingHooks() {
if (!this.page)
throw new Error('Page not initialized');
await this.page.evaluate(() => {
// Create global tracking object
window.__COMPONENT_TRACKER__ = {
components: new Map(),
domMap: new Map(),
renderCounts: new Map(),
lastRenderTimes: new Map(),
};
// Hook into React DevTools if available
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
const tracker = window.__COMPONENT_TRACKER__;
// Patch onCommitFiberRoot to track renders
const originalOnCommit = hook.onCommitFiberRoot;
hook.onCommitFiberRoot = function (...args) {
if (originalOnCommit) {
originalOnCommit.apply(this, args);
}
// Track render time
const root = args[1];
if (root && root.current) {
tracker.lastRenderTimes.set('react', performance.now());
}
};
}
// Hook into Vue 3 if available
if (window.Vue || window.app) {
const vue = window.Vue || window.app?.config?.globalProperties?.Vue;
if (vue && vue.version?.startsWith('3')) {
const tracker = window.__COMPONENT_TRACKER__;
// Track Vue 3 component updates
if (window.app?.__app_context__) {
tracker.lastRenderTimes.set('vue', performance.now());
}
}
}
// Hook into Angular if available
if (window.ng?.probe) {
const tracker = window.__COMPONENT_TRACKER__;
tracker.lastRenderTimes.set('angular', performance.now());
}
});
}
async getComponentTree() {
if (!this.page)
throw new Error('Page not initialized');
const startTime = performance.now();
const components = [];
// Extract React components
const reactComponents = await this.extractReactComponents();
components.push(...reactComponents);
// Extract Vue components
const vueComponents = await this.extractVueComponents();
components.push(...vueComponents);
// Extract Angular components
const angularComponents = await this.extractAngularComponents();
components.push(...angularComponents);
// Update cache
for (const comp of components) {
this.componentCache.set(comp.name, comp);
for (const domId of comp.domNodes) {
this.domToComponentMap.set(domId, comp.name);
}
}
const duration = performance.now() - startTime;
if (duration > 50) {
console.warn(`Component tree extraction took ${duration.toFixed(2)}ms (target: <50ms)`);
}
return components;
}
async extractReactComponents() {
if (!this.page)
return [];
return await this.page.evaluate(() => {
const components = [];
const tracker = window.__COMPONENT_TRACKER__;
try {
// Try to access React DevTools hook
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
if (!hook || !hook.renderers || hook.renderers.size === 0) {
return components;
}
// Iterate through React renderers
hook.renderers.forEach((renderer) => {
if (!renderer || !renderer.getCurrentFiber)
return;
// Find root fiber
const roots = hook.getFiberRoots?.(renderer.version) || [];
roots.forEach((root) => {
if (!root.current)
return;
const visitedFibers = new WeakSet();
const walkFiber = (fiber, parentName) => {
if (!fiber || visitedFibers.has(fiber))
return;
visitedFibers.add(fiber);
const compInfo = extractComponentFromFiber(fiber, parentName, tracker);
if (compInfo) {
components.push(compInfo);
// Walk children
let child = fiber.child;
while (child) {
walkFiber(child, compInfo.name);
child = child.sibling;
}
}
else if (fiber.child) {
walkFiber(fiber.child, parentName);
}
if (fiber.sibling) {
walkFiber(fiber.sibling, parentName);
}
};
walkFiber(root.current);
});
});
}
catch (e) {
console.warn('React component extraction error:', e);
}
function extractComponentFromFiber(fiber, parentName, tracker) {
try {
// Skip non-component fibers
if (!fiber.type || typeof fiber.type === 'string')
return null;
const name = getComponentName(fiber);
if (!name ||
name.startsWith('_') ||
name.startsWith('Provider') ||
name.startsWith('Consumer')) {
return null;
}
const domNodes = [];
const stateNode = fiber.stateNode;
if (stateNode && stateNode instanceof Element) {
const id = stateNode.id || `react-${Math.random().toString(36).substr(2, 9)}`;
if (!stateNode.id)
stateNode.id = id;
domNodes.push(id);
}
const source = {
file: 'unknown',
framework: 'react',
};
// Try to get source location from debug info
if (fiber._debugSource) {
source.file = fiber._debugSource.fileName || 'unknown';
source.line = fiber._debugSource.lineNumber;
source.column = fiber._debugSource.columnNumber;
}
else if (fiber.type._debugSource) {
source.file = fiber.type._debugSource.fileName || 'unknown';
source.line = fiber.type._debugSource.lineNumber;
source.column = fiber.type._debugSource.columnNumber;
}
const countKey = `react:${name}`;
const renderCount = tracker.renderCounts.get(countKey) || 0;
tracker.renderCounts.set(countKey, renderCount + 1);
return {
name,
type: 'react',
source,
props: fiber.memoizedProps || {},
state: fiber.memoizedState || undefined,
parent: parentName,
children: [],
domNodes,
renderCount: renderCount + 1,
lastRenderTime: tracker.lastRenderTimes.get('react') || 0,
};
}
catch (e) {
return null;
}
}
function getComponentName(fiber) {
if (!fiber || !fiber.type)
return 'Unknown';
if (typeof fiber.type === 'function') {
return fiber.type.displayName || fiber.type.name || 'Anonymous';
}
if (fiber.elementType && typeof fiber.elementType === 'function') {
return fiber.elementType.displayName || fiber.elementType.name || 'Anonymous';
}
return 'Unknown';
}
return components;
});
}
async extractVueComponents() {
if (!this.page)
return [];
return await this.page.evaluate(() => {
const components = [];
const tracker = window.__COMPONENT_TRACKER__;
try {
// Check for Vue 3
const app = window.app;
if (!app || !app._instance)
return components;
const visitedInstances = new WeakSet();
const walkInstance = (instance, parentName) => {
if (!instance || visitedInstances.has(instance))
return;
visitedInstances.add(instance);
const name = instance.type?.name || instance.type?.__name || 'Anonymous';
if (name.startsWith('_'))
return;
const domNodes = [];
if (instance.vnode?.el && instance.vnode.el instanceof Element) {
const id = instance.vnode.el.id || `vue-${Math.random().toString(36).substr(2, 9)}`;
if (!instance.vnode.el.id)
instance.vnode.el.id = id;
domNodes.push(id);
}
const source = {
file: instance.type?.__file || 'unknown',
framework: 'vue',
};
const countKey = `vue:${name}`;
const renderCount = tracker.renderCounts.get(countKey) || 0;
tracker.renderCounts.set(countKey, renderCount + 1);
components.push({
name,
type: 'vue',
source,
props: instance.props || {},
state: instance.data || instance.setupState || undefined,
parent: parentName,
children: [],
domNodes,
renderCount: renderCount + 1,
lastRenderTime: tracker.lastRenderTimes.get('vue') || 0,
});
// Walk children
const children = instance.subTree?.children || [];
if (Array.isArray(children)) {
children.forEach((child) => {
if (child && child.component) {
walkInstance(child.component, name);
}
});
}
};
walkInstance(app._instance);
}
catch (e) {
console.warn('Vue component extraction error:', e);
}
return components;
});
}
async extractAngularComponents() {
if (!this.page)
return [];
return await this.page.evaluate(() => {
const components = [];
const tracker = window.__COMPONENT_TRACKER__;
try {
const ng = window.ng;
if (!ng?.probe)
return components;
// Find all Angular components in the DOM
const allElements = document.querySelectorAll('*');
const visitedComponents = new Set();
allElements.forEach((el) => {
try {
const debugElement = ng.probe(el);
if (!debugElement || !debugElement.componentInstance)
return;
const instance = debugElement.componentInstance;
if (visitedComponents.has(instance))
return;
visitedComponents.add(instance);
const name = instance.constructor?.name || 'Anonymous';
if (name === 'Object' || name.startsWith('_'))
return;
const id = el.id || `ng-${Math.random().toString(36).substr(2, 9)}`;
if (!el.id)
el.id = id;
const source = {
file: 'unknown',
framework: 'angular',
};
const countKey = `angular:${name}`;
const renderCount = tracker.renderCounts.get(countKey) || 0;
tracker.renderCounts.set(countKey, renderCount + 1);
// Extract props (inputs) and state
const props = {};
const state = {};
for (const key in instance) {
if (instance.hasOwnProperty(key) && !key.startsWith('_')) {
const value = instance[key];
if (typeof value !== 'function') {
state[key] = value;
}
}
}
components.push({
name,
type: 'angular',
source,
props,
state,
parent: undefined,
children: [],
domNodes: [id],
renderCount: renderCount + 1,
lastRenderTime: tracker.lastRenderTimes.get('angular') || 0,
});
}
catch (e) {
// Skip elements that aren't Angular components
}
});
}
catch (e) {
console.warn('Angular component extraction error:', e);
}
return components;
});
}
async getComponentAtElement(selector) {
if (!this.page)
throw new Error('Page not initialized');
try {
// First, ensure we have the latest component tree
await this.getComponentTree();
// Get the element's ID or create one
const elementId = await this.page.evaluate((sel) => {
const el = document.querySelector(sel);
if (!el)
return null;
const htmlEl = el;
if (htmlEl.id)
return htmlEl.id;
const newId = `elem-${Math.random().toString(36).substr(2, 9)}`;
htmlEl.id = newId;
return newId;
}, selector);
if (!elementId)
return null;
// Look up the component in our map
const componentName = this.domToComponentMap.get(elementId);
if (!componentName)
return null;
return this.componentCache.get(componentName) || null;
}
catch (e) {
console.error('Error getting component at element:', e);
return null;
}
}
}
//# sourceMappingURL=component-tracker.js.map