browser-devtools-mcp 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +93 -21
- package/README.md +93 -16
- package/dist/cli.js +53 -1721
- package/dist/core.js +764 -0
- package/dist/daemon-server.js +1 -338
- package/dist/index.d.ts +2 -0
- package/dist/index.js +210 -80
- package/dist/otel/otel-initializer.bundle.js +0 -1
- package/dist/tools/a11y/index.d.ts +2 -0
- package/dist/tools/content/index.d.ts +2 -0
- package/dist/tools/debug/index.d.ts +2 -0
- package/dist/tools/figma/compare/index.d.ts +56 -0
- package/dist/tools/figma/compare/types.d.ts +15 -0
- package/dist/tools/figma/index.d.ts +2 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/interaction/index.d.ts +2 -0
- package/dist/tools/navigation/index.d.ts +2 -0
- package/dist/tools/o11y/index.d.ts +2 -0
- package/dist/tools/react/index.d.ts +2 -0
- package/dist/tools/run/index.d.ts +2 -0
- package/dist/tools/stub/index.d.ts +2 -0
- package/dist/tools/sync/index.d.ts +2 -0
- package/dist/tools/types.d.ts +21 -0
- package/dist/types.d.ts +78 -0
- package/package.json +11 -11
- package/dist/browser.js +0 -213
- package/dist/browser.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config.js +0 -96
- package/dist/config.js.map +0 -1
- package/dist/context.js +0 -298
- package/dist/context.js.map +0 -1
- package/dist/daemon-server.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/logger.js +0 -97
- package/dist/logger.js.map +0 -1
- package/dist/mcp-server.js +0 -401
- package/dist/mcp-server.js.map +0 -1
- package/dist/otel/otel-controller.js +0 -323
- package/dist/otel/otel-controller.js.map +0 -1
- package/dist/otel/otel-initializer.bundle.js.map +0 -7
- package/dist/otel/otel-proxy.js +0 -407
- package/dist/otel/otel-proxy.js.map +0 -1
- package/dist/server-info.js +0 -186
- package/dist/server-info.js.map +0 -1
- package/dist/tools/a11y/index.js +0 -7
- package/dist/tools/a11y/index.js.map +0 -1
- package/dist/tools/a11y/take-aria-snapshot.js +0 -54
- package/dist/tools/a11y/take-aria-snapshot.js.map +0 -1
- package/dist/tools/a11y/take-ax-tree-snapshot.js +0 -850
- package/dist/tools/a11y/take-ax-tree-snapshot.js.map +0 -1
- package/dist/tools/content/get-as-html.js +0 -164
- package/dist/tools/content/get-as-html.js.map +0 -1
- package/dist/tools/content/get-as-text.js +0 -76
- package/dist/tools/content/get-as-text.js.map +0 -1
- package/dist/tools/content/index.js +0 -14
- package/dist/tools/content/index.js.map +0 -1
- package/dist/tools/content/save-as-pdf.js +0 -119
- package/dist/tools/content/save-as-pdf.js.map +0 -1
- package/dist/tools/content/take-screenshot.js +0 -297
- package/dist/tools/content/take-screenshot.js.map +0 -1
- package/dist/tools/figma/compare/compare-image-embedding.js +0 -159
- package/dist/tools/figma/compare/compare-image-embedding.js.map +0 -1
- package/dist/tools/figma/compare/compare-mssim.js +0 -98
- package/dist/tools/figma/compare/compare-mssim.js.map +0 -1
- package/dist/tools/figma/compare/compare-text-embedding.js +0 -291
- package/dist/tools/figma/compare/compare-text-embedding.js.map +0 -1
- package/dist/tools/figma/compare/index.js +0 -139
- package/dist/tools/figma/compare/index.js.map +0 -1
- package/dist/tools/figma/compare/types.js +0 -3
- package/dist/tools/figma/compare/types.js.map +0 -1
- package/dist/tools/figma/compare/vector.js +0 -46
- package/dist/tools/figma/compare/vector.js.map +0 -1
- package/dist/tools/figma/compare-page-with-design.js +0 -240
- package/dist/tools/figma/compare-page-with-design.js.map +0 -1
- package/dist/tools/figma/figma-service.js +0 -134
- package/dist/tools/figma/figma-service.js.map +0 -1
- package/dist/tools/figma/index.js +0 -6
- package/dist/tools/figma/index.js.map +0 -1
- package/dist/tools/index.js +0 -41
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/interaction/click.js +0 -29
- package/dist/tools/interaction/click.js.map +0 -1
- package/dist/tools/interaction/drag.js +0 -41
- package/dist/tools/interaction/drag.js.map +0 -1
- package/dist/tools/interaction/fill.js +0 -28
- package/dist/tools/interaction/fill.js.map +0 -1
- package/dist/tools/interaction/hover.js +0 -29
- package/dist/tools/interaction/hover.js.map +0 -1
- package/dist/tools/interaction/index.js +0 -24
- package/dist/tools/interaction/index.js.map +0 -1
- package/dist/tools/interaction/press-key.js +0 -99
- package/dist/tools/interaction/press-key.js.map +0 -1
- package/dist/tools/interaction/resize-viewport.js +0 -110
- package/dist/tools/interaction/resize-viewport.js.map +0 -1
- package/dist/tools/interaction/resize-window.js +0 -261
- package/dist/tools/interaction/resize-window.js.map +0 -1
- package/dist/tools/interaction/scroll.js +0 -304
- package/dist/tools/interaction/scroll.js.map +0 -1
- package/dist/tools/interaction/select.js +0 -30
- package/dist/tools/interaction/select.js.map +0 -1
- package/dist/tools/navigation/go-back.js +0 -76
- package/dist/tools/navigation/go-back.js.map +0 -1
- package/dist/tools/navigation/go-forward.js +0 -76
- package/dist/tools/navigation/go-forward.js.map +0 -1
- package/dist/tools/navigation/go-to.js +0 -80
- package/dist/tools/navigation/go-to.js.map +0 -1
- package/dist/tools/navigation/index.js +0 -14
- package/dist/tools/navigation/index.js.map +0 -1
- package/dist/tools/navigation/reload.js +0 -76
- package/dist/tools/navigation/reload.js.map +0 -1
- package/dist/tools/o11y/get-console-messages.js +0 -151
- package/dist/tools/o11y/get-console-messages.js.map +0 -1
- package/dist/tools/o11y/get-http-requests.js +0 -216
- package/dist/tools/o11y/get-http-requests.js.map +0 -1
- package/dist/tools/o11y/get-trace-id.js +0 -30
- package/dist/tools/o11y/get-trace-id.js.map +0 -1
- package/dist/tools/o11y/get-web-vitals.js +0 -595
- package/dist/tools/o11y/get-web-vitals.js.map +0 -1
- package/dist/tools/o11y/index.js +0 -18
- package/dist/tools/o11y/index.js.map +0 -1
- package/dist/tools/o11y/new-trace-id.js +0 -32
- package/dist/tools/o11y/new-trace-id.js.map +0 -1
- package/dist/tools/o11y/set-trace-id.js +0 -28
- package/dist/tools/o11y/set-trace-id.js.map +0 -1
- package/dist/tools/react/get-component-for-element.js +0 -941
- package/dist/tools/react/get-component-for-element.js.map +0 -1
- package/dist/tools/react/get-element-for-component.js +0 -1190
- package/dist/tools/react/get-element-for-component.js.map +0 -1
- package/dist/tools/react/index.js +0 -10
- package/dist/tools/react/index.js.map +0 -1
- package/dist/tools/run/index.js +0 -7
- package/dist/tools/run/index.js.map +0 -1
- package/dist/tools/run/js-in-browser.js +0 -51
- package/dist/tools/run/js-in-browser.js.map +0 -1
- package/dist/tools/run/js-in-sandbox.js +0 -175
- package/dist/tools/run/js-in-sandbox.js.map +0 -1
- package/dist/tools/stub/clear.js +0 -41
- package/dist/tools/stub/clear.js.map +0 -1
- package/dist/tools/stub/index.js +0 -14
- package/dist/tools/stub/index.js.map +0 -1
- package/dist/tools/stub/intercept-http-request.js +0 -112
- package/dist/tools/stub/intercept-http-request.js.map +0 -1
- package/dist/tools/stub/list.js +0 -75
- package/dist/tools/stub/list.js.map +0 -1
- package/dist/tools/stub/mock-http-response.js +0 -152
- package/dist/tools/stub/mock-http-response.js.map +0 -1
- package/dist/tools/stub/stub-controller.js +0 -284
- package/dist/tools/stub/stub-controller.js.map +0 -1
- package/dist/tools/sync/index.js +0 -6
- package/dist/tools/sync/index.js.map +0 -1
- package/dist/tools/sync/wait-for-network-idle.js +0 -152
- package/dist/tools/sync/wait-for-network-idle.js.map +0 -1
- package/dist/tools/tool-executor.js +0 -79
- package/dist/tools/tool-executor.js.map +0 -1
- package/dist/tools/types.js +0 -3
- package/dist/tools/types.js.map +0 -1
- package/dist/types.js +0 -55
- package/dist/types.js.map +0 -1
- package/dist/utils/cli-utils.js +0 -253
- package/dist/utils/cli-utils.js.map +0 -1
- package/dist/utils.js +0 -85
- package/dist/utils.js.map +0 -1
|
@@ -1,1190 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GetElementForComponent = void 0;
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
/**
|
|
6
|
-
* -----------------------------------------------------------------------------
|
|
7
|
-
* Defaults
|
|
8
|
-
* -----------------------------------------------------------------------------
|
|
9
|
-
*
|
|
10
|
-
* Notes:
|
|
11
|
-
* - Keep defaults conservative to avoid huge outputs and runaway fiber traversals.
|
|
12
|
-
* - All constants are explicitly typed to match your code style.
|
|
13
|
-
*/
|
|
14
|
-
const DEFAULT_MATCH_STRATEGY = 'contains';
|
|
15
|
-
const DEFAULT_MAX_ELEMENTS = 200;
|
|
16
|
-
const DEFAULT_ONLY_VISIBLE = true;
|
|
17
|
-
const DEFAULT_ONLY_IN_VIEWPORT = true;
|
|
18
|
-
const DEFAULT_TEXT_PREVIEW_MAX_LENGTH = 80;
|
|
19
|
-
// Search / output controls
|
|
20
|
-
const DEFAULT_MAX_MATCHES = 5;
|
|
21
|
-
const DEFAULT_STACK_LIMIT = 50;
|
|
22
|
-
// Internal safety caps (avoid runaway traversal / DOM scanning costs)
|
|
23
|
-
const INTERNAL_MAX_ROOTS_SCAN = 20;
|
|
24
|
-
const INTERNAL_MAX_FIBERS_VISITED = 250_000;
|
|
25
|
-
class GetElementForComponent {
|
|
26
|
-
name() {
|
|
27
|
-
return 'react_get-element-for-component';
|
|
28
|
-
}
|
|
29
|
-
description() {
|
|
30
|
-
return `
|
|
31
|
-
Maps a React COMPONENT INSTANCE to the DOM elements it renders (its "DOM footprint") by traversing the React Fiber graph.
|
|
32
|
-
|
|
33
|
-
Selection strategy:
|
|
34
|
-
- Best: provide an anchor (anchorSelector OR anchorX/anchorY) to target the correct instance near that UI.
|
|
35
|
-
- Optionally provide a query (componentName and/or fileNameHint/lineNumber) to search the Fiber graph.
|
|
36
|
-
- If both anchor + query are provided, we rank candidates and pick the best match near the anchor.
|
|
37
|
-
|
|
38
|
-
Important behavior:
|
|
39
|
-
- The React DevTools hook is helpful but NOT strictly required.
|
|
40
|
-
- If hook exists, roots are discovered reliably via getFiberRoots().
|
|
41
|
-
- If hook doesn't exist, we fall back to scanning the DOM for __reactFiber$ pointers (best-effort).
|
|
42
|
-
- Query search returns multiple candidates (up to maxMatches) and ranks them.
|
|
43
|
-
- Debug source information is best-effort and may be missing even in dev builds depending on bundler/build flags.
|
|
44
|
-
|
|
45
|
-
Operational note for MCP users:
|
|
46
|
-
- If you are using a persistent/headful browser and want more reliable root discovery and component search,
|
|
47
|
-
install the "React Developer Tools" Chrome extension in that browser profile (manual step by the user).
|
|
48
|
-
`.trim();
|
|
49
|
-
}
|
|
50
|
-
inputSchema() {
|
|
51
|
-
return {
|
|
52
|
-
anchorSelector: zod_1.z
|
|
53
|
-
.string()
|
|
54
|
-
.optional()
|
|
55
|
-
.describe('DOM CSS selector used as an anchor to pick a concrete component instance near that element.'),
|
|
56
|
-
anchorX: zod_1.z
|
|
57
|
-
.number()
|
|
58
|
-
.int()
|
|
59
|
-
.nonnegative()
|
|
60
|
-
.optional()
|
|
61
|
-
.describe('Anchor X coordinate (viewport pixels).'),
|
|
62
|
-
anchorY: zod_1.z
|
|
63
|
-
.number()
|
|
64
|
-
.int()
|
|
65
|
-
.nonnegative()
|
|
66
|
-
.optional()
|
|
67
|
-
.describe('Anchor Y coordinate (viewport pixels).'),
|
|
68
|
-
componentName: zod_1.z
|
|
69
|
-
.string()
|
|
70
|
-
.optional()
|
|
71
|
-
.describe('React component name/displayName to search in the Fiber graph.'),
|
|
72
|
-
matchStrategy: zod_1.z
|
|
73
|
-
.enum(['exact', 'contains'])
|
|
74
|
-
.optional()
|
|
75
|
-
.default(DEFAULT_MATCH_STRATEGY)
|
|
76
|
-
.describe('How to match componentName against Fiber type/displayName.'),
|
|
77
|
-
fileNameHint: zod_1.z
|
|
78
|
-
.string()
|
|
79
|
-
.optional()
|
|
80
|
-
.describe('Best-effort debug source file hint (usually endsWith match), e.g., "UserCard.tsx".'),
|
|
81
|
-
lineNumber: zod_1.z
|
|
82
|
-
.number()
|
|
83
|
-
.int()
|
|
84
|
-
.positive()
|
|
85
|
-
.optional()
|
|
86
|
-
.describe('Optional debug source line number hint (dev builds only).'),
|
|
87
|
-
maxElements: zod_1.z
|
|
88
|
-
.number()
|
|
89
|
-
.int()
|
|
90
|
-
.positive()
|
|
91
|
-
.optional()
|
|
92
|
-
.default(DEFAULT_MAX_ELEMENTS)
|
|
93
|
-
.describe('Maximum number of DOM elements to return from the selected component subtree.'),
|
|
94
|
-
onlyVisible: zod_1.z
|
|
95
|
-
.boolean()
|
|
96
|
-
.optional()
|
|
97
|
-
.default(DEFAULT_ONLY_VISIBLE)
|
|
98
|
-
.describe('If true, only visually visible elements are returned.'),
|
|
99
|
-
onlyInViewport: zod_1.z
|
|
100
|
-
.boolean()
|
|
101
|
-
.optional()
|
|
102
|
-
.default(DEFAULT_ONLY_IN_VIEWPORT)
|
|
103
|
-
.describe('If true, only elements intersecting the viewport are returned.'),
|
|
104
|
-
textPreviewMaxLength: zod_1.z
|
|
105
|
-
.number()
|
|
106
|
-
.int()
|
|
107
|
-
.positive()
|
|
108
|
-
.optional()
|
|
109
|
-
.default(DEFAULT_TEXT_PREVIEW_MAX_LENGTH)
|
|
110
|
-
.describe('Max length for per-element text preview (innerText/textContent/aria-label).'),
|
|
111
|
-
maxMatches: zod_1.z
|
|
112
|
-
.number()
|
|
113
|
-
.int()
|
|
114
|
-
.positive()
|
|
115
|
-
.optional()
|
|
116
|
-
.default(DEFAULT_MAX_MATCHES)
|
|
117
|
-
.describe('Max number of matching component candidates to return/rank.'),
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
outputSchema() {
|
|
121
|
-
return {
|
|
122
|
-
reactDetected: zod_1.z
|
|
123
|
-
.boolean()
|
|
124
|
-
.describe('True if __REACT_DEVTOOLS_GLOBAL_HOOK__ looks available.'),
|
|
125
|
-
fiberDetected: zod_1.z
|
|
126
|
-
.boolean()
|
|
127
|
-
.describe('True if DOM appears to contain React Fiber pointers (__reactFiber$...).'),
|
|
128
|
-
rootDiscovery: zod_1.z
|
|
129
|
-
.enum(['devtools-hook', 'dom-fiber-scan', 'none'])
|
|
130
|
-
.describe('How roots were discovered.'),
|
|
131
|
-
component: zod_1.z
|
|
132
|
-
.object({
|
|
133
|
-
name: zod_1.z.string().nullable(),
|
|
134
|
-
displayName: zod_1.z.string().nullable(),
|
|
135
|
-
debugSource: zod_1.z
|
|
136
|
-
.object({
|
|
137
|
-
fileName: zod_1.z.string().nullable(),
|
|
138
|
-
lineNumber: zod_1.z.number().int().nullable(),
|
|
139
|
-
columnNumber: zod_1.z.number().int().nullable(),
|
|
140
|
-
})
|
|
141
|
-
.nullable(),
|
|
142
|
-
componentStack: zod_1.z.array(zod_1.z.string()),
|
|
143
|
-
selection: zod_1.z.enum([
|
|
144
|
-
'anchor',
|
|
145
|
-
'query',
|
|
146
|
-
'query+anchor',
|
|
147
|
-
'unknown',
|
|
148
|
-
]),
|
|
149
|
-
scoring: zod_1.z
|
|
150
|
-
.object({
|
|
151
|
-
score: zod_1.z.number(),
|
|
152
|
-
nameMatched: zod_1.z.boolean(),
|
|
153
|
-
debugSourceMatched: zod_1.z.boolean(),
|
|
154
|
-
anchorRelated: zod_1.z.boolean(),
|
|
155
|
-
proximityPx: zod_1.z.number().nullable().optional(),
|
|
156
|
-
})
|
|
157
|
-
.optional(),
|
|
158
|
-
})
|
|
159
|
-
.nullable(),
|
|
160
|
-
candidates: zod_1.z
|
|
161
|
-
.array(zod_1.z.object({
|
|
162
|
-
name: zod_1.z.string().nullable(),
|
|
163
|
-
displayName: zod_1.z.string().nullable(),
|
|
164
|
-
debugSource: zod_1.z
|
|
165
|
-
.object({
|
|
166
|
-
fileName: zod_1.z.string().nullable(),
|
|
167
|
-
lineNumber: zod_1.z.number().int().nullable(),
|
|
168
|
-
columnNumber: zod_1.z.number().int().nullable(),
|
|
169
|
-
})
|
|
170
|
-
.nullable(),
|
|
171
|
-
componentStack: zod_1.z.array(zod_1.z.string()),
|
|
172
|
-
selection: zod_1.z.enum([
|
|
173
|
-
'anchor',
|
|
174
|
-
'query',
|
|
175
|
-
'query+anchor',
|
|
176
|
-
'unknown',
|
|
177
|
-
]),
|
|
178
|
-
scoring: zod_1.z
|
|
179
|
-
.object({
|
|
180
|
-
score: zod_1.z.number(),
|
|
181
|
-
nameMatched: zod_1.z.boolean(),
|
|
182
|
-
debugSourceMatched: zod_1.z.boolean(),
|
|
183
|
-
anchorRelated: zod_1.z.boolean(),
|
|
184
|
-
proximityPx: zod_1.z.number().nullable().optional(),
|
|
185
|
-
})
|
|
186
|
-
.optional(),
|
|
187
|
-
domFootprintCount: zod_1.z.number().int(),
|
|
188
|
-
}))
|
|
189
|
-
.describe('Ranked candidate matches (best-first).'),
|
|
190
|
-
elements: zod_1.z.array(zod_1.z.object({
|
|
191
|
-
tagName: zod_1.z.string(),
|
|
192
|
-
id: zod_1.z.string().nullable(),
|
|
193
|
-
className: zod_1.z.string().nullable(),
|
|
194
|
-
selectorHint: zod_1.z.string().nullable(),
|
|
195
|
-
textPreview: zod_1.z.string().nullable(),
|
|
196
|
-
boundingBox: zod_1.z
|
|
197
|
-
.object({
|
|
198
|
-
x: zod_1.z.number(),
|
|
199
|
-
y: zod_1.z.number(),
|
|
200
|
-
width: zod_1.z.number(),
|
|
201
|
-
height: zod_1.z.number(),
|
|
202
|
-
})
|
|
203
|
-
.nullable(),
|
|
204
|
-
isVisible: zod_1.z.boolean(),
|
|
205
|
-
isInViewport: zod_1.z.boolean(),
|
|
206
|
-
})),
|
|
207
|
-
notes: zod_1.z.array(zod_1.z.string()),
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
async handle(context, args) {
|
|
211
|
-
const anchorSelector = args.anchorSelector;
|
|
212
|
-
const anchorX = args.anchorX;
|
|
213
|
-
const anchorY = args.anchorY;
|
|
214
|
-
const componentName = args.componentName;
|
|
215
|
-
const matchStrategy = args.matchStrategy ?? DEFAULT_MATCH_STRATEGY;
|
|
216
|
-
const fileNameHint = args.fileNameHint;
|
|
217
|
-
const lineNumber = args.lineNumber;
|
|
218
|
-
const maxElements = args.maxElements ?? DEFAULT_MAX_ELEMENTS;
|
|
219
|
-
const onlyVisible = args.onlyVisible ?? DEFAULT_ONLY_VISIBLE;
|
|
220
|
-
const onlyInViewport = args.onlyInViewport ?? DEFAULT_ONLY_IN_VIEWPORT;
|
|
221
|
-
const textPreviewMaxLength = args.textPreviewMaxLength ?? DEFAULT_TEXT_PREVIEW_MAX_LENGTH;
|
|
222
|
-
const maxMatches = args.maxMatches ?? DEFAULT_MAX_MATCHES;
|
|
223
|
-
const hasAnchorSelector = typeof anchorSelector === 'string' &&
|
|
224
|
-
anchorSelector.trim().length > 0;
|
|
225
|
-
const hasAnchorPoint = typeof anchorX === 'number' && typeof anchorY === 'number';
|
|
226
|
-
const hasQuery = (typeof componentName === 'string' &&
|
|
227
|
-
componentName.trim().length > 0) ||
|
|
228
|
-
(typeof fileNameHint === 'string' &&
|
|
229
|
-
fileNameHint.trim().length > 0) ||
|
|
230
|
-
typeof lineNumber === 'number';
|
|
231
|
-
if (!hasAnchorSelector && !hasAnchorPoint && !hasQuery) {
|
|
232
|
-
throw new Error('Provide at least one targeting method: anchorSelector, (anchorX+anchorY), or componentName/debugSource hints.');
|
|
233
|
-
}
|
|
234
|
-
const result = await context.page.evaluate(({ anchorSelectorEval, anchorXEval, anchorYEval, componentNameEval, matchStrategyEval, fileNameHintEval, lineNumberEval, maxElementsEval, onlyVisibleEval, onlyInViewportEval, textPreviewMaxLengthEval, maxMatchesEval, maxRootsScan, maxFibersVisited, stackLimit, }) => {
|
|
235
|
-
/**
|
|
236
|
-
* -----------------------------------------------------------------
|
|
237
|
-
* Helpers (run in the browser page context)
|
|
238
|
-
* -----------------------------------------------------------------
|
|
239
|
-
*/
|
|
240
|
-
const notes = [];
|
|
241
|
-
function isElement(v) {
|
|
242
|
-
return (typeof Element !== 'undefined' &&
|
|
243
|
-
v instanceof Element);
|
|
244
|
-
}
|
|
245
|
-
function normalizeStr(v) {
|
|
246
|
-
if (typeof v !== 'string') {
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
const t = v.trim();
|
|
250
|
-
if (!t) {
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
return t;
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* React attaches internal keys like:
|
|
257
|
-
* - __reactFiber$<random>
|
|
258
|
-
* - __reactInternalInstance$<random> (older)
|
|
259
|
-
*/
|
|
260
|
-
function getFiberFromDomElement(el) {
|
|
261
|
-
const anyEl = el;
|
|
262
|
-
const keys = Object.keys(anyEl);
|
|
263
|
-
for (const k of keys) {
|
|
264
|
-
if (k.startsWith('__reactFiber$')) {
|
|
265
|
-
const f = anyEl[k];
|
|
266
|
-
if (f) {
|
|
267
|
-
return f;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
for (const k of keys) {
|
|
272
|
-
if (k.startsWith('__reactInternalInstance$')) {
|
|
273
|
-
const f = anyEl[k];
|
|
274
|
-
if (f) {
|
|
275
|
-
return f;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* DevTools hook provides reliable Fiber roots via getFiberRoots().
|
|
283
|
-
* If it's missing, we can still try to use DOM fiber pointers (best-effort),
|
|
284
|
-
* but component search becomes less reliable.
|
|
285
|
-
*/
|
|
286
|
-
function getDevtoolsHook() {
|
|
287
|
-
const g = globalThis;
|
|
288
|
-
const hook = g.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
289
|
-
if (!hook) {
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
if (typeof hook.getFiberRoots !== 'function') {
|
|
293
|
-
return null;
|
|
294
|
-
}
|
|
295
|
-
return hook;
|
|
296
|
-
}
|
|
297
|
-
function getAllRootsFromHook(hook) {
|
|
298
|
-
const roots = [];
|
|
299
|
-
const renderers = hook.renderers;
|
|
300
|
-
if (!renderers ||
|
|
301
|
-
typeof renderers.forEach !== 'function') {
|
|
302
|
-
return roots;
|
|
303
|
-
}
|
|
304
|
-
renderers.forEach((_renderer, rendererId) => {
|
|
305
|
-
try {
|
|
306
|
-
const set = hook.getFiberRoots(rendererId);
|
|
307
|
-
if (set &&
|
|
308
|
-
typeof set.forEach === 'function') {
|
|
309
|
-
set.forEach((root) => {
|
|
310
|
-
if (root) {
|
|
311
|
-
roots.push(root);
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
// ignore
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
return roots.slice(0, maxRootsScan);
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* DOM-only fallback: find a few fiber pointers by scanning the DOM.
|
|
324
|
-
* This provides seed fibers even without the DevTools hook.
|
|
325
|
-
*/
|
|
326
|
-
function getSeedFibersFromDomScan(maxSeeds) {
|
|
327
|
-
const out = [];
|
|
328
|
-
const els = Array.from(document.querySelectorAll('*'));
|
|
329
|
-
const step = els.length > 5000
|
|
330
|
-
? Math.ceil(els.length / 5000)
|
|
331
|
-
: 1;
|
|
332
|
-
for (let i = 0; i < els.length; i += step) {
|
|
333
|
-
const el = els[i];
|
|
334
|
-
const f = getFiberFromDomElement(el);
|
|
335
|
-
if (f) {
|
|
336
|
-
out.push(f);
|
|
337
|
-
if (out.length >= maxSeeds) {
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return out;
|
|
343
|
-
}
|
|
344
|
-
function getFunctionDisplayName(fn) {
|
|
345
|
-
if (typeof fn !== 'function') {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
const anyFn = fn;
|
|
349
|
-
const dn = normalizeStr(anyFn.displayName);
|
|
350
|
-
if (dn) {
|
|
351
|
-
return dn;
|
|
352
|
-
}
|
|
353
|
-
const nm = normalizeStr(anyFn.name);
|
|
354
|
-
if (nm) {
|
|
355
|
-
return nm;
|
|
356
|
-
}
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
|
-
function getFiberTypeName(fiber) {
|
|
360
|
-
if (!fiber) {
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
const t = fiber.type ?? fiber.elementType ?? null;
|
|
364
|
-
if (!t) {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
if (typeof t === 'function') {
|
|
368
|
-
return getFunctionDisplayName(t);
|
|
369
|
-
}
|
|
370
|
-
if (typeof t === 'string') {
|
|
371
|
-
return t; // host component name
|
|
372
|
-
}
|
|
373
|
-
const dn = normalizeStr(t.displayName);
|
|
374
|
-
if (dn) {
|
|
375
|
-
return dn;
|
|
376
|
-
}
|
|
377
|
-
return normalizeStr(t.name);
|
|
378
|
-
}
|
|
379
|
-
function getDisplayName(fiber) {
|
|
380
|
-
if (!fiber) {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
const t = fiber.type ?? fiber.elementType ?? null;
|
|
384
|
-
if (!t) {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
const dn = normalizeStr(t.displayName);
|
|
388
|
-
if (dn) {
|
|
389
|
-
return dn;
|
|
390
|
-
}
|
|
391
|
-
return getFiberTypeName(fiber);
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* Best-effort debug source extraction.
|
|
395
|
-
*
|
|
396
|
-
* IMPORTANT:
|
|
397
|
-
* Even when React DevTools shows "Rendered by <file>:<line>",
|
|
398
|
-
* fiber._debugSource can still be null because DevTools may derive source
|
|
399
|
-
* via stack traces + sourcemaps and keep that mapping internally.
|
|
400
|
-
*/
|
|
401
|
-
function getDebugSource(fiber) {
|
|
402
|
-
const seen = new Set();
|
|
403
|
-
const queue = [];
|
|
404
|
-
const push = (x) => {
|
|
405
|
-
if (!x) {
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
if (seen.has(x)) {
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
seen.add(x);
|
|
412
|
-
queue.push(x);
|
|
413
|
-
};
|
|
414
|
-
push(fiber);
|
|
415
|
-
push(fiber?.alternate);
|
|
416
|
-
// Walk a few owners up (best-effort)
|
|
417
|
-
for (let i = 0; i < 8; i++) {
|
|
418
|
-
const f = queue[i];
|
|
419
|
-
if (!f) {
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
push(f?._debugOwner);
|
|
423
|
-
push(f?._debugOwner?.alternate);
|
|
424
|
-
}
|
|
425
|
-
for (const f of queue) {
|
|
426
|
-
const src = f?._debugSource ??
|
|
427
|
-
f?._debugOwner?._debugSource ??
|
|
428
|
-
f?.type?._debugSource ??
|
|
429
|
-
f?.elementType?._debugSource ??
|
|
430
|
-
null;
|
|
431
|
-
if (!src) {
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
const fileName = normalizeStr(src.fileName) ?? null;
|
|
435
|
-
const lineNumber = typeof src.lineNumber === 'number'
|
|
436
|
-
? src.lineNumber
|
|
437
|
-
: null;
|
|
438
|
-
const columnNumber = typeof src.columnNumber === 'number'
|
|
439
|
-
? src.columnNumber
|
|
440
|
-
: null;
|
|
441
|
-
if (fileName ||
|
|
442
|
-
lineNumber !== null ||
|
|
443
|
-
columnNumber !== null) {
|
|
444
|
-
return { fileName, lineNumber, columnNumber };
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
function isHostFiber(fiber) {
|
|
450
|
-
const sn = fiber?.stateNode;
|
|
451
|
-
return isElement(sn);
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Build a best-effort component stack from selected fiber up to root.
|
|
455
|
-
* We prefer non-host fibers for readability, but fall back to include hosts if needed.
|
|
456
|
-
*/
|
|
457
|
-
function buildComponentStack(fiber, limit) {
|
|
458
|
-
const out = [];
|
|
459
|
-
let cur = fiber ?? null;
|
|
460
|
-
for (let i = 0; i < limit; i++) {
|
|
461
|
-
if (!cur) {
|
|
462
|
-
break;
|
|
463
|
-
}
|
|
464
|
-
const name = getDisplayName(cur);
|
|
465
|
-
const host = isHostFiber(cur);
|
|
466
|
-
if (name && !host) {
|
|
467
|
-
out.push(name);
|
|
468
|
-
}
|
|
469
|
-
cur = cur.return ?? null;
|
|
470
|
-
}
|
|
471
|
-
if (out.length === 0) {
|
|
472
|
-
cur = fiber ?? null;
|
|
473
|
-
for (let i = 0; i < limit; i++) {
|
|
474
|
-
if (!cur) {
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
const name = getDisplayName(cur);
|
|
478
|
-
if (name) {
|
|
479
|
-
out.push(name);
|
|
480
|
-
}
|
|
481
|
-
cur = cur.return ?? null;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
// De-dupe consecutive duplicates (wrappers can repeat)
|
|
485
|
-
const deduped = [];
|
|
486
|
-
for (const n of out) {
|
|
487
|
-
if (deduped.length === 0) {
|
|
488
|
-
deduped.push(n);
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
if (deduped[deduped.length - 1] !== n) {
|
|
492
|
-
deduped.push(n);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
return deduped;
|
|
497
|
-
}
|
|
498
|
-
function firstMeaningfulComponentAbove(fiber) {
|
|
499
|
-
let cur = fiber ?? null;
|
|
500
|
-
const visited = new Set();
|
|
501
|
-
while (cur) {
|
|
502
|
-
const key = cur.alternate ?? cur;
|
|
503
|
-
if (visited.has(key)) {
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
visited.add(key);
|
|
507
|
-
if (!isHostFiber(cur)) {
|
|
508
|
-
return cur;
|
|
509
|
-
}
|
|
510
|
-
cur = cur.return ?? null;
|
|
511
|
-
}
|
|
512
|
-
return null;
|
|
513
|
-
}
|
|
514
|
-
function matchesName(candidate, query, strategy) {
|
|
515
|
-
if (!candidate) {
|
|
516
|
-
return false;
|
|
517
|
-
}
|
|
518
|
-
const a = candidate.trim();
|
|
519
|
-
const b = query.trim();
|
|
520
|
-
if (!a || !b) {
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
if (strategy === 'exact') {
|
|
524
|
-
return a === b;
|
|
525
|
-
}
|
|
526
|
-
return a.toLowerCase().includes(b.toLowerCase());
|
|
527
|
-
}
|
|
528
|
-
function matchesDebugSource(fiber, fileNameHint, lineNumber) {
|
|
529
|
-
const src = getDebugSource(fiber);
|
|
530
|
-
if (!src) {
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
if (fileNameHint) {
|
|
534
|
-
const hint = fileNameHint.trim();
|
|
535
|
-
if (hint) {
|
|
536
|
-
const fn = (src.fileName ?? '').trim();
|
|
537
|
-
if (!fn) {
|
|
538
|
-
return false;
|
|
539
|
-
}
|
|
540
|
-
const fnLower = fn.toLowerCase();
|
|
541
|
-
const hintLower = hint.toLowerCase();
|
|
542
|
-
const ok = fnLower.endsWith(hintLower) ||
|
|
543
|
-
fnLower.includes(hintLower);
|
|
544
|
-
if (!ok) {
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
if (typeof lineNumber === 'number') {
|
|
550
|
-
if (src.lineNumber === null) {
|
|
551
|
-
return false;
|
|
552
|
-
}
|
|
553
|
-
// Allow small drift due to transforms
|
|
554
|
-
const delta = Math.abs(src.lineNumber - lineNumber);
|
|
555
|
-
if (delta > 3) {
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
return true;
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Roots can be either:
|
|
563
|
-
* - a "root object" containing .current
|
|
564
|
-
* - a fiber itself
|
|
565
|
-
*
|
|
566
|
-
* We start from root.current.child to avoid getting stuck at the HostRoot wrapper.
|
|
567
|
-
*/
|
|
568
|
-
function toStartFiber(rootOrFiber) {
|
|
569
|
-
if (!rootOrFiber) {
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
const maybeCurrent = rootOrFiber.current;
|
|
573
|
-
if (maybeCurrent) {
|
|
574
|
-
return maybeCurrent.child ?? maybeCurrent;
|
|
575
|
-
}
|
|
576
|
-
return rootOrFiber;
|
|
577
|
-
}
|
|
578
|
-
function pickAnchorElement(anchorSelector, x, y) {
|
|
579
|
-
if (anchorSelector && anchorSelector.trim()) {
|
|
580
|
-
const el = document.querySelector(anchorSelector);
|
|
581
|
-
if (!el) {
|
|
582
|
-
notes.push(`anchorSelector did not match any element: ${anchorSelector}`);
|
|
583
|
-
return null;
|
|
584
|
-
}
|
|
585
|
-
return el;
|
|
586
|
-
}
|
|
587
|
-
if (typeof x === 'number' && typeof y === 'number') {
|
|
588
|
-
const el = document.elementFromPoint(x, y);
|
|
589
|
-
if (!el) {
|
|
590
|
-
notes.push(`anchorPoint did not hit any element: (${x}, ${y})`);
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
return el;
|
|
594
|
-
}
|
|
595
|
-
return null;
|
|
596
|
-
}
|
|
597
|
-
function selectorHintFor(el) {
|
|
598
|
-
const dt = normalizeStr(el.getAttribute('data-testid')) ??
|
|
599
|
-
normalizeStr(el.getAttribute('data-test-id')) ??
|
|
600
|
-
normalizeStr(el.getAttribute('data-test')) ??
|
|
601
|
-
null;
|
|
602
|
-
if (dt) {
|
|
603
|
-
return `[data-testid='${dt.replace(/'/g, "\\'")}']`;
|
|
604
|
-
}
|
|
605
|
-
const ds = normalizeStr(el.getAttribute('data-selector')) ??
|
|
606
|
-
null;
|
|
607
|
-
if (ds) {
|
|
608
|
-
return `[data-selector='${ds.replace(/'/g, "\\'")}']`;
|
|
609
|
-
}
|
|
610
|
-
if (el.id) {
|
|
611
|
-
try {
|
|
612
|
-
return `#${CSS.escape(el.id)}`;
|
|
613
|
-
}
|
|
614
|
-
catch {
|
|
615
|
-
return `#${el.id}`;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return el.tagName.toLowerCase();
|
|
619
|
-
}
|
|
620
|
-
function textPreviewFor(el, maxLen) {
|
|
621
|
-
const aria = normalizeStr(el.getAttribute('aria-label')) ?? null;
|
|
622
|
-
if (aria) {
|
|
623
|
-
return aria.slice(0, maxLen);
|
|
624
|
-
}
|
|
625
|
-
const txt = String(el.innerText ?? el.textContent ?? '').trim();
|
|
626
|
-
if (!txt) {
|
|
627
|
-
return null;
|
|
628
|
-
}
|
|
629
|
-
return txt.slice(0, maxLen);
|
|
630
|
-
}
|
|
631
|
-
function computeRuntime(el) {
|
|
632
|
-
const r = el.getBoundingClientRect();
|
|
633
|
-
const s = getComputedStyle(el);
|
|
634
|
-
const isVisible = s.display !== 'none' &&
|
|
635
|
-
s.visibility !== 'hidden' &&
|
|
636
|
-
parseFloat(s.opacity || '1') > 0 &&
|
|
637
|
-
r.width > 0 &&
|
|
638
|
-
r.height > 0;
|
|
639
|
-
const vw = window.innerWidth;
|
|
640
|
-
const vh = window.innerHeight;
|
|
641
|
-
const isInViewport = r.right > 0 &&
|
|
642
|
-
r.bottom > 0 &&
|
|
643
|
-
r.left < vw &&
|
|
644
|
-
r.top < vh;
|
|
645
|
-
const boundingBox = Number.isFinite(r.x) &&
|
|
646
|
-
Number.isFinite(r.y) &&
|
|
647
|
-
Number.isFinite(r.width) &&
|
|
648
|
-
Number.isFinite(r.height)
|
|
649
|
-
? {
|
|
650
|
-
x: r.x,
|
|
651
|
-
y: r.y,
|
|
652
|
-
width: r.width,
|
|
653
|
-
height: r.height,
|
|
654
|
-
}
|
|
655
|
-
: null;
|
|
656
|
-
const centerX = r.left + r.width / 2;
|
|
657
|
-
const centerY = r.top + r.height / 2;
|
|
658
|
-
return {
|
|
659
|
-
boundingBox,
|
|
660
|
-
isVisible,
|
|
661
|
-
isInViewport,
|
|
662
|
-
centerX,
|
|
663
|
-
centerY,
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
function collectDomElementsFromFiberSubtree(fiber, maxElements, onlyVisible, onlyInViewport, textPreviewMaxLength) {
|
|
667
|
-
const out = [];
|
|
668
|
-
const seen = new Set();
|
|
669
|
-
const stack = [];
|
|
670
|
-
if (fiber) {
|
|
671
|
-
stack.push(fiber);
|
|
672
|
-
}
|
|
673
|
-
const visited = new Set();
|
|
674
|
-
while (stack.length > 0) {
|
|
675
|
-
if (out.length >= maxElements) {
|
|
676
|
-
break;
|
|
677
|
-
}
|
|
678
|
-
const f = stack.pop();
|
|
679
|
-
if (!f) {
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
const key = f.alternate ?? f;
|
|
683
|
-
if (visited.has(key)) {
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
visited.add(key);
|
|
687
|
-
if (isHostFiber(f)) {
|
|
688
|
-
const el = f.stateNode;
|
|
689
|
-
if (!seen.has(el)) {
|
|
690
|
-
seen.add(el);
|
|
691
|
-
const rt = computeRuntime(el);
|
|
692
|
-
if (onlyVisible && !rt.isVisible) {
|
|
693
|
-
// skip
|
|
694
|
-
}
|
|
695
|
-
else {
|
|
696
|
-
if (onlyInViewport &&
|
|
697
|
-
!rt.isInViewport) {
|
|
698
|
-
// skip
|
|
699
|
-
}
|
|
700
|
-
else {
|
|
701
|
-
const tagName = el.tagName.toLowerCase();
|
|
702
|
-
const id = el.id
|
|
703
|
-
? String(el.id)
|
|
704
|
-
: null;
|
|
705
|
-
const classNameRaw = el.className;
|
|
706
|
-
const className = typeof classNameRaw === 'string'
|
|
707
|
-
? classNameRaw.trim()
|
|
708
|
-
? classNameRaw.trim()
|
|
709
|
-
: null
|
|
710
|
-
: null;
|
|
711
|
-
out.push({
|
|
712
|
-
tagName,
|
|
713
|
-
id,
|
|
714
|
-
className,
|
|
715
|
-
selectorHint: selectorHintFor(el),
|
|
716
|
-
textPreview: textPreviewFor(el, textPreviewMaxLength),
|
|
717
|
-
boundingBox: rt.boundingBox,
|
|
718
|
-
isVisible: rt.isVisible,
|
|
719
|
-
isInViewport: rt.isInViewport,
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
const child = f.child ?? null;
|
|
726
|
-
const sibling = f.sibling ?? null;
|
|
727
|
-
if (child) {
|
|
728
|
-
stack.push(child);
|
|
729
|
-
}
|
|
730
|
-
if (sibling) {
|
|
731
|
-
stack.push(sibling);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return out;
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* Search for matching fibers and return multiple candidates.
|
|
738
|
-
* Key fixes vs the old version:
|
|
739
|
-
* - Start traversal from root.current.child (if root object), not from root itself
|
|
740
|
-
* - Traverse a stable "current" graph; de-dupe using alternate/current pair
|
|
741
|
-
* - Collect multiple matches up to maxMatches
|
|
742
|
-
*/
|
|
743
|
-
function findFibersByQuery(roots, query, maxMatches) {
|
|
744
|
-
const nameQ = query.componentName
|
|
745
|
-
? query.componentName.trim()
|
|
746
|
-
: undefined;
|
|
747
|
-
const hasNameQ = Boolean(nameQ);
|
|
748
|
-
const fileQ = query.fileNameHint
|
|
749
|
-
? query.fileNameHint.trim()
|
|
750
|
-
: undefined;
|
|
751
|
-
const hasFileQ = Boolean(fileQ);
|
|
752
|
-
const hasLineQ = typeof query.lineNumber === 'number';
|
|
753
|
-
const wantsAnything = hasNameQ || hasFileQ || hasLineQ;
|
|
754
|
-
if (!wantsAnything) {
|
|
755
|
-
return [];
|
|
756
|
-
}
|
|
757
|
-
const stack = [];
|
|
758
|
-
for (const r of roots) {
|
|
759
|
-
const start = toStartFiber(r);
|
|
760
|
-
if (start) {
|
|
761
|
-
stack.push(start);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
const visited = new Set();
|
|
765
|
-
const matches = [];
|
|
766
|
-
let visitedCount = 0;
|
|
767
|
-
while (stack.length > 0) {
|
|
768
|
-
if (matches.length >= maxMatches) {
|
|
769
|
-
break;
|
|
770
|
-
}
|
|
771
|
-
const f = stack.pop();
|
|
772
|
-
if (!f) {
|
|
773
|
-
continue;
|
|
774
|
-
}
|
|
775
|
-
const key = f.alternate ?? f;
|
|
776
|
-
if (visited.has(key)) {
|
|
777
|
-
continue;
|
|
778
|
-
}
|
|
779
|
-
visited.add(key);
|
|
780
|
-
visitedCount++;
|
|
781
|
-
if (visitedCount > maxFibersVisited) {
|
|
782
|
-
notes.push(`Fiber traversal safety cap reached (${maxFibersVisited}). Results may be incomplete.`);
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
const dn = getDisplayName(f);
|
|
786
|
-
const tn = getFiberTypeName(f);
|
|
787
|
-
const nameMatches = hasNameQ
|
|
788
|
-
? matchesName(dn, nameQ, query.matchStrategy) ||
|
|
789
|
-
matchesName(tn, nameQ, query.matchStrategy)
|
|
790
|
-
: true;
|
|
791
|
-
const srcMatches = hasFileQ || hasLineQ
|
|
792
|
-
? matchesDebugSource(f, fileQ, query.lineNumber)
|
|
793
|
-
: true;
|
|
794
|
-
if (nameMatches && srcMatches) {
|
|
795
|
-
matches.push(f);
|
|
796
|
-
}
|
|
797
|
-
const child = f.child ?? null;
|
|
798
|
-
const sibling = f.sibling ?? null;
|
|
799
|
-
if (child) {
|
|
800
|
-
stack.push(child);
|
|
801
|
-
}
|
|
802
|
-
if (sibling) {
|
|
803
|
-
stack.push(sibling);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
return matches;
|
|
807
|
-
}
|
|
808
|
-
/**
|
|
809
|
-
* Score a candidate fiber using a heuristic:
|
|
810
|
-
* - Prefer explicit name matches and debugSource matches (if provided)
|
|
811
|
-
* - Prefer candidates related to the anchor (same subtree / overlapping area)
|
|
812
|
-
* - Prefer candidates with some DOM footprint (host nodes)
|
|
813
|
-
* - Prefer candidates near anchor point (if provided)
|
|
814
|
-
*/
|
|
815
|
-
function scoreCandidate(fiber, anchorEl, anchorX, anchorY, q, maxElements, onlyVisible, onlyInViewport, textPreviewMaxLength) {
|
|
816
|
-
const nameQ = q.componentName
|
|
817
|
-
? q.componentName.trim()
|
|
818
|
-
: undefined;
|
|
819
|
-
const hasNameQ = Boolean(nameQ);
|
|
820
|
-
const dn = getDisplayName(fiber);
|
|
821
|
-
const tn = getFiberTypeName(fiber);
|
|
822
|
-
const nameMatched = hasNameQ
|
|
823
|
-
? matchesName(dn, nameQ, q.matchStrategy) ||
|
|
824
|
-
matchesName(tn, nameQ, q.matchStrategy)
|
|
825
|
-
: false;
|
|
826
|
-
const debugSourceMatched = Boolean(normalizeStr(q.fileNameHint)) ||
|
|
827
|
-
typeof q.lineNumber === 'number'
|
|
828
|
-
? matchesDebugSource(fiber, normalizeStr(q.fileNameHint) ?? undefined, typeof q.lineNumber === 'number'
|
|
829
|
-
? q.lineNumber
|
|
830
|
-
: undefined)
|
|
831
|
-
: false;
|
|
832
|
-
const dom = collectDomElementsFromFiberSubtree(fiber, maxElements, onlyVisible, onlyInViewport, textPreviewMaxLength);
|
|
833
|
-
let anchorRelated = false;
|
|
834
|
-
let proximityPx = null;
|
|
835
|
-
if (anchorEl) {
|
|
836
|
-
const anchorSel = selectorHintFor(anchorEl);
|
|
837
|
-
anchorRelated =
|
|
838
|
-
dom.some((d) => {
|
|
839
|
-
if (!d.selectorHint || !anchorSel) {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
return d.selectorHint === anchorSel;
|
|
843
|
-
}) ||
|
|
844
|
-
dom.some((d) => {
|
|
845
|
-
if (!d.boundingBox) {
|
|
846
|
-
return false;
|
|
847
|
-
}
|
|
848
|
-
const r = anchorEl.getBoundingClientRect();
|
|
849
|
-
const bb = d.boundingBox;
|
|
850
|
-
// Overlap heuristic
|
|
851
|
-
const overlaps = r.left < bb.x + bb.width &&
|
|
852
|
-
r.left + r.width > bb.x &&
|
|
853
|
-
r.top < bb.y + bb.height &&
|
|
854
|
-
r.top + r.height > bb.y;
|
|
855
|
-
return overlaps;
|
|
856
|
-
});
|
|
857
|
-
if (typeof anchorX === 'number' &&
|
|
858
|
-
typeof anchorY === 'number' &&
|
|
859
|
-
dom.length > 0) {
|
|
860
|
-
let best = null;
|
|
861
|
-
for (const item of dom) {
|
|
862
|
-
if (!item.boundingBox) {
|
|
863
|
-
continue;
|
|
864
|
-
}
|
|
865
|
-
const cx = item.boundingBox.x +
|
|
866
|
-
item.boundingBox.width / 2;
|
|
867
|
-
const cy = item.boundingBox.y +
|
|
868
|
-
item.boundingBox.height / 2;
|
|
869
|
-
const dx = cx - anchorX;
|
|
870
|
-
const dy = cy - anchorY;
|
|
871
|
-
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
872
|
-
if (best === null) {
|
|
873
|
-
best = dist;
|
|
874
|
-
}
|
|
875
|
-
else {
|
|
876
|
-
if (dist < best) {
|
|
877
|
-
best = dist;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
proximityPx = best;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
let score = 0;
|
|
885
|
-
score += nameMatched ? 3.0 : 0;
|
|
886
|
-
score += debugSourceMatched ? 3.0 : 0;
|
|
887
|
-
score += anchorRelated ? 2.0 : 0;
|
|
888
|
-
// Favor some DOM footprint (but cap influence)
|
|
889
|
-
score += Math.min(1.5, dom.length / 25);
|
|
890
|
-
if (typeof proximityPx === 'number' &&
|
|
891
|
-
Number.isFinite(proximityPx)) {
|
|
892
|
-
// 0px => +1.5, 300px => ~+0.3, 1000px => tiny
|
|
893
|
-
const p = Math.max(0, Math.min(1, 1 - proximityPx / 1000));
|
|
894
|
-
score += 1.5 * p;
|
|
895
|
-
}
|
|
896
|
-
return {
|
|
897
|
-
score,
|
|
898
|
-
nameMatched,
|
|
899
|
-
debugSourceMatched,
|
|
900
|
-
anchorRelated,
|
|
901
|
-
proximityPx,
|
|
902
|
-
dom,
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
/**
|
|
906
|
-
* -----------------------------------------------------------------------------
|
|
907
|
-
* Main logic
|
|
908
|
-
* -----------------------------------------------------------------------------
|
|
909
|
-
*/
|
|
910
|
-
const hook = getDevtoolsHook();
|
|
911
|
-
const reactDetected = Boolean(hook);
|
|
912
|
-
// Tell the user explicitly how to make this better
|
|
913
|
-
if (!reactDetected) {
|
|
914
|
-
notes.push('React DevTools hook was not detected (__REACT_DEVTOOLS_GLOBAL_HOOK__).');
|
|
915
|
-
notes.push('If you are using a persistent/headful browser profile, install the "React Developer Tools" Chrome extension in that profile (manual step by the user) to enable reliable root discovery and component search.');
|
|
916
|
-
}
|
|
917
|
-
else {
|
|
918
|
-
notes.push('React DevTools hook detected. Root discovery will use getFiberRoots() (more reliable).');
|
|
919
|
-
}
|
|
920
|
-
const fiberDetected = (() => {
|
|
921
|
-
const body = document.body;
|
|
922
|
-
if (!body) {
|
|
923
|
-
return false;
|
|
924
|
-
}
|
|
925
|
-
const f = getFiberFromDomElement(body);
|
|
926
|
-
if (f) {
|
|
927
|
-
return true;
|
|
928
|
-
}
|
|
929
|
-
const seeds = getSeedFibersFromDomScan(1);
|
|
930
|
-
if (seeds.length > 0) {
|
|
931
|
-
return true;
|
|
932
|
-
}
|
|
933
|
-
return false;
|
|
934
|
-
})();
|
|
935
|
-
if (!fiberDetected) {
|
|
936
|
-
notes.push('No React Fiber pointers were detected on DOM nodes (__reactFiber$...). This can happen if the page is not React, React has not rendered yet, or internals are unavailable.');
|
|
937
|
-
}
|
|
938
|
-
else {
|
|
939
|
-
notes.push('React Fiber pointers detected on DOM nodes. Anchor-based mapping may still work without the DevTools hook (best-effort).');
|
|
940
|
-
}
|
|
941
|
-
const anchorEl = pickAnchorElement(anchorSelectorEval, anchorXEval, anchorYEval);
|
|
942
|
-
// 1) Anchor-based selection (works without hook if DOM fiber exists)
|
|
943
|
-
let anchorSelected = null;
|
|
944
|
-
if (anchorEl) {
|
|
945
|
-
const anchorFiber = getFiberFromDomElement(anchorEl);
|
|
946
|
-
if (anchorFiber) {
|
|
947
|
-
const candidateA = anchorFiber;
|
|
948
|
-
const candidateB = anchorFiber.alternate ?? null;
|
|
949
|
-
// Prefer the side that appears more "connected"
|
|
950
|
-
let chosen = candidateA;
|
|
951
|
-
if (candidateB) {
|
|
952
|
-
if ((candidateB.child || candidateB.sibling) &&
|
|
953
|
-
!(candidateA.child || candidateA.sibling)) {
|
|
954
|
-
chosen = candidateB;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
const nearestComponent = firstMeaningfulComponentAbove(chosen);
|
|
958
|
-
if (nearestComponent) {
|
|
959
|
-
anchorSelected = nearestComponent;
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
notes.push('Anchor fiber found but no meaningful component fiber was found above it.');
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
else {
|
|
966
|
-
notes.push('Anchor element found but React fiber was not found on it.');
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
// 2) Discover roots (hook or DOM scan)
|
|
970
|
-
let roots = [];
|
|
971
|
-
let rootDiscovery = 'none';
|
|
972
|
-
if (hook) {
|
|
973
|
-
const r = getAllRootsFromHook(hook);
|
|
974
|
-
if (r.length > 0) {
|
|
975
|
-
roots = r;
|
|
976
|
-
rootDiscovery = 'devtools-hook';
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
if (roots.length === 0) {
|
|
980
|
-
if (fiberDetected) {
|
|
981
|
-
const seeds = getSeedFibersFromDomScan(10);
|
|
982
|
-
if (seeds.length > 0) {
|
|
983
|
-
roots = seeds;
|
|
984
|
-
rootDiscovery = 'dom-fiber-scan';
|
|
985
|
-
notes.push('Using DOM fiber scan as fallback root discovery (best-effort).');
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
// 3) Query search (optional)
|
|
990
|
-
const hasQuery = Boolean(normalizeStr(componentNameEval)) ||
|
|
991
|
-
Boolean(normalizeStr(fileNameHintEval)) ||
|
|
992
|
-
typeof lineNumberEval === 'number';
|
|
993
|
-
const query = {
|
|
994
|
-
componentName: normalizeStr(componentNameEval) ?? undefined,
|
|
995
|
-
matchStrategy: matchStrategyEval,
|
|
996
|
-
fileNameHint: normalizeStr(fileNameHintEval) ?? undefined,
|
|
997
|
-
lineNumber: typeof lineNumberEval === 'number'
|
|
998
|
-
? lineNumberEval
|
|
999
|
-
: undefined,
|
|
1000
|
-
};
|
|
1001
|
-
let queryMatches = [];
|
|
1002
|
-
if (hasQuery) {
|
|
1003
|
-
if (roots.length > 0) {
|
|
1004
|
-
queryMatches = findFibersByQuery(roots, query, Math.max(1, Math.floor(maxMatchesEval)));
|
|
1005
|
-
}
|
|
1006
|
-
else {
|
|
1007
|
-
notes.push('Query was provided but no roots could be discovered. Provide an anchorSelector/anchorX+anchorY so we can map via DOM fiber pointers.');
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
// 4) Candidate pool
|
|
1011
|
-
const candidates = [];
|
|
1012
|
-
if (anchorSelected) {
|
|
1013
|
-
candidates.push(anchorSelected);
|
|
1014
|
-
}
|
|
1015
|
-
for (const f of queryMatches) {
|
|
1016
|
-
candidates.push(f);
|
|
1017
|
-
}
|
|
1018
|
-
// De-dupe candidate pool by alternate key
|
|
1019
|
-
const uniq = [];
|
|
1020
|
-
const seenCand = new Set();
|
|
1021
|
-
for (const f of candidates) {
|
|
1022
|
-
const key = f?.alternate ?? f;
|
|
1023
|
-
if (f && !seenCand.has(key)) {
|
|
1024
|
-
seenCand.add(key);
|
|
1025
|
-
uniq.push(f);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
if (uniq.length === 0) {
|
|
1029
|
-
// Hook missing should be explicit in the notes (user action)
|
|
1030
|
-
if (!reactDetected) {
|
|
1031
|
-
notes.push('No candidates found. Without the DevTools hook, component search may be unreliable unless you provide a strong anchor.');
|
|
1032
|
-
}
|
|
1033
|
-
return {
|
|
1034
|
-
reactDetected,
|
|
1035
|
-
fiberDetected,
|
|
1036
|
-
rootDiscovery,
|
|
1037
|
-
component: null,
|
|
1038
|
-
candidates: [],
|
|
1039
|
-
elements: [],
|
|
1040
|
-
notes: [
|
|
1041
|
-
...notes,
|
|
1042
|
-
'Failed to select a target component fiber. Provide a better anchor, or ensure React is present and render has happened.',
|
|
1043
|
-
],
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
// 5) Score candidates and pick the best
|
|
1047
|
-
const scored = [];
|
|
1048
|
-
for (const f of uniq) {
|
|
1049
|
-
const s = scoreCandidate(f, anchorEl, anchorXEval, anchorYEval, query, maxElementsEval, onlyVisibleEval, onlyInViewportEval, textPreviewMaxLengthEval);
|
|
1050
|
-
let selection = 'unknown';
|
|
1051
|
-
const isAnchor = anchorSelected
|
|
1052
|
-
? (anchorSelected.alternate ?? anchorSelected) ===
|
|
1053
|
-
(f.alternate ?? f)
|
|
1054
|
-
: false;
|
|
1055
|
-
const isQuery = queryMatches.some((qf) => (qf.alternate ?? qf) === (f.alternate ?? f));
|
|
1056
|
-
if (isAnchor && isQuery) {
|
|
1057
|
-
selection = 'query+anchor';
|
|
1058
|
-
}
|
|
1059
|
-
else {
|
|
1060
|
-
if (isAnchor) {
|
|
1061
|
-
selection = 'anchor';
|
|
1062
|
-
}
|
|
1063
|
-
else {
|
|
1064
|
-
if (isQuery) {
|
|
1065
|
-
selection = 'query';
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
selection = 'unknown';
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
const name = getFiberTypeName(f);
|
|
1073
|
-
const displayName = getDisplayName(f);
|
|
1074
|
-
const debugSource = getDebugSource(f);
|
|
1075
|
-
const componentStack = buildComponentStack(f, stackLimit);
|
|
1076
|
-
const meta = {
|
|
1077
|
-
name,
|
|
1078
|
-
displayName,
|
|
1079
|
-
debugSource,
|
|
1080
|
-
componentStack,
|
|
1081
|
-
selection,
|
|
1082
|
-
scoring: {
|
|
1083
|
-
score: s.score,
|
|
1084
|
-
nameMatched: s.nameMatched,
|
|
1085
|
-
debugSourceMatched: s.debugSourceMatched,
|
|
1086
|
-
anchorRelated: s.anchorRelated,
|
|
1087
|
-
proximityPx: s.proximityPx,
|
|
1088
|
-
},
|
|
1089
|
-
};
|
|
1090
|
-
scored.push({
|
|
1091
|
-
fiber: f,
|
|
1092
|
-
meta,
|
|
1093
|
-
dom: s.dom,
|
|
1094
|
-
domFootprintCount: s.dom.length,
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
scored.sort((a, b) => (b.meta.scoring?.score ?? 0) -
|
|
1098
|
-
(a.meta.scoring?.score ?? 0));
|
|
1099
|
-
const best = scored[0];
|
|
1100
|
-
const outCandidates = scored
|
|
1101
|
-
.slice(0, Math.max(1, Math.floor(maxMatchesEval)))
|
|
1102
|
-
.map((c) => {
|
|
1103
|
-
return {
|
|
1104
|
-
...c.meta,
|
|
1105
|
-
domFootprintCount: c.domFootprintCount,
|
|
1106
|
-
};
|
|
1107
|
-
});
|
|
1108
|
-
const elements = best.dom;
|
|
1109
|
-
// Notes about debug source / missing info
|
|
1110
|
-
if (hasQuery &&
|
|
1111
|
-
(query.fileNameHint ||
|
|
1112
|
-
typeof query.lineNumber === 'number')) {
|
|
1113
|
-
const anyHasDebug = outCandidates.some((c) => {
|
|
1114
|
-
if (c.debugSource?.fileName) {
|
|
1115
|
-
return true;
|
|
1116
|
-
}
|
|
1117
|
-
if (c.debugSource?.lineNumber !== null) {
|
|
1118
|
-
return true;
|
|
1119
|
-
}
|
|
1120
|
-
return false;
|
|
1121
|
-
});
|
|
1122
|
-
if (!anyHasDebug) {
|
|
1123
|
-
notes.push('debugSource hints were provided, but no _debugSource information was found on matched fibers. This is common in production builds and some dev toolchains.');
|
|
1124
|
-
notes.push('React DevTools may still display "Rendered by <file>:<line>" using sourcemaps/stack traces even when fiber._debugSource is null.');
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
if (elements.length >= maxElementsEval) {
|
|
1128
|
-
notes.push(`Element list was truncated at maxElements=${maxElementsEval}. Increase maxElements if needed.`);
|
|
1129
|
-
}
|
|
1130
|
-
if (elements.length === 0) {
|
|
1131
|
-
notes.push('No DOM elements were returned for the selected component. It may render no host elements, or filtering (onlyVisible/onlyInViewport) removed them.');
|
|
1132
|
-
}
|
|
1133
|
-
// Explain hook vs fallback
|
|
1134
|
-
if (!reactDetected && fiberDetected) {
|
|
1135
|
-
notes.push('React DevTools hook was not detected, but DOM fiber pointers were found. Using DOM-fiber scanning and anchor-based mapping (best-effort).');
|
|
1136
|
-
}
|
|
1137
|
-
else {
|
|
1138
|
-
if (!reactDetected && !fiberDetected) {
|
|
1139
|
-
notes.push('React DevTools hook was not detected and no DOM fiber pointers were found. Component-to-DOM mapping is unavailable on this page.');
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
return {
|
|
1143
|
-
reactDetected,
|
|
1144
|
-
fiberDetected,
|
|
1145
|
-
rootDiscovery,
|
|
1146
|
-
component: best.meta,
|
|
1147
|
-
candidates: outCandidates,
|
|
1148
|
-
elements,
|
|
1149
|
-
notes: [
|
|
1150
|
-
...notes,
|
|
1151
|
-
'Component metadata is best-effort. Wrappers (memo/forwardRef/HOCs), Suspense/Offscreen, and portals may make names/stacks noisy.',
|
|
1152
|
-
],
|
|
1153
|
-
};
|
|
1154
|
-
}, {
|
|
1155
|
-
anchorSelectorEval: typeof anchorSelector === 'string' &&
|
|
1156
|
-
anchorSelector.trim()
|
|
1157
|
-
? anchorSelector.trim()
|
|
1158
|
-
: undefined,
|
|
1159
|
-
anchorXEval: typeof anchorX === 'number' && Number.isFinite(anchorX)
|
|
1160
|
-
? Math.floor(anchorX)
|
|
1161
|
-
: undefined,
|
|
1162
|
-
anchorYEval: typeof anchorY === 'number' && Number.isFinite(anchorY)
|
|
1163
|
-
? Math.floor(anchorY)
|
|
1164
|
-
: undefined,
|
|
1165
|
-
componentNameEval: typeof componentName === 'string' &&
|
|
1166
|
-
componentName.trim()
|
|
1167
|
-
? componentName.trim()
|
|
1168
|
-
: undefined,
|
|
1169
|
-
matchStrategyEval: matchStrategy,
|
|
1170
|
-
fileNameHintEval: typeof fileNameHint === 'string' && fileNameHint.trim()
|
|
1171
|
-
? fileNameHint.trim()
|
|
1172
|
-
: undefined,
|
|
1173
|
-
lineNumberEval: typeof lineNumber === 'number' &&
|
|
1174
|
-
Number.isFinite(lineNumber)
|
|
1175
|
-
? Math.floor(lineNumber)
|
|
1176
|
-
: undefined,
|
|
1177
|
-
maxElementsEval: Math.max(1, Math.floor(maxElements)),
|
|
1178
|
-
onlyVisibleEval: Boolean(onlyVisible),
|
|
1179
|
-
onlyInViewportEval: Boolean(onlyInViewport),
|
|
1180
|
-
textPreviewMaxLengthEval: Math.max(1, Math.floor(textPreviewMaxLength)),
|
|
1181
|
-
maxMatchesEval: Math.max(1, Math.floor(maxMatches)),
|
|
1182
|
-
maxRootsScan: INTERNAL_MAX_ROOTS_SCAN,
|
|
1183
|
-
maxFibersVisited: INTERNAL_MAX_FIBERS_VISITED,
|
|
1184
|
-
stackLimit: DEFAULT_STACK_LIMIT,
|
|
1185
|
-
});
|
|
1186
|
-
return result;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
exports.GetElementForComponent = GetElementForComponent;
|
|
1190
|
-
//# sourceMappingURL=get-element-for-component.js.map
|