@react-component-selector-mcp/react 1.0.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/dist/index.d.ts +21 -0
- package/dist/index.js +1198 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1198 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useState, useRef, useEffect } from 'react';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
5
|
+
import { nanoid } from 'nanoid';
|
|
6
|
+
import { SourceMapConsumer } from 'source-map-js';
|
|
7
|
+
import { toPng } from 'html-to-image';
|
|
8
|
+
|
|
9
|
+
var ComponentTypeSchema = z.enum(["function", "class", "forwardRef", "memo"]);
|
|
10
|
+
var ComponentInfoSchema = z.object({
|
|
11
|
+
name: z.string(),
|
|
12
|
+
type: ComponentTypeSchema
|
|
13
|
+
});
|
|
14
|
+
var SourceLocationSchema = z.object({
|
|
15
|
+
filePath: z.string().nullable(),
|
|
16
|
+
lineNumber: z.number().nullable(),
|
|
17
|
+
columnNumber: z.number().nullable()
|
|
18
|
+
});
|
|
19
|
+
var BoundingRectSchema = z.object({
|
|
20
|
+
x: z.number(),
|
|
21
|
+
y: z.number(),
|
|
22
|
+
width: z.number(),
|
|
23
|
+
height: z.number(),
|
|
24
|
+
top: z.number(),
|
|
25
|
+
right: z.number(),
|
|
26
|
+
bottom: z.number(),
|
|
27
|
+
left: z.number()
|
|
28
|
+
});
|
|
29
|
+
var DOMInfoSchema = z.object({
|
|
30
|
+
tagName: z.string(),
|
|
31
|
+
className: z.string().nullable(),
|
|
32
|
+
boundingRect: BoundingRectSchema
|
|
33
|
+
});
|
|
34
|
+
var ScreenshotSchema = z.object({
|
|
35
|
+
dataUrl: z.string(),
|
|
36
|
+
width: z.number(),
|
|
37
|
+
height: z.number()
|
|
38
|
+
});
|
|
39
|
+
var SelectionContextSchema = z.object({
|
|
40
|
+
pageUrl: z.string(),
|
|
41
|
+
parentComponents: z.array(z.string())
|
|
42
|
+
});
|
|
43
|
+
var SelectionDataSchema = z.object({
|
|
44
|
+
id: z.string(),
|
|
45
|
+
timestamp: z.number(),
|
|
46
|
+
component: ComponentInfoSchema,
|
|
47
|
+
source: SourceLocationSchema,
|
|
48
|
+
props: z.record(z.unknown()),
|
|
49
|
+
state: z.record(z.unknown()).nullable(),
|
|
50
|
+
dom: DOMInfoSchema,
|
|
51
|
+
screenshot: ScreenshotSchema,
|
|
52
|
+
context: SelectionContextSchema
|
|
53
|
+
});
|
|
54
|
+
var MessageTypeSchema = z.enum([
|
|
55
|
+
"selection",
|
|
56
|
+
"ping",
|
|
57
|
+
"pong",
|
|
58
|
+
"connect",
|
|
59
|
+
"disconnect",
|
|
60
|
+
"error",
|
|
61
|
+
"selectionMode"
|
|
62
|
+
]);
|
|
63
|
+
var BaseMessageSchema = z.object({
|
|
64
|
+
type: MessageTypeSchema,
|
|
65
|
+
timestamp: z.number()
|
|
66
|
+
});
|
|
67
|
+
var SelectionMessageSchema = BaseMessageSchema.extend({
|
|
68
|
+
type: z.literal("selection"),
|
|
69
|
+
payload: SelectionDataSchema
|
|
70
|
+
});
|
|
71
|
+
var PingMessageSchema = BaseMessageSchema.extend({
|
|
72
|
+
type: z.literal("ping")
|
|
73
|
+
});
|
|
74
|
+
var PongMessageSchema = BaseMessageSchema.extend({
|
|
75
|
+
type: z.literal("pong")
|
|
76
|
+
});
|
|
77
|
+
var ConnectMessageSchema = BaseMessageSchema.extend({
|
|
78
|
+
type: z.literal("connect"),
|
|
79
|
+
payload: z.object({
|
|
80
|
+
clientId: z.string(),
|
|
81
|
+
userAgent: z.string().optional()
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
var DisconnectMessageSchema = BaseMessageSchema.extend({
|
|
85
|
+
type: z.literal("disconnect"),
|
|
86
|
+
payload: z.object({
|
|
87
|
+
clientId: z.string(),
|
|
88
|
+
reason: z.string().optional()
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
var ErrorMessageSchema = BaseMessageSchema.extend({
|
|
92
|
+
type: z.literal("error"),
|
|
93
|
+
payload: z.object({
|
|
94
|
+
code: z.string(),
|
|
95
|
+
message: z.string()
|
|
96
|
+
})
|
|
97
|
+
});
|
|
98
|
+
var SelectionModeMessageSchema = BaseMessageSchema.extend({
|
|
99
|
+
type: z.literal("selectionMode"),
|
|
100
|
+
payload: z.object({
|
|
101
|
+
enabled: z.boolean(),
|
|
102
|
+
message: z.string().optional()
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
var WebSocketMessageSchema = z.discriminatedUnion("type", [
|
|
106
|
+
SelectionMessageSchema,
|
|
107
|
+
PingMessageSchema,
|
|
108
|
+
PongMessageSchema,
|
|
109
|
+
ConnectMessageSchema,
|
|
110
|
+
DisconnectMessageSchema,
|
|
111
|
+
ErrorMessageSchema,
|
|
112
|
+
SelectionModeMessageSchema
|
|
113
|
+
]);
|
|
114
|
+
function createMessage(type, payload) {
|
|
115
|
+
const base = { type, timestamp: Date.now() };
|
|
116
|
+
if (payload !== void 0) {
|
|
117
|
+
return { ...base, payload };
|
|
118
|
+
}
|
|
119
|
+
return base;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/hooks/useWebSocketClient.ts
|
|
123
|
+
function useWebSocketClient(options) {
|
|
124
|
+
const { port } = options;
|
|
125
|
+
const onSelectionModeChangeRef = useRef(options.onSelectionModeChange);
|
|
126
|
+
const onConnectionChangeRef = useRef(options.onConnectionChange);
|
|
127
|
+
onSelectionModeChangeRef.current = options.onSelectionModeChange;
|
|
128
|
+
onConnectionChangeRef.current = options.onConnectionChange;
|
|
129
|
+
const wsRef = useRef(null);
|
|
130
|
+
const reconnectTimeoutRef = useRef(null);
|
|
131
|
+
const pingIntervalRef = useRef(null);
|
|
132
|
+
const isCleaningUpRef = useRef(false);
|
|
133
|
+
const [connected, setConnected] = useState(false);
|
|
134
|
+
const [clientId, setClientId] = useState(null);
|
|
135
|
+
const sendSelection = useCallback((data) => {
|
|
136
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
137
|
+
const message = createMessage("selection", data);
|
|
138
|
+
wsRef.current.send(JSON.stringify(message));
|
|
139
|
+
} else {
|
|
140
|
+
console.warn("[component-picker] Cannot send selection - not connected");
|
|
141
|
+
}
|
|
142
|
+
}, []);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
isCleaningUpRef.current = false;
|
|
145
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const connect = () => {
|
|
149
|
+
if (wsRef.current?.readyState === WebSocket.OPEN || isCleaningUpRef.current) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
154
|
+
ws.onopen = () => {
|
|
155
|
+
if (isCleaningUpRef.current) {
|
|
156
|
+
ws.close();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
console.log("[component-picker] Connected to server");
|
|
160
|
+
setConnected(true);
|
|
161
|
+
onConnectionChangeRef.current?.(true);
|
|
162
|
+
pingIntervalRef.current = setInterval(() => {
|
|
163
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
164
|
+
ws.send(JSON.stringify(createMessage("ping")));
|
|
165
|
+
}
|
|
166
|
+
}, 25e3);
|
|
167
|
+
};
|
|
168
|
+
ws.onclose = () => {
|
|
169
|
+
console.log("[component-picker] Disconnected from server");
|
|
170
|
+
setConnected(false);
|
|
171
|
+
setClientId(null);
|
|
172
|
+
onConnectionChangeRef.current?.(false);
|
|
173
|
+
if (pingIntervalRef.current) {
|
|
174
|
+
clearInterval(pingIntervalRef.current);
|
|
175
|
+
pingIntervalRef.current = null;
|
|
176
|
+
}
|
|
177
|
+
if (!isCleaningUpRef.current) {
|
|
178
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
179
|
+
connect();
|
|
180
|
+
}, 3e3);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
ws.onerror = () => {
|
|
184
|
+
};
|
|
185
|
+
ws.onmessage = (event) => {
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(event.data);
|
|
188
|
+
const result = WebSocketMessageSchema.safeParse(parsed);
|
|
189
|
+
if (!result.success) {
|
|
190
|
+
console.warn("[component-picker] Invalid message:", result.error);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const message = result.data;
|
|
194
|
+
switch (message.type) {
|
|
195
|
+
case "connect":
|
|
196
|
+
setClientId(message.payload.clientId);
|
|
197
|
+
break;
|
|
198
|
+
case "selectionMode":
|
|
199
|
+
onSelectionModeChangeRef.current?.(
|
|
200
|
+
message.payload.enabled,
|
|
201
|
+
message.payload.message
|
|
202
|
+
);
|
|
203
|
+
break;
|
|
204
|
+
case "pong":
|
|
205
|
+
break;
|
|
206
|
+
default:
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error("[component-picker] Error handling message:", error);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
wsRef.current = ws;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error("[component-picker] Failed to connect:", error);
|
|
216
|
+
if (!isCleaningUpRef.current) {
|
|
217
|
+
reconnectTimeoutRef.current = setTimeout(() => {
|
|
218
|
+
connect();
|
|
219
|
+
}, 3e3);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
connect();
|
|
224
|
+
return () => {
|
|
225
|
+
isCleaningUpRef.current = true;
|
|
226
|
+
if (reconnectTimeoutRef.current) {
|
|
227
|
+
clearTimeout(reconnectTimeoutRef.current);
|
|
228
|
+
reconnectTimeoutRef.current = null;
|
|
229
|
+
}
|
|
230
|
+
if (pingIntervalRef.current) {
|
|
231
|
+
clearInterval(pingIntervalRef.current);
|
|
232
|
+
pingIntervalRef.current = null;
|
|
233
|
+
}
|
|
234
|
+
if (wsRef.current) {
|
|
235
|
+
wsRef.current.close();
|
|
236
|
+
wsRef.current = null;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}, [port]);
|
|
240
|
+
return {
|
|
241
|
+
connected,
|
|
242
|
+
sendSelection,
|
|
243
|
+
clientId
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function useKeyboardShortcut(options) {
|
|
247
|
+
const { key = "C", onTrigger, enabled = true } = options;
|
|
248
|
+
const handleKeyDown = useCallback(
|
|
249
|
+
(event) => {
|
|
250
|
+
if (!enabled) return;
|
|
251
|
+
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
|
252
|
+
const modifierKey = isMac ? event.metaKey : event.ctrlKey;
|
|
253
|
+
if (modifierKey && event.altKey && event.key.toUpperCase() === key.toUpperCase()) {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
event.stopPropagation();
|
|
256
|
+
onTrigger();
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
[key, onTrigger, enabled]
|
|
260
|
+
);
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (!enabled) return;
|
|
263
|
+
window.addEventListener("keydown", handleKeyDown, true);
|
|
264
|
+
return () => {
|
|
265
|
+
window.removeEventListener("keydown", handleKeyDown, true);
|
|
266
|
+
};
|
|
267
|
+
}, [handleKeyDown, enabled]);
|
|
268
|
+
}
|
|
269
|
+
function useSelectionMode() {
|
|
270
|
+
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
|
271
|
+
const [selectionMessage, setSelectionMessage] = useState();
|
|
272
|
+
const enableSelectionMode = useCallback((message) => {
|
|
273
|
+
setIsSelectionMode(true);
|
|
274
|
+
setSelectionMessage(message);
|
|
275
|
+
}, []);
|
|
276
|
+
const disableSelectionMode = useCallback(() => {
|
|
277
|
+
setIsSelectionMode(false);
|
|
278
|
+
setSelectionMessage(void 0);
|
|
279
|
+
}, []);
|
|
280
|
+
const toggleSelectionMode = useCallback(() => {
|
|
281
|
+
setIsSelectionMode((prev) => !prev);
|
|
282
|
+
if (isSelectionMode) {
|
|
283
|
+
setSelectionMessage(void 0);
|
|
284
|
+
}
|
|
285
|
+
}, [isSelectionMode]);
|
|
286
|
+
return {
|
|
287
|
+
isSelectionMode,
|
|
288
|
+
selectionMessage,
|
|
289
|
+
enableSelectionMode,
|
|
290
|
+
disableSelectionMode,
|
|
291
|
+
toggleSelectionMode
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
var FIBER_TAGS = {
|
|
295
|
+
FunctionComponent: 0,
|
|
296
|
+
ClassComponent: 1,
|
|
297
|
+
ForwardRef: 11,
|
|
298
|
+
MemoComponent: 14,
|
|
299
|
+
SimpleMemoComponent: 15
|
|
300
|
+
};
|
|
301
|
+
function useFiberInspector() {
|
|
302
|
+
const getFiberFromElement = useCallback((element) => {
|
|
303
|
+
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers) {
|
|
304
|
+
for (const renderer of window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.values()) {
|
|
305
|
+
const fiber = renderer.findFiberByHostInstance?.(element);
|
|
306
|
+
if (fiber) return fiber;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const fiberKey = Object.keys(element).find(
|
|
310
|
+
(key) => key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")
|
|
311
|
+
);
|
|
312
|
+
if (fiberKey) {
|
|
313
|
+
const fiber = element[fiberKey];
|
|
314
|
+
return fiber ?? null;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
}, []);
|
|
318
|
+
const getComponentType = useCallback((fiber) => {
|
|
319
|
+
switch (fiber.tag) {
|
|
320
|
+
case FIBER_TAGS.ClassComponent:
|
|
321
|
+
return "class";
|
|
322
|
+
case FIBER_TAGS.ForwardRef:
|
|
323
|
+
return "forwardRef";
|
|
324
|
+
case FIBER_TAGS.MemoComponent:
|
|
325
|
+
case FIBER_TAGS.SimpleMemoComponent:
|
|
326
|
+
return "memo";
|
|
327
|
+
case FIBER_TAGS.FunctionComponent:
|
|
328
|
+
default:
|
|
329
|
+
return "function";
|
|
330
|
+
}
|
|
331
|
+
}, []);
|
|
332
|
+
const getComponentName2 = useCallback((fiber) => {
|
|
333
|
+
const type = fiber.type;
|
|
334
|
+
if (!type) return "Unknown";
|
|
335
|
+
if (typeof type === "function") {
|
|
336
|
+
const fn = type;
|
|
337
|
+
return fn.displayName || fn.name || "Anonymous";
|
|
338
|
+
}
|
|
339
|
+
if (typeof type === "object" && type !== null) {
|
|
340
|
+
const obj = type;
|
|
341
|
+
if (obj.displayName) return obj.displayName;
|
|
342
|
+
if (obj.render) return obj.render.displayName || obj.render.name || "ForwardRef";
|
|
343
|
+
if (obj.type) return obj.type.displayName || obj.type.name || "Memo";
|
|
344
|
+
}
|
|
345
|
+
return "Unknown";
|
|
346
|
+
}, []);
|
|
347
|
+
const findNearestComponentFiber = useCallback((fiber) => {
|
|
348
|
+
let current = fiber;
|
|
349
|
+
while (current) {
|
|
350
|
+
const tag = current.tag;
|
|
351
|
+
if (tag === FIBER_TAGS.FunctionComponent || tag === FIBER_TAGS.ClassComponent || tag === FIBER_TAGS.ForwardRef || tag === FIBER_TAGS.MemoComponent || tag === FIBER_TAGS.SimpleMemoComponent) {
|
|
352
|
+
const name = getComponentName2(current);
|
|
353
|
+
if (!name.startsWith("_") && name !== "Unknown" && name !== "Anonymous") {
|
|
354
|
+
return current;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
current = current.return;
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}, [getComponentName2]);
|
|
361
|
+
const getParentComponents = useCallback(
|
|
362
|
+
(fiber) => {
|
|
363
|
+
const parents = [];
|
|
364
|
+
let current = fiber.return;
|
|
365
|
+
while (current && parents.length < 10) {
|
|
366
|
+
const tag = current.tag;
|
|
367
|
+
if (tag === FIBER_TAGS.FunctionComponent || tag === FIBER_TAGS.ClassComponent || tag === FIBER_TAGS.ForwardRef || tag === FIBER_TAGS.MemoComponent || tag === FIBER_TAGS.SimpleMemoComponent) {
|
|
368
|
+
const name = getComponentName2(current);
|
|
369
|
+
if (!name.startsWith("_") && name !== "Unknown") {
|
|
370
|
+
parents.push(name);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
current = current.return;
|
|
374
|
+
}
|
|
375
|
+
return parents;
|
|
376
|
+
},
|
|
377
|
+
[getComponentName2]
|
|
378
|
+
);
|
|
379
|
+
const serializeProps = useCallback((props) => {
|
|
380
|
+
const result = {};
|
|
381
|
+
for (const [key, value] of Object.entries(props)) {
|
|
382
|
+
if (key === "children" || key === "key" || key === "ref") continue;
|
|
383
|
+
try {
|
|
384
|
+
if (typeof value === "function") {
|
|
385
|
+
result[key] = "[Function]";
|
|
386
|
+
} else if (value instanceof Element) {
|
|
387
|
+
result[key] = "[Element]";
|
|
388
|
+
} else if (typeof value === "object" && value !== null) {
|
|
389
|
+
JSON.stringify(value);
|
|
390
|
+
result[key] = value;
|
|
391
|
+
} else {
|
|
392
|
+
result[key] = value;
|
|
393
|
+
}
|
|
394
|
+
} catch {
|
|
395
|
+
result[key] = "[Circular or Unserializable]";
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return result;
|
|
399
|
+
}, []);
|
|
400
|
+
const extractState = useCallback((fiber) => {
|
|
401
|
+
if (fiber.tag !== FIBER_TAGS.ClassComponent) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
const instance = fiber.stateNode;
|
|
405
|
+
if (instance?.state) {
|
|
406
|
+
try {
|
|
407
|
+
JSON.stringify(instance.state);
|
|
408
|
+
return instance.state;
|
|
409
|
+
} catch {
|
|
410
|
+
return { error: "[Unserializable state]" };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}, []);
|
|
415
|
+
const extractFiberData = useCallback(
|
|
416
|
+
(fiber) => {
|
|
417
|
+
return {
|
|
418
|
+
componentInfo: {
|
|
419
|
+
name: getComponentName2(fiber),
|
|
420
|
+
type: getComponentType(fiber)
|
|
421
|
+
},
|
|
422
|
+
props: serializeProps(fiber.memoizedProps || {}),
|
|
423
|
+
state: extractState(fiber),
|
|
424
|
+
parentComponents: getParentComponents(fiber),
|
|
425
|
+
debugSource: {
|
|
426
|
+
fileName: fiber._debugSource?.fileName ?? null,
|
|
427
|
+
lineNumber: fiber._debugSource?.lineNumber ?? null,
|
|
428
|
+
columnNumber: fiber._debugSource?.columnNumber ?? null
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
},
|
|
432
|
+
[getComponentName2, getComponentType, serializeProps, extractState, getParentComponents]
|
|
433
|
+
);
|
|
434
|
+
return {
|
|
435
|
+
getFiberFromElement,
|
|
436
|
+
extractFiberData,
|
|
437
|
+
findNearestComponentFiber
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function SelectionOverlay({
|
|
441
|
+
enabled,
|
|
442
|
+
message,
|
|
443
|
+
onSelect,
|
|
444
|
+
onCancel
|
|
445
|
+
}) {
|
|
446
|
+
const [highlight, setHighlight] = useState(null);
|
|
447
|
+
const hoveredElementRef = useRef(null);
|
|
448
|
+
const getComponentName2 = useCallback((element) => {
|
|
449
|
+
const fiberKey = Object.keys(element).find(
|
|
450
|
+
(k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$")
|
|
451
|
+
);
|
|
452
|
+
let reactName = null;
|
|
453
|
+
if (fiberKey) {
|
|
454
|
+
const fiber = element[fiberKey];
|
|
455
|
+
if (fiber?.type && typeof fiber.type === "function") {
|
|
456
|
+
const fn = fiber.type;
|
|
457
|
+
reactName = fn.displayName || fn.name || null;
|
|
458
|
+
}
|
|
459
|
+
if (!reactName || reactName === "div" || reactName === "button") {
|
|
460
|
+
let current = fiber;
|
|
461
|
+
while (current?.return) {
|
|
462
|
+
current = current.return;
|
|
463
|
+
if (current?.type && typeof current.type === "function") {
|
|
464
|
+
const fn = current.type;
|
|
465
|
+
const name = fn.displayName || fn.name;
|
|
466
|
+
if (name && !["Fragment", "Suspense", "Provider", "Consumer"].includes(name)) {
|
|
467
|
+
reactName = name;
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const textContent = element.textContent?.trim().slice(0, 20) || "";
|
|
475
|
+
const textSuffix = textContent ? ` "${textContent}${element.textContent && element.textContent.length > 20 ? "..." : ""}"` : "";
|
|
476
|
+
if (reactName && !["div", "button", "span", "p", "h1", "h2", "h3"].includes(reactName.toLowerCase())) {
|
|
477
|
+
return `<${reactName}>${textSuffix}`;
|
|
478
|
+
}
|
|
479
|
+
if (element.className && typeof element.className === "string" && element.className.trim()) {
|
|
480
|
+
const classes = element.className.trim().split(/\s+/)[0];
|
|
481
|
+
return `.${classes}${textSuffix}`;
|
|
482
|
+
}
|
|
483
|
+
return `<${element.tagName.toLowerCase()}>${textSuffix}`;
|
|
484
|
+
}, []);
|
|
485
|
+
const handleMouseMove = useCallback(
|
|
486
|
+
(event) => {
|
|
487
|
+
if (!enabled) return;
|
|
488
|
+
const target = event.target;
|
|
489
|
+
if (target.closest("[data-component-picker]")) {
|
|
490
|
+
setHighlight(null);
|
|
491
|
+
hoveredElementRef.current = null;
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (["HTML", "BODY", "SCRIPT", "STYLE", "NOSCRIPT"].includes(target.tagName)) {
|
|
495
|
+
setHighlight(null);
|
|
496
|
+
hoveredElementRef.current = null;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const rect = target.getBoundingClientRect();
|
|
500
|
+
hoveredElementRef.current = target;
|
|
501
|
+
setHighlight({
|
|
502
|
+
top: rect.top,
|
|
503
|
+
left: rect.left,
|
|
504
|
+
width: rect.width,
|
|
505
|
+
height: rect.height,
|
|
506
|
+
componentName: getComponentName2(target)
|
|
507
|
+
});
|
|
508
|
+
},
|
|
509
|
+
[enabled, getComponentName2]
|
|
510
|
+
);
|
|
511
|
+
const handleClick = useCallback(
|
|
512
|
+
(event) => {
|
|
513
|
+
if (!enabled) return;
|
|
514
|
+
const target = event.target;
|
|
515
|
+
if (target.closest("[data-component-picker]")) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
event.stopPropagation();
|
|
520
|
+
if (hoveredElementRef.current) {
|
|
521
|
+
onSelect(hoveredElementRef.current);
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
[enabled, onSelect]
|
|
525
|
+
);
|
|
526
|
+
const handleKeyDown = useCallback(
|
|
527
|
+
(event) => {
|
|
528
|
+
if (!enabled) return;
|
|
529
|
+
if (event.key === "Escape") {
|
|
530
|
+
event.preventDefault();
|
|
531
|
+
onCancel();
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
[enabled, onCancel]
|
|
535
|
+
);
|
|
536
|
+
useEffect(() => {
|
|
537
|
+
if (!enabled) {
|
|
538
|
+
setHighlight(null);
|
|
539
|
+
hoveredElementRef.current = null;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
document.addEventListener("mousemove", handleMouseMove, true);
|
|
543
|
+
document.addEventListener("click", handleClick, true);
|
|
544
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
545
|
+
document.body.style.cursor = "crosshair";
|
|
546
|
+
return () => {
|
|
547
|
+
document.removeEventListener("mousemove", handleMouseMove, true);
|
|
548
|
+
document.removeEventListener("click", handleClick, true);
|
|
549
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
550
|
+
document.body.style.cursor = "";
|
|
551
|
+
};
|
|
552
|
+
}, [enabled, handleMouseMove, handleClick, handleKeyDown]);
|
|
553
|
+
if (!enabled) return null;
|
|
554
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
555
|
+
highlight && /* @__PURE__ */ jsx(
|
|
556
|
+
"div",
|
|
557
|
+
{
|
|
558
|
+
"data-component-picker": "highlight",
|
|
559
|
+
style: {
|
|
560
|
+
position: "fixed",
|
|
561
|
+
top: highlight.top,
|
|
562
|
+
left: highlight.left,
|
|
563
|
+
width: highlight.width,
|
|
564
|
+
height: highlight.height,
|
|
565
|
+
border: "2px solid #3b82f6",
|
|
566
|
+
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
|
567
|
+
pointerEvents: "none",
|
|
568
|
+
zIndex: 999998,
|
|
569
|
+
boxSizing: "border-box"
|
|
570
|
+
},
|
|
571
|
+
children: /* @__PURE__ */ jsx(
|
|
572
|
+
"div",
|
|
573
|
+
{
|
|
574
|
+
style: {
|
|
575
|
+
position: "absolute",
|
|
576
|
+
top: -24,
|
|
577
|
+
left: -2,
|
|
578
|
+
padding: "2px 8px",
|
|
579
|
+
backgroundColor: "#3b82f6",
|
|
580
|
+
color: "white",
|
|
581
|
+
fontSize: "12px",
|
|
582
|
+
fontFamily: "system-ui, sans-serif",
|
|
583
|
+
fontWeight: 500,
|
|
584
|
+
borderRadius: "4px 4px 0 0",
|
|
585
|
+
whiteSpace: "nowrap",
|
|
586
|
+
maxWidth: "300px",
|
|
587
|
+
overflow: "hidden",
|
|
588
|
+
textOverflow: "ellipsis"
|
|
589
|
+
},
|
|
590
|
+
children: highlight.componentName
|
|
591
|
+
}
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
),
|
|
595
|
+
/* @__PURE__ */ jsxs(
|
|
596
|
+
"div",
|
|
597
|
+
{
|
|
598
|
+
"data-component-picker": "info-bar",
|
|
599
|
+
style: {
|
|
600
|
+
position: "fixed",
|
|
601
|
+
top: 0,
|
|
602
|
+
left: 0,
|
|
603
|
+
right: 0,
|
|
604
|
+
padding: "12px 16px",
|
|
605
|
+
backgroundColor: "#3b82f6",
|
|
606
|
+
color: "white",
|
|
607
|
+
fontFamily: "system-ui, sans-serif",
|
|
608
|
+
fontSize: "14px",
|
|
609
|
+
textAlign: "center",
|
|
610
|
+
zIndex: 999999,
|
|
611
|
+
display: "flex",
|
|
612
|
+
justifyContent: "center",
|
|
613
|
+
alignItems: "center",
|
|
614
|
+
gap: "16px",
|
|
615
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)"
|
|
616
|
+
},
|
|
617
|
+
children: [
|
|
618
|
+
/* @__PURE__ */ jsx("span", { style: { fontWeight: 500 }, children: message || "Click a component to select it" }),
|
|
619
|
+
/* @__PURE__ */ jsx("span", { style: { opacity: 0.8, fontSize: "12px" }, children: "Press ESC to cancel" })
|
|
620
|
+
]
|
|
621
|
+
}
|
|
622
|
+
)
|
|
623
|
+
] });
|
|
624
|
+
}
|
|
625
|
+
var sourceMapCache = /* @__PURE__ */ new Map();
|
|
626
|
+
var failedFetches = /* @__PURE__ */ new Set();
|
|
627
|
+
async function fetchSourceMap(scriptUrl) {
|
|
628
|
+
if (sourceMapCache.has(scriptUrl)) {
|
|
629
|
+
return sourceMapCache.get(scriptUrl) || null;
|
|
630
|
+
}
|
|
631
|
+
if (failedFetches.has(scriptUrl)) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
const sourceMapUrl = await findSourceMapUrl(scriptUrl);
|
|
636
|
+
if (!sourceMapUrl) {
|
|
637
|
+
failedFetches.add(scriptUrl);
|
|
638
|
+
sourceMapCache.set(scriptUrl, null);
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
const response = await fetch(sourceMapUrl);
|
|
642
|
+
if (!response.ok) {
|
|
643
|
+
failedFetches.add(scriptUrl);
|
|
644
|
+
sourceMapCache.set(scriptUrl, null);
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
const sourceMapData = await response.json();
|
|
648
|
+
const consumer = new SourceMapConsumer(sourceMapData);
|
|
649
|
+
sourceMapCache.set(scriptUrl, consumer);
|
|
650
|
+
return consumer;
|
|
651
|
+
} catch (error) {
|
|
652
|
+
console.debug("[component-picker] Failed to fetch source map:", error);
|
|
653
|
+
failedFetches.add(scriptUrl);
|
|
654
|
+
sourceMapCache.set(scriptUrl, null);
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function findSourceMapUrl(scriptUrl) {
|
|
659
|
+
try {
|
|
660
|
+
const scriptResponse = await fetch(scriptUrl);
|
|
661
|
+
if (scriptResponse.ok) {
|
|
662
|
+
const scriptContent = await scriptResponse.text();
|
|
663
|
+
const match = scriptContent.match(
|
|
664
|
+
/\/\/[#@]\s*sourceMappingURL=([^\s'"]+)/
|
|
665
|
+
);
|
|
666
|
+
if (match?.[1]) {
|
|
667
|
+
const mapUrl = match[1];
|
|
668
|
+
if (mapUrl.startsWith("data:")) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
return new URL(mapUrl, scriptUrl).href;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const patterns = [
|
|
675
|
+
`${scriptUrl}.map`,
|
|
676
|
+
scriptUrl.replace(/\.js$/, ".js.map"),
|
|
677
|
+
scriptUrl.replace(/\.mjs$/, ".mjs.map")
|
|
678
|
+
];
|
|
679
|
+
for (const pattern of patterns) {
|
|
680
|
+
try {
|
|
681
|
+
const response = await fetch(pattern, { method: "HEAD" });
|
|
682
|
+
if (response.ok) {
|
|
683
|
+
return pattern;
|
|
684
|
+
}
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
} catch {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function resolveSourcePath(sourcePath, scriptUrl) {
|
|
694
|
+
if (sourcePath.startsWith("webpack://")) {
|
|
695
|
+
const match = sourcePath.match(/webpack:\/\/(?:[^/]+)?\/\.?\/?(.+)/);
|
|
696
|
+
if (match?.[1]) {
|
|
697
|
+
return match[1];
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (sourcePath.startsWith("[project]/")) {
|
|
701
|
+
return sourcePath.replace("[project]/", "");
|
|
702
|
+
}
|
|
703
|
+
if (sourcePath.startsWith("./") || sourcePath.startsWith("../")) {
|
|
704
|
+
try {
|
|
705
|
+
const scriptDir = scriptUrl.substring(0, scriptUrl.lastIndexOf("/"));
|
|
706
|
+
const resolved = new URL(sourcePath, scriptDir + "/").pathname;
|
|
707
|
+
return resolved.replace(/^\//, "");
|
|
708
|
+
} catch {
|
|
709
|
+
return sourcePath;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (sourcePath.startsWith("/")) {
|
|
713
|
+
return sourcePath.substring(1);
|
|
714
|
+
}
|
|
715
|
+
return sourcePath;
|
|
716
|
+
}
|
|
717
|
+
async function searchSourceMapsForComponent(componentName) {
|
|
718
|
+
const scripts = Array.from(document.querySelectorAll("script[src]")).map((s) => s.src).filter((src) => src && !src.includes("node_modules"));
|
|
719
|
+
const nextScripts = Array.from(
|
|
720
|
+
document.querySelectorAll('script[src*="/_next/"]')
|
|
721
|
+
).map((s) => s.src);
|
|
722
|
+
const allScripts = [.../* @__PURE__ */ new Set([...scripts, ...nextScripts])];
|
|
723
|
+
for (const scriptUrl of allScripts) {
|
|
724
|
+
const consumer = await fetchSourceMap(scriptUrl);
|
|
725
|
+
if (!consumer) continue;
|
|
726
|
+
const sources = consumer.sources || [];
|
|
727
|
+
for (const source of sources) {
|
|
728
|
+
const fileName = source.split("/").pop() || "";
|
|
729
|
+
const baseName = fileName.replace(/\.(tsx?|jsx?)$/, "");
|
|
730
|
+
if (baseName === componentName || fileName.toLowerCase().includes(componentName.toLowerCase())) {
|
|
731
|
+
try {
|
|
732
|
+
let firstMapping = null;
|
|
733
|
+
consumer.eachMapping((mapping) => {
|
|
734
|
+
if (mapping.source === source && !firstMapping) {
|
|
735
|
+
if (mapping.name === componentName && mapping.originalLine !== null && mapping.originalColumn !== null) {
|
|
736
|
+
firstMapping = {
|
|
737
|
+
line: mapping.originalLine,
|
|
738
|
+
column: mapping.originalColumn
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
if (firstMapping !== null) {
|
|
744
|
+
return {
|
|
745
|
+
source: resolveSourcePath(source, scriptUrl),
|
|
746
|
+
line: firstMapping.line,
|
|
747
|
+
column: firstMapping.column
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
if (baseName === componentName) {
|
|
751
|
+
return {
|
|
752
|
+
source: resolveSourcePath(source, scriptUrl),
|
|
753
|
+
line: 1,
|
|
754
|
+
column: 0
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/utils/stackTraceParser.ts
|
|
766
|
+
function parseStackTrace(error) {
|
|
767
|
+
const stack = error.stack;
|
|
768
|
+
if (!stack) return [];
|
|
769
|
+
const frames = [];
|
|
770
|
+
const lines = stack.split("\n");
|
|
771
|
+
for (const line of lines) {
|
|
772
|
+
const frame = parseStackLine(line);
|
|
773
|
+
if (frame) {
|
|
774
|
+
frames.push(frame);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return frames;
|
|
778
|
+
}
|
|
779
|
+
function parseStackLine(line) {
|
|
780
|
+
const chromeMatch = line.match(
|
|
781
|
+
/^\s*at\s+(?:async\s+)?(?:(\S+)\s+)?\(?(https?:\/\/[^)]+|file:\/\/[^)]+):(\d+):(\d+)\)?/
|
|
782
|
+
);
|
|
783
|
+
if (chromeMatch) {
|
|
784
|
+
return {
|
|
785
|
+
functionName: chromeMatch[1] || null,
|
|
786
|
+
url: chromeMatch[2],
|
|
787
|
+
lineNumber: parseInt(chromeMatch[3], 10),
|
|
788
|
+
columnNumber: parseInt(chromeMatch[4], 10)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const firefoxMatch = line.match(
|
|
792
|
+
/^(?:(\S*)@)?(https?:\/\/[^:]+|file:\/\/[^:]+):(\d+):(\d+)/
|
|
793
|
+
);
|
|
794
|
+
if (firefoxMatch) {
|
|
795
|
+
return {
|
|
796
|
+
functionName: firefoxMatch[1] || null,
|
|
797
|
+
url: firefoxMatch[2],
|
|
798
|
+
lineNumber: parseInt(firefoxMatch[3], 10),
|
|
799
|
+
columnNumber: parseInt(firefoxMatch[4], 10)
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
function filterInternalFrames(frames) {
|
|
805
|
+
const internalPatterns = [
|
|
806
|
+
/node_modules/,
|
|
807
|
+
/react-dom/,
|
|
808
|
+
/react\.production/,
|
|
809
|
+
/react\.development/,
|
|
810
|
+
/scheduler/,
|
|
811
|
+
/\/_next\/static\/chunks\/webpack/,
|
|
812
|
+
/\/__webpack_/,
|
|
813
|
+
/\/turbopack-/,
|
|
814
|
+
// React internal function names
|
|
815
|
+
/^(?:renderWithHooks|mountIndeterminateComponent|beginWork|performUnitOfWork)/,
|
|
816
|
+
/^(?:callCallback|invokeGuardedCallbackDev|invokeGuardedCallback)/,
|
|
817
|
+
/^(?:commitRoot|flushSync|batchedUpdates)/
|
|
818
|
+
];
|
|
819
|
+
return frames.filter((frame) => {
|
|
820
|
+
for (const pattern of internalPatterns) {
|
|
821
|
+
if (pattern.test(frame.url)) {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (frame.functionName) {
|
|
826
|
+
for (const pattern of internalPatterns) {
|
|
827
|
+
if (pattern.test(frame.functionName)) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return true;
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/utils/sourceLocationResolver.ts
|
|
837
|
+
async function resolveSourceLocation(fiber, element) {
|
|
838
|
+
const debugSourceResult = tryDebugSource(fiber);
|
|
839
|
+
if (debugSourceResult.filePath) {
|
|
840
|
+
return debugSourceResult;
|
|
841
|
+
}
|
|
842
|
+
const sourceMapResult = await trySourceMapResolution(fiber);
|
|
843
|
+
if (sourceMapResult?.filePath) {
|
|
844
|
+
return sourceMapResult;
|
|
845
|
+
}
|
|
846
|
+
const stackResult = tryStackTraceParsing();
|
|
847
|
+
if (stackResult?.filePath) {
|
|
848
|
+
return stackResult;
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
filePath: null,
|
|
852
|
+
lineNumber: null,
|
|
853
|
+
columnNumber: null
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
function tryDebugSource(fiber) {
|
|
857
|
+
if (fiber) {
|
|
858
|
+
console.log("[component-picker] Fiber keys:", Object.keys(fiber));
|
|
859
|
+
console.log("[component-picker] _debugSource:", fiber._debugSource);
|
|
860
|
+
console.log("[component-picker] _debugInfo:", fiber._debugInfo);
|
|
861
|
+
}
|
|
862
|
+
if (!fiber?._debugSource) {
|
|
863
|
+
return { filePath: null, lineNumber: null, columnNumber: null };
|
|
864
|
+
}
|
|
865
|
+
const { fileName, lineNumber, columnNumber } = fiber._debugSource;
|
|
866
|
+
if (!fileName) {
|
|
867
|
+
return { filePath: null, lineNumber: null, columnNumber: null };
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
filePath: formatFilePath(fileName),
|
|
871
|
+
lineNumber: lineNumber ?? null,
|
|
872
|
+
columnNumber: columnNumber ?? null
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
async function trySourceMapResolution(fiber, _element) {
|
|
876
|
+
try {
|
|
877
|
+
const componentName = fiber?.type ? getComponentName(fiber.type) : null;
|
|
878
|
+
if (!componentName) {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
const result = await searchSourceMapsForComponent(componentName);
|
|
882
|
+
if (!result) {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
filePath: formatFilePath(result.source),
|
|
887
|
+
lineNumber: result.line,
|
|
888
|
+
columnNumber: result.column
|
|
889
|
+
};
|
|
890
|
+
} catch (error) {
|
|
891
|
+
console.debug("[component-picker] Source map resolution failed:", error);
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function tryStackTraceParsing() {
|
|
896
|
+
try {
|
|
897
|
+
const error = new Error();
|
|
898
|
+
const frames = parseStackTrace(error);
|
|
899
|
+
const userFrames = filterInternalFrames(frames);
|
|
900
|
+
if (userFrames.length === 0) {
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
const frame = userFrames[0];
|
|
904
|
+
const filePath = extractFilePathFromUrl(frame.url);
|
|
905
|
+
if (!filePath) {
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
return {
|
|
909
|
+
filePath,
|
|
910
|
+
lineNumber: frame.lineNumber,
|
|
911
|
+
columnNumber: frame.columnNumber
|
|
912
|
+
};
|
|
913
|
+
} catch {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function getComponentName(type) {
|
|
918
|
+
if (!type) return null;
|
|
919
|
+
if (typeof type === "function") {
|
|
920
|
+
return type.displayName || type.name || null;
|
|
921
|
+
}
|
|
922
|
+
if (typeof type === "object" && type !== null) {
|
|
923
|
+
const obj = type;
|
|
924
|
+
return obj.displayName || obj.render?.displayName || obj.render?.name || null;
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
function extractFilePathFromUrl(url) {
|
|
929
|
+
try {
|
|
930
|
+
const urlObj = new URL(url);
|
|
931
|
+
let path = urlObj.pathname;
|
|
932
|
+
path = path.replace(/^\/+/, "");
|
|
933
|
+
path = path.replace(/^_next\/static\/chunks\//, "").replace(/^_next\/static\/[^/]+\/pages\//, "pages/").replace(/^\/@fs\//, "").replace(/^@vite\//, "").replace(/^webpack:\/\/[^/]+\//, "").replace(/^\[project\]\//, "");
|
|
934
|
+
path = path.split("?")[0]?.split("#")[0] || path;
|
|
935
|
+
if (!path.includes("/") && !path.match(/\.(tsx?|jsx?|mjs)$/)) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
return path || null;
|
|
939
|
+
} catch {
|
|
940
|
+
return url;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function formatFilePath(filePath) {
|
|
944
|
+
if (!filePath) return null;
|
|
945
|
+
let cleaned = filePath.replace(/^webpack:\/\/[^/]+\//, "").replace(/^\.\//g, "").replace(/^\/+/, "").replace(/^\[project\]\//, "");
|
|
946
|
+
cleaned = cleaned.replace(/\\/g, "/");
|
|
947
|
+
return cleaned;
|
|
948
|
+
}
|
|
949
|
+
var DEFAULT_OPTIONS = {
|
|
950
|
+
maxWidth: 800,
|
|
951
|
+
maxHeight: 600,
|
|
952
|
+
backgroundColor: "#ffffff",
|
|
953
|
+
pixelRatio: 1,
|
|
954
|
+
padding: 10
|
|
955
|
+
};
|
|
956
|
+
async function captureScreenshot(element, options = {}) {
|
|
957
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
958
|
+
try {
|
|
959
|
+
const rect = element.getBoundingClientRect();
|
|
960
|
+
const width = Math.min(rect.width + opts.padding * 2, opts.maxWidth);
|
|
961
|
+
const height = Math.min(rect.height + opts.padding * 2, opts.maxHeight);
|
|
962
|
+
const dataUrl = await toPng(element, {
|
|
963
|
+
backgroundColor: opts.backgroundColor,
|
|
964
|
+
pixelRatio: opts.pixelRatio,
|
|
965
|
+
width: rect.width,
|
|
966
|
+
height: rect.height,
|
|
967
|
+
style: {
|
|
968
|
+
margin: "0",
|
|
969
|
+
padding: "0"
|
|
970
|
+
},
|
|
971
|
+
filter: (node) => {
|
|
972
|
+
if (node instanceof Element) {
|
|
973
|
+
if (node.hasAttribute("data-component-picker")) {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
return {
|
|
981
|
+
dataUrl,
|
|
982
|
+
width: Math.round(width),
|
|
983
|
+
height: Math.round(height)
|
|
984
|
+
};
|
|
985
|
+
} catch (error) {
|
|
986
|
+
console.error("[component-picker] Screenshot capture failed:", error);
|
|
987
|
+
return {
|
|
988
|
+
dataUrl: createFallbackScreenshot(element),
|
|
989
|
+
width: 200,
|
|
990
|
+
height: 100
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
function createFallbackScreenshot(element) {
|
|
995
|
+
const canvas = document.createElement("canvas");
|
|
996
|
+
canvas.width = 200;
|
|
997
|
+
canvas.height = 100;
|
|
998
|
+
const ctx = canvas.getContext("2d");
|
|
999
|
+
if (ctx) {
|
|
1000
|
+
ctx.fillStyle = "#f0f0f0";
|
|
1001
|
+
ctx.fillRect(0, 0, 200, 100);
|
|
1002
|
+
ctx.fillStyle = "#666";
|
|
1003
|
+
ctx.font = "12px sans-serif";
|
|
1004
|
+
ctx.textAlign = "center";
|
|
1005
|
+
ctx.fillText("Screenshot unavailable", 100, 45);
|
|
1006
|
+
ctx.fillText(element.tagName.toLowerCase(), 100, 65);
|
|
1007
|
+
}
|
|
1008
|
+
return canvas.toDataURL("image/png");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// src/utils/componentMetadata.ts
|
|
1012
|
+
function extractDOMInfo(element) {
|
|
1013
|
+
const rect = element.getBoundingClientRect();
|
|
1014
|
+
return {
|
|
1015
|
+
tagName: element.tagName.toLowerCase(),
|
|
1016
|
+
className: element.className || null,
|
|
1017
|
+
boundingRect: {
|
|
1018
|
+
x: rect.x,
|
|
1019
|
+
y: rect.y,
|
|
1020
|
+
width: rect.width,
|
|
1021
|
+
height: rect.height,
|
|
1022
|
+
top: rect.top,
|
|
1023
|
+
right: rect.right,
|
|
1024
|
+
bottom: rect.bottom,
|
|
1025
|
+
left: rect.left
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
async function buildSelectionData(element, fiberData, options = {}) {
|
|
1030
|
+
const fiber = options.fiber ?? {
|
|
1031
|
+
_debugSource: fiberData.debugSource
|
|
1032
|
+
};
|
|
1033
|
+
const source = await resolveSourceLocation(fiber);
|
|
1034
|
+
const screenshot = await captureScreenshot(element, options.screenshotOptions);
|
|
1035
|
+
const selectionData = {
|
|
1036
|
+
id: nanoid(),
|
|
1037
|
+
timestamp: Date.now(),
|
|
1038
|
+
component: fiberData.componentInfo,
|
|
1039
|
+
source: {
|
|
1040
|
+
filePath: formatFilePath(source.filePath),
|
|
1041
|
+
lineNumber: source.lineNumber,
|
|
1042
|
+
columnNumber: source.columnNumber
|
|
1043
|
+
},
|
|
1044
|
+
props: fiberData.props,
|
|
1045
|
+
state: fiberData.state,
|
|
1046
|
+
dom: extractDOMInfo(element),
|
|
1047
|
+
screenshot,
|
|
1048
|
+
context: {
|
|
1049
|
+
pageUrl: window.location.href,
|
|
1050
|
+
parentComponents: fiberData.parentComponents
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
return selectionData;
|
|
1054
|
+
}
|
|
1055
|
+
function ComponentPicker(props) {
|
|
1056
|
+
if (process.env.NODE_ENV !== "development") {
|
|
1057
|
+
return props.children;
|
|
1058
|
+
}
|
|
1059
|
+
return /* @__PURE__ */ jsx(ComponentPickerImpl, { ...props });
|
|
1060
|
+
}
|
|
1061
|
+
function ComponentPickerImpl({
|
|
1062
|
+
port = 3333,
|
|
1063
|
+
children,
|
|
1064
|
+
shortcutKey = "C",
|
|
1065
|
+
onConnectionChange,
|
|
1066
|
+
onSelect
|
|
1067
|
+
}) {
|
|
1068
|
+
const { isSelectionMode, selectionMessage, enableSelectionMode, disableSelectionMode } = useSelectionMode();
|
|
1069
|
+
const { getFiberFromElement, extractFiberData, findNearestComponentFiber } = useFiberInspector();
|
|
1070
|
+
const { connected, sendSelection } = useWebSocketClient({
|
|
1071
|
+
port,
|
|
1072
|
+
onSelectionModeChange: (enabled, message) => {
|
|
1073
|
+
if (enabled) {
|
|
1074
|
+
enableSelectionMode(message);
|
|
1075
|
+
} else {
|
|
1076
|
+
disableSelectionMode();
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
onConnectionChange
|
|
1080
|
+
});
|
|
1081
|
+
useKeyboardShortcut({
|
|
1082
|
+
key: shortcutKey,
|
|
1083
|
+
onTrigger: () => {
|
|
1084
|
+
if (isSelectionMode) {
|
|
1085
|
+
disableSelectionMode();
|
|
1086
|
+
} else {
|
|
1087
|
+
enableSelectionMode();
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
enabled: connected
|
|
1091
|
+
});
|
|
1092
|
+
const handleSelect = useCallback(
|
|
1093
|
+
async (element) => {
|
|
1094
|
+
try {
|
|
1095
|
+
const fiber = getFiberFromElement(element);
|
|
1096
|
+
if (!fiber) {
|
|
1097
|
+
console.warn("[component-picker] No React fiber found for element");
|
|
1098
|
+
disableSelectionMode();
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const componentFiber = findNearestComponentFiber(fiber);
|
|
1102
|
+
if (!componentFiber) {
|
|
1103
|
+
console.warn("[component-picker] No component fiber found");
|
|
1104
|
+
disableSelectionMode();
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const fiberData = extractFiberData(componentFiber);
|
|
1108
|
+
const selectionData = await buildSelectionData(element, fiberData, {
|
|
1109
|
+
fiber: componentFiber
|
|
1110
|
+
});
|
|
1111
|
+
sendSelection(selectionData);
|
|
1112
|
+
onSelect?.(selectionData.component.name, selectionData.source.filePath);
|
|
1113
|
+
console.log(
|
|
1114
|
+
`[component-picker] Selected: ${selectionData.component.name}`,
|
|
1115
|
+
selectionData.source.filePath ? `at ${selectionData.source.filePath}:${selectionData.source.lineNumber}` : ""
|
|
1116
|
+
);
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
console.error("[component-picker] Selection error:", error);
|
|
1119
|
+
} finally {
|
|
1120
|
+
disableSelectionMode();
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
[
|
|
1124
|
+
getFiberFromElement,
|
|
1125
|
+
findNearestComponentFiber,
|
|
1126
|
+
extractFiberData,
|
|
1127
|
+
sendSelection,
|
|
1128
|
+
disableSelectionMode,
|
|
1129
|
+
onSelect
|
|
1130
|
+
]
|
|
1131
|
+
);
|
|
1132
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1133
|
+
children,
|
|
1134
|
+
/* @__PURE__ */ jsx(
|
|
1135
|
+
SelectionOverlay,
|
|
1136
|
+
{
|
|
1137
|
+
enabled: isSelectionMode,
|
|
1138
|
+
message: selectionMessage,
|
|
1139
|
+
onSelect: handleSelect,
|
|
1140
|
+
onCancel: disableSelectionMode
|
|
1141
|
+
}
|
|
1142
|
+
),
|
|
1143
|
+
process.env.NODE_ENV === "development" && /* @__PURE__ */ jsxs(
|
|
1144
|
+
"button",
|
|
1145
|
+
{
|
|
1146
|
+
"data-component-picker": "status",
|
|
1147
|
+
onClick: () => {
|
|
1148
|
+
if (!connected) return;
|
|
1149
|
+
if (isSelectionMode) {
|
|
1150
|
+
disableSelectionMode();
|
|
1151
|
+
} else {
|
|
1152
|
+
enableSelectionMode();
|
|
1153
|
+
}
|
|
1154
|
+
},
|
|
1155
|
+
style: {
|
|
1156
|
+
position: "fixed",
|
|
1157
|
+
bottom: 16,
|
|
1158
|
+
right: 16,
|
|
1159
|
+
padding: "8px 12px",
|
|
1160
|
+
backgroundColor: !connected ? "#ef4444" : isSelectionMode ? "#f59e0b" : "#22c55e",
|
|
1161
|
+
color: "white",
|
|
1162
|
+
borderRadius: "9999px",
|
|
1163
|
+
fontSize: "12px",
|
|
1164
|
+
fontFamily: "system-ui, sans-serif",
|
|
1165
|
+
fontWeight: 500,
|
|
1166
|
+
zIndex: 999997,
|
|
1167
|
+
opacity: 0.9,
|
|
1168
|
+
display: "flex",
|
|
1169
|
+
alignItems: "center",
|
|
1170
|
+
gap: "6px",
|
|
1171
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)",
|
|
1172
|
+
border: "none",
|
|
1173
|
+
cursor: connected ? "pointer" : "not-allowed",
|
|
1174
|
+
transition: "background-color 0.2s ease"
|
|
1175
|
+
},
|
|
1176
|
+
children: [
|
|
1177
|
+
/* @__PURE__ */ jsx(
|
|
1178
|
+
"span",
|
|
1179
|
+
{
|
|
1180
|
+
style: {
|
|
1181
|
+
width: 8,
|
|
1182
|
+
height: 8,
|
|
1183
|
+
borderRadius: "50%",
|
|
1184
|
+
backgroundColor: "white",
|
|
1185
|
+
animation: !connected ? "pulse 2s infinite" : "none"
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
),
|
|
1189
|
+
!connected ? "Connecting..." : isSelectionMode ? "Click a Component" : "Select Component"
|
|
1190
|
+
]
|
|
1191
|
+
}
|
|
1192
|
+
)
|
|
1193
|
+
] });
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
export { ComponentPicker };
|
|
1197
|
+
//# sourceMappingURL=index.js.map
|
|
1198
|
+
//# sourceMappingURL=index.js.map
|