|
2 | 2 | "use strict"; |
3 | 3 |
|
4 | 4 | class FrameworkDetector { |
5 | | - getComponentInfo(element) { |
6 | | - const reactInfo = this.getReactInfo(element); |
7 | | - if (reactInfo) return { ...reactInfo, framework: "React" }; |
| 5 | + detectFramework() { |
| 6 | + if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) { |
| 7 | + return "React"; |
| 8 | + } |
| 9 | + if (window.ng) { |
| 10 | + return "Angular"; |
| 11 | + } |
| 12 | + return null; |
| 13 | + } |
8 | 14 |
|
9 | | - const angularInfo = this.getAngularInfo(element); |
10 | | - if (angularInfo) return { ...angularInfo, framework: "Angular" }; |
| 15 | + getComponentInfo(element) { |
| 16 | + const framework = this.detectFramework(); |
| 17 | + |
| 18 | + if (framework === "React") { |
| 19 | + const reactInfo = this.getReactInfo(element); |
| 20 | + if (reactInfo) return { ...reactInfo, framework: "React" }; |
| 21 | + } else if (framework === "Angular") { |
| 22 | + const angularInfo = this.getAngularInfo(element); |
| 23 | + if (angularInfo) return { ...angularInfo, framework: "Angular" }; |
| 24 | + } |
11 | 25 |
|
12 | 26 | return null; |
13 | 27 | } |
|
19 | 33 |
|
20 | 34 | if (!fiberKey) return null; |
21 | 35 |
|
22 | | - const fiber = element[fiberKey]; |
23 | | - if (!fiber?._debugOwner?.elementType) return null; |
24 | | - |
25 | | - const componentType = fiber._debugOwner.elementType; |
26 | | - const name = |
27 | | - componentType.name || |
28 | | - componentType.displayName || |
29 | | - fiber._debugOwner.type?.name; |
30 | | - |
31 | | - if (!name || this.isReactInternal(name)) return null; |
32 | | - |
33 | | - let fileName = ""; |
34 | | - let lineNumber = ""; |
35 | | - |
36 | | - if (fiber._debugSource) { |
37 | | - fileName = fiber._debugSource.fileName; |
38 | | - lineNumber = fiber._debugSource.lineNumber; |
39 | | - } else if (fiber._debugOwner._debugSource) { |
40 | | - fileName = fiber._debugOwner._debugSource.fileName; |
41 | | - lineNumber = fiber._debugOwner._debugSource.lineNumber; |
42 | | - } else if (fiber._debugOwner._debugStack) { |
43 | | - const stack = fiber._debugOwner._debugStack.stack; |
44 | | - if (stack) { |
45 | | - const lines = stack.split("\n"); |
46 | | - for (const line of lines) { |
47 | | - let match = line.match(/at\s+\w+\s+\(([^)]+):(\d+):\d+\)/); |
48 | | - if (!match) { |
49 | | - match = line.match(/at\s+(.+):(\d+):(\d+)$/); |
50 | | - } |
51 | | - if ( |
52 | | - match && |
53 | | - (match[1].includes(".jsx") || match[1].includes(".tsx")) |
54 | | - ) { |
55 | | - const fullPath = match[1]; |
56 | | - fileName = fullPath.split("/").pop(); |
57 | | - lineNumber = match[2]; |
58 | | - break; |
59 | | - } |
| 36 | + let fiber = element[fiberKey]; |
| 37 | + if (!fiber) return null; |
| 38 | + |
| 39 | + while (fiber) { |
| 40 | + const name = fiber.type?.name || fiber.type?.displayName; |
| 41 | + if (name && !this.isReactInternal(name)) { |
| 42 | + break; |
| 43 | + } |
| 44 | + fiber = fiber._debugOwner; |
| 45 | + if (!fiber) return null; |
| 46 | + } |
| 47 | + |
| 48 | + const name = fiber.type?.name || fiber.type?.displayName; |
| 49 | + if (!name) return null; |
| 50 | + |
| 51 | + const sources = []; |
| 52 | + const functionsToLocate = []; |
| 53 | + let currentFiber = fiber; |
| 54 | + let finalName = name; |
| 55 | + let finalProps = fiber.memoizedProps; |
| 56 | + |
| 57 | + while (currentFiber) { |
| 58 | + if (currentFiber._debugSource) { |
| 59 | + if (sources.length === 0) { |
| 60 | + finalName = |
| 61 | + currentFiber.type?.name || |
| 62 | + currentFiber.type?.displayName || |
| 63 | + finalName; |
| 64 | + finalProps = currentFiber.memoizedProps || finalProps; |
60 | 65 | } |
| 66 | + sources.push({ |
| 67 | + fileName: currentFiber._debugSource.fileName, |
| 68 | + lineNumber: currentFiber._debugSource.lineNumber, |
| 69 | + }); |
| 70 | + } else if ( |
| 71 | + currentFiber.type && |
| 72 | + typeof currentFiber.type === "function" |
| 73 | + ) { |
| 74 | + functionsToLocate.push(currentFiber.type); |
61 | 75 | } |
62 | | - } else { |
63 | | - fileName = "not available"; |
| 76 | + |
| 77 | + currentFiber = currentFiber._debugOwner; |
64 | 78 | } |
65 | 79 |
|
| 80 | + const files = sources.map((s) => `${s.fileName}:${s.lineNumber}`); |
| 81 | + |
66 | 82 | return { |
67 | | - name, |
68 | | - props: fiber._debugOwner.memoizedProps, |
69 | | - file: |
70 | | - fileName && lineNumber |
71 | | - ? `${fileName}:${lineNumber}` |
72 | | - : fileName, |
73 | | - needsSourceDetection: !fileName || !lineNumber, |
| 83 | + name: finalName, |
| 84 | + props: finalProps, |
| 85 | + files: files.length > 0 ? files : null, |
| 86 | + functionsToLocate: functionsToLocate, |
| 87 | + needsSourceDetection: sources.length === 0, |
74 | 88 | elementId: element.getAttribute("data-claude-devtools-id"), |
75 | 89 | }; |
76 | 90 | } |
77 | 91 |
|
78 | 92 | getAngularInfo(element) { |
79 | | - if (window.ng?.getOwningComponent) { |
80 | | - const component = window.ng.getOwningComponent(element); |
81 | | - if (component) { |
82 | | - const props = {}; |
83 | | - |
84 | | - for (const key in component) { |
85 | | - if ( |
86 | | - component.hasOwnProperty(key) && |
87 | | - !key.startsWith("_") && |
88 | | - !key.startsWith("ng") |
89 | | - ) { |
90 | | - const value = component[key]; |
91 | | - if (typeof value !== "function" && typeof value !== "undefined") { |
92 | | - props[key] = value; |
93 | | - } |
94 | | - } |
95 | | - } |
| 93 | + if (!window.ng?.getOwningComponent) return null; |
| 94 | + |
| 95 | + let currentComponent = window.ng.getOwningComponent(element); |
| 96 | + if (!currentComponent) return null; |
96 | 97 |
|
97 | | - return { |
98 | | - name: component.constructor.name.startsWith("_") |
99 | | - ? component.constructor.name.substring(1) |
100 | | - : component.constructor.name, |
101 | | - props: Object.keys(props).length > 0 ? props : null, |
102 | | - file: "not available", |
103 | | - needsSourceDetection: true, |
104 | | - elementId: element.getAttribute("data-claude-devtools-id"), |
105 | | - }; |
| 98 | + const functionsToLocate = []; |
| 99 | + let finalName = null; |
| 100 | + let finalProps = null; |
| 101 | + |
| 102 | + while (currentComponent) { |
| 103 | + if (!finalName) { |
| 104 | + finalName = currentComponent.constructor.name.startsWith("_") |
| 105 | + ? currentComponent.constructor.name.substring(1) |
| 106 | + : currentComponent.constructor.name; |
| 107 | + finalProps = currentComponent; |
| 108 | + } |
| 109 | + |
| 110 | + functionsToLocate.push(currentComponent.constructor); |
| 111 | + |
| 112 | + try { |
| 113 | + currentComponent = window.ng.getOwningComponent(currentComponent); |
| 114 | + } catch { |
| 115 | + break; |
106 | 116 | } |
107 | 117 | } |
108 | 118 |
|
109 | | - return null; |
| 119 | + return { |
| 120 | + name: finalName, |
| 121 | + props: finalProps, |
| 122 | + files: null, |
| 123 | + functionsToLocate: functionsToLocate, |
| 124 | + needsSourceDetection: true, |
| 125 | + elementId: element.getAttribute("data-claude-devtools-id"), |
| 126 | + }; |
110 | 127 | } |
111 | 128 |
|
112 | 129 | isReactInternal(name) { |
113 | 130 | const internals = ["Fragment", "StrictMode", "Profiler", "Suspense"]; |
114 | | - return internals.includes(name) || name.startsWith("React."); |
| 131 | + return ( |
| 132 | + internals.includes(name) || |
| 133 | + name.startsWith("React.") || |
| 134 | + name.startsWith("Primitive.") |
| 135 | + ); |
115 | 136 | } |
116 | 137 | } |
117 | 138 |
|
|
134 | 155 | ); |
135 | 156 | const componentInfo = element ? detector.getComponentInfo(element) : null; |
136 | 157 |
|
| 158 | + if (componentInfo?.functionsToLocate) { |
| 159 | + window.__claudeDevToolsFunctionsToLocate = |
| 160 | + componentInfo.functionsToLocate; |
| 161 | + } |
| 162 | + |
137 | 163 | const sanitizedInfo = componentInfo |
138 | 164 | ? { |
139 | 165 | name: componentInfo.name, |
140 | 166 | framework: componentInfo.framework, |
141 | | - file: componentInfo.file, |
| 167 | + files: componentInfo.files, |
142 | 168 | props: sanitizeProps(componentInfo.props), |
143 | 169 | needsSourceDetection: componentInfo.needsSourceDetection, |
144 | 170 | } |
|
0 commit comments