|
1 | 1 | // Disable ESLint rule for underscore dangle usage in this file (React internals) |
2 | 2 | /* eslint-disable no-underscore-dangle */ |
3 | 3 |
|
4 | | -import { |
5 | | - ComponentInfo, CSSProperties, ElementPosition, TargetedElement, RawPointedDOMElement, |
6 | | -} from '@mcp-pointer/shared/types'; |
| 4 | +import { RawPointedDOMElement } from '@mcp-pointer/shared/types'; |
7 | 5 | import logger from './logger'; |
8 | 6 |
|
9 | | -export interface ReactSourceInfo { |
10 | | - fileName: string; |
11 | | - lineNumber?: number; |
12 | | - columnNumber?: number; |
13 | | -} |
14 | | - |
15 | | -/** |
16 | | - * Get source file information from a DOM element's React component |
17 | | - */ |
18 | | -export function getSourceFromElement(element: HTMLElement): ReactSourceInfo | null { |
19 | | - // Find React Fiber key |
20 | | - const fiberKey = Object.keys(element).find((key) => key.startsWith('__reactFiber$') |
21 | | - || key.startsWith('__reactInternalInstance$')); |
22 | | - |
23 | | - if (!fiberKey) return null; |
24 | | - |
25 | | - const fiber = (element as any)[fiberKey]; |
26 | | - if (!fiber) return null; |
27 | | - |
28 | | - // Walk up fiber tree to find component fiber (skip DOM fibers) |
29 | | - let componentFiber = fiber; |
30 | | - while (componentFiber && typeof componentFiber.type === 'string') { |
31 | | - componentFiber = componentFiber.return; |
32 | | - } |
33 | | - |
34 | | - if (!componentFiber) return null; |
35 | | - |
36 | | - // Try multiple source locations (React version differences) |
37 | | - // React 18: _debugSource |
38 | | - if (componentFiber._debugSource) { |
39 | | - return { |
40 | | - fileName: componentFiber._debugSource.fileName, |
41 | | - lineNumber: componentFiber._debugSource.lineNumber, |
42 | | - columnNumber: componentFiber._debugSource.columnNumber, |
43 | | - }; |
44 | | - } |
45 | | - |
46 | | - // React 19: _debugInfo (often null) |
47 | | - if (componentFiber._debugInfo) { |
48 | | - return componentFiber._debugInfo; |
49 | | - } |
50 | | - |
51 | | - // Babel plugin: __source on element type |
52 | | - if (componentFiber.elementType?.__source) { |
53 | | - return { |
54 | | - fileName: componentFiber.elementType.__source.fileName, |
55 | | - lineNumber: componentFiber.elementType.__source.lineNumber, |
56 | | - columnNumber: componentFiber.elementType.__source.columnNumber, |
57 | | - }; |
58 | | - } |
59 | | - |
60 | | - // Alternative: _owner chain |
61 | | - if (componentFiber._debugOwner?._debugSource) { |
62 | | - return { |
63 | | - fileName: componentFiber._debugOwner._debugSource.fileName, |
64 | | - lineNumber: componentFiber._debugOwner._debugSource.lineNumber, |
65 | | - columnNumber: componentFiber._debugOwner._debugSource.columnNumber, |
66 | | - }; |
67 | | - } |
68 | | - |
69 | | - // Check pendingProps for __source |
70 | | - if (componentFiber.pendingProps?.__source) { |
71 | | - return { |
72 | | - fileName: componentFiber.pendingProps.__source.fileName, |
73 | | - lineNumber: componentFiber.pendingProps.__source.lineNumber, |
74 | | - columnNumber: componentFiber.pendingProps.__source.columnNumber, |
75 | | - }; |
76 | | - } |
77 | | - |
78 | | - return null; |
79 | | -} |
80 | | - |
81 | | -/** |
82 | | - * Extract React Fiber information from an element |
83 | | - */ |
84 | | -export function getReactFiberInfo(element: HTMLElement): ComponentInfo | undefined { |
85 | | - try { |
86 | | - // Use comprehensive source detection |
87 | | - const sourceInfo = getSourceFromElement(element); |
88 | | - |
89 | | - // Also get component name |
90 | | - const fiberKey = Object.keys(element).find((key) => key.startsWith('__reactFiber$') |
91 | | - || key.startsWith('__reactInternalInstance$')); |
92 | | - |
93 | | - if (fiberKey) { |
94 | | - const fiber = (element as any)[fiberKey]; |
95 | | - if (fiber) { |
96 | | - // Find component fiber |
97 | | - let componentFiber = fiber; |
98 | | - while (componentFiber && typeof componentFiber.type === 'string') { |
99 | | - componentFiber = componentFiber.return; |
100 | | - } |
101 | | - |
102 | | - if (componentFiber && componentFiber.type && typeof componentFiber.type === 'function') { |
103 | | - const componentName = componentFiber.type.displayName |
104 | | - || componentFiber.type.name |
105 | | - || 'Unknown'; |
106 | | - |
107 | | - let sourceFile: string | undefined; |
108 | | - if (sourceInfo) { |
109 | | - const fileName = sourceInfo.fileName.split('/').pop() || sourceInfo.fileName; |
110 | | - sourceFile = sourceInfo.lineNumber |
111 | | - ? `${fileName}:${sourceInfo.lineNumber}` |
112 | | - : fileName; |
113 | | - } |
114 | | - |
115 | | - const result = { |
116 | | - name: componentName, |
117 | | - sourceFile, |
118 | | - framework: 'react' as const, |
119 | | - }; |
120 | | - |
121 | | - logger.debug('🧬 Found React Fiber info:', result); |
122 | | - return result; |
123 | | - } |
124 | | - } |
125 | | - } |
126 | | - |
127 | | - return undefined; |
128 | | - } catch (error) { |
129 | | - logger.error('🚨 Error extracting Fiber info:', error); |
130 | | - return undefined; |
131 | | - } |
132 | | -} |
133 | | - |
134 | | -/** |
135 | | - * Extract all attributes from an HTML element |
136 | | - */ |
137 | | -export function getElementAttributes(element: HTMLElement): Record<string, string> { |
138 | | - const attributes: Record<string, string> = {}; |
139 | | - for (let i = 0; i < element.attributes.length; i += 1) { |
140 | | - const attr = element.attributes[i]; |
141 | | - attributes[attr.name] = attr.value; |
142 | | - } |
143 | | - return attributes; |
144 | | -} |
145 | | - |
146 | | -/** |
147 | | - * Generate a CSS selector for an element |
148 | | - */ |
149 | | -export function generateSelector(element: HTMLElement): string { |
150 | | - let selector = element.tagName.toLowerCase(); |
151 | | - if (element.id) selector += `#${element.id}`; |
152 | | - if (element.className) { |
153 | | - const classNameStr = typeof element.className === 'string' |
154 | | - ? element.className |
155 | | - : (element.className as any).baseVal || ''; |
156 | | - const classes = classNameStr.split(' ').filter((c: string) => c.trim()); |
157 | | - if (classes.length > 0) selector += `.${classes.join('.')}`; |
158 | | - } |
159 | | - return selector; |
160 | | -} |
161 | | - |
162 | | -/** |
163 | | - * Get element position relative to the page |
164 | | - */ |
165 | | -export function getElementPosition(element: HTMLElement): ElementPosition { |
166 | | - const rect = element.getBoundingClientRect(); |
167 | | - return { |
168 | | - x: rect.left + window.scrollX, |
169 | | - y: rect.top + window.scrollY, |
170 | | - width: rect.width, |
171 | | - height: rect.height, |
172 | | - }; |
173 | | -} |
174 | | - |
175 | | -/** |
176 | | - * Extract relevant CSS properties from an element |
177 | | - */ |
178 | | -export function getElementCSSProperties(element: HTMLElement): CSSProperties { |
179 | | - const computedStyle = window.getComputedStyle(element); |
180 | | - return { |
181 | | - display: computedStyle.display, |
182 | | - position: computedStyle.position, |
183 | | - fontSize: computedStyle.fontSize, |
184 | | - color: computedStyle.color, |
185 | | - backgroundColor: computedStyle.backgroundColor, |
186 | | - }; |
187 | | -} |
188 | | - |
189 | | -/** |
190 | | - * Extract CSS classes from an element as an array |
191 | | - */ |
192 | | -export function getElementClasses(element: HTMLElement): string[] { |
193 | | - if (!element.className) return []; |
194 | | - const classNameStr = typeof element.className === 'string' |
195 | | - ? element.className |
196 | | - : (element.className as any).baseVal || ''; |
197 | | - return classNameStr.split(' ').filter((c: string) => c.trim()); |
198 | | -} |
199 | | - |
200 | | -export function adaptTargetToElement(element: HTMLElement): TargetedElement { |
201 | | - return { |
202 | | - selector: generateSelector(element), |
203 | | - tagName: element.tagName, |
204 | | - id: element.id || undefined, |
205 | | - classes: getElementClasses(element), |
206 | | - innerText: element.innerText || element.textContent || '', |
207 | | - attributes: getElementAttributes(element), |
208 | | - position: getElementPosition(element), |
209 | | - cssProperties: getElementCSSProperties(element), |
210 | | - componentInfo: getReactFiberInfo(element), |
211 | | - timestamp: Date.now(), |
212 | | - url: window.location.href, |
213 | | - }; |
214 | | -} |
215 | | - |
216 | 7 | /** |
217 | 8 | * Extract raw React Fiber from an element (if present) |
218 | 9 | */ |
|
0 commit comments