fixdog 0.0.1 → 0.0.2
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 +21 -0
- package/README.md +80 -433
- package/dist/api/client.d.ts +74 -0
- package/dist/components/ConversationalInputReact.d.ts +26 -0
- package/dist/components/ElementInfoDisplayReact.d.ts +9 -0
- package/dist/components/FixdogSidebarReact.d.ts +29 -0
- package/dist/fiber.d.ts +9 -0
- package/dist/index.cjs.js +33 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +6 -158
- package/dist/index.esm.js +29 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/inspector-B4F5CBT7.cjs.js +1159 -0
- package/dist/inspector-B4F5CBT7.cjs.js.map +1 -0
- package/dist/inspector-BL2pNjn-.cjs.js +1173 -0
- package/dist/inspector-BL2pNjn-.cjs.js.map +1 -0
- package/dist/inspector-Bg6uSvk0.esm.js +1273 -0
- package/dist/inspector-Bg6uSvk0.esm.js.map +1 -0
- package/dist/inspector-BuOffbVc.cjs.js +1280 -0
- package/dist/inspector-BuOffbVc.cjs.js.map +1 -0
- package/dist/inspector-CNgFkZOU.esm.js +1185 -0
- package/dist/inspector-CNgFkZOU.esm.js.map +1 -0
- package/dist/inspector-CPF1N9dL.esm.js +1185 -0
- package/dist/inspector-CPF1N9dL.esm.js.map +1 -0
- package/dist/inspector-CPGK5Lg7.esm.js +1155 -0
- package/dist/inspector-CPGK5Lg7.esm.js.map +1 -0
- package/dist/inspector-CWcTSREy.cjs.js +1174 -0
- package/dist/inspector-CWcTSREy.cjs.js.map +1 -0
- package/dist/inspector-Cn_bl9Io.cjs.js +1189 -0
- package/dist/inspector-Cn_bl9Io.cjs.js.map +1 -0
- package/dist/inspector-D9DuXirp.cjs.js +1189 -0
- package/dist/inspector-D9DuXirp.cjs.js.map +1 -0
- package/dist/inspector-DQEtAjyM.esm.js +1129 -0
- package/dist/inspector-DQEtAjyM.esm.js.map +1 -0
- package/dist/inspector-DVlU9p44.cjs.js +1189 -0
- package/dist/inspector-DVlU9p44.cjs.js.map +1 -0
- package/dist/inspector-DaRVppX9.cjs.js +1134 -0
- package/dist/inspector-DaRVppX9.cjs.js.map +1 -0
- package/dist/inspector-huqtI2MD.esm.js +1170 -0
- package/dist/inspector-huqtI2MD.esm.js.map +1 -0
- package/dist/inspector-spoCY1tf.esm.js +1169 -0
- package/dist/inspector-spoCY1tf.esm.js.map +1 -0
- package/dist/inspector-tY1kJK5_.esm.js +1185 -0
- package/dist/inspector-tY1kJK5_.esm.js.map +1 -0
- package/dist/inspector.d.ts +43 -0
- package/dist/keyboard.d.ts +10 -0
- package/dist/overlay.d.ts +31 -0
- package/dist/react/InspectorProvider.d.ts +6 -0
- package/dist/react/index.cjs.js +32 -0
- package/dist/react/index.cjs.js.map +1 -0
- package/dist/react/index.esm.js +30 -0
- package/dist/react/index.esm.js.map +1 -0
- package/dist/sidebar/SidebarRuntime.d.ts +8 -0
- package/dist/sidebar-runtime.esm.js +2122 -0
- package/dist/sidebar-runtime.esm.js.map +1 -0
- package/dist/sidebar-runtime.iife.js +2991 -0
- package/dist/styles/sidebarStyles.d.ts +2 -0
- package/dist/styles.d.ts +8 -0
- package/dist/types/sidebar.d.ts +62 -0
- package/dist/types.d.ts +47 -0
- package/dist/utils/cookies.d.ts +10 -0
- package/dist/utils/devMode.d.ts +17 -0
- package/dist/utils/sessionStorage.d.ts +19 -0
- package/package.json +57 -40
- package/USAGE.md +0 -77
- package/dist/client/index.d.mts +0 -110
- package/dist/client/index.d.ts +0 -110
- package/dist/client/index.js +0 -1601
- package/dist/client/index.mjs +0 -1582
- package/dist/client/init.d.mts +0 -67
- package/dist/client/init.d.ts +0 -67
- package/dist/client/init.js +0 -1609
- package/dist/client/init.mjs +0 -1593
- package/dist/index.d.mts +0 -158
- package/dist/index.js +0 -1635
- package/dist/index.mjs +0 -1606
- package/src/api/client.ts +0 -141
- package/src/client/index.ts +0 -75
- package/src/client/init.tsx +0 -78
- package/src/components/ConversationalInputReact.tsx +0 -406
- package/src/components/ElementInfoDisplayReact.tsx +0 -84
- package/src/components/UiDogSidebarReact.tsx +0 -49
- package/src/element-detector.ts +0 -186
- package/src/index.ts +0 -228
- package/src/instrument.ts +0 -171
- package/src/sidebar-initializer.ts +0 -171
- package/src/source-resolver.ts +0 -121
- package/src/styles/sidebarStyles.ts +0 -597
- package/src/types/css.d.ts +0 -9
- package/src/types/sidebar.ts +0 -56
- package/src/types.ts +0 -119
- package/tsconfig.json +0 -23
- package/tsup.config.ts +0 -40
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var sourceMapJs = require('source-map-js');
|
|
4
|
+
|
|
5
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
6
|
+
const STYLE_ELEMENT_ID = 'fixdog-sdk-styles';
|
|
7
|
+
const OVERLAY_CLASS = 'fixdog-overlay';
|
|
8
|
+
const TOOLTIP_CLASS = 'fixdog-tooltip';
|
|
9
|
+
const HIGHLIGHT_CLASS = 'fixdog-highlight';
|
|
10
|
+
const SELECTION_CLASS = 'fixdog-selection';
|
|
11
|
+
const SELECTION_BADGE_CLASS = 'fixdog-selection-badge';
|
|
12
|
+
// Selection highlight color (blue, distinct from hover cyan)
|
|
13
|
+
const SELECTION_COLOR = '#3b82f6';
|
|
14
|
+
const baseStyles = (options) => `
|
|
15
|
+
.${OVERLAY_CLASS} {
|
|
16
|
+
position: fixed;
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
border: 2px solid ${options.borderColor};
|
|
19
|
+
background: rgba(97, 218, 251, 0.08);
|
|
20
|
+
border-radius: 4px;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
transition: all 50ms ease-out;
|
|
23
|
+
z-index: ${options.zIndex};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.${TOOLTIP_CLASS} {
|
|
27
|
+
position: fixed;
|
|
28
|
+
pointer-events: none;
|
|
29
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
30
|
+
font-size: 12px;
|
|
31
|
+
line-height: 1.4;
|
|
32
|
+
color: #e5f6ff;
|
|
33
|
+
background: rgba(0, 0, 0, 0.78);
|
|
34
|
+
border: 1px solid ${options.borderColor};
|
|
35
|
+
border-radius: 6px;
|
|
36
|
+
padding: 8px 10px;
|
|
37
|
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
|
|
38
|
+
z-index: ${options.zIndex};
|
|
39
|
+
max-width: 320px;
|
|
40
|
+
transform: translateY(-4px);
|
|
41
|
+
transition: opacity 50ms ease-out, transform 50ms ease-out;
|
|
42
|
+
opacity: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.${TOOLTIP_CLASS} .fixdog-name {
|
|
46
|
+
display: block;
|
|
47
|
+
font-weight: 700;
|
|
48
|
+
color: ${options.borderColor};
|
|
49
|
+
margin-bottom: 2px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.${TOOLTIP_CLASS} .fixdog-file {
|
|
53
|
+
display: block;
|
|
54
|
+
color: #98c379;
|
|
55
|
+
word-break: break-all;
|
|
56
|
+
margin-bottom: 2px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.${TOOLTIP_CLASS} .fixdog-hint {
|
|
60
|
+
display: block;
|
|
61
|
+
color: #c9d1d9;
|
|
62
|
+
opacity: 0.8;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.${HIGHLIGHT_CLASS} {
|
|
66
|
+
outline: 2px solid ${options.borderColor};
|
|
67
|
+
outline-offset: 2px;
|
|
68
|
+
background: rgba(97, 218, 251, 0.08);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Selection highlight - persistent blue border for selected components */
|
|
72
|
+
.${SELECTION_CLASS} {
|
|
73
|
+
position: fixed;
|
|
74
|
+
pointer-events: none;
|
|
75
|
+
border: 2px solid ${SELECTION_COLOR};
|
|
76
|
+
background: rgba(59, 130, 246, 0.1);
|
|
77
|
+
border-radius: 4px;
|
|
78
|
+
box-sizing: border-box;
|
|
79
|
+
z-index: ${options.zIndex - 1};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Numbered badge for selected components */
|
|
83
|
+
.${SELECTION_BADGE_CLASS} {
|
|
84
|
+
position: fixed;
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
width: 24px;
|
|
87
|
+
height: 24px;
|
|
88
|
+
background: ${SELECTION_COLOR};
|
|
89
|
+
color: #ffffff;
|
|
90
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
font-weight: 700;
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
border-radius: 50%;
|
|
97
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
98
|
+
z-index: ${options.zIndex + 1};
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
103
|
+
class Overlay {
|
|
104
|
+
constructor(options) {
|
|
105
|
+
this.styleEl = null;
|
|
106
|
+
this.highlight = null;
|
|
107
|
+
this.tooltip = null;
|
|
108
|
+
this.mounted = false;
|
|
109
|
+
this.doc = document;
|
|
110
|
+
// Multi-selection tracking
|
|
111
|
+
this.selections = new Map();
|
|
112
|
+
this.options = options;
|
|
113
|
+
}
|
|
114
|
+
mount(doc = document) {
|
|
115
|
+
if (this.mounted)
|
|
116
|
+
return;
|
|
117
|
+
this.mounted = true;
|
|
118
|
+
this.doc = doc;
|
|
119
|
+
this.styleEl = doc.getElementById(STYLE_ELEMENT_ID);
|
|
120
|
+
if (!this.styleEl) {
|
|
121
|
+
this.styleEl = doc.createElement("style");
|
|
122
|
+
this.styleEl.id = STYLE_ELEMENT_ID;
|
|
123
|
+
this.styleEl.textContent = baseStyles(this.options);
|
|
124
|
+
doc.head.appendChild(this.styleEl);
|
|
125
|
+
}
|
|
126
|
+
this.highlight = doc.createElement("div");
|
|
127
|
+
this.highlight.className = `${OVERLAY_CLASS} ${HIGHLIGHT_CLASS}`;
|
|
128
|
+
this.tooltip = doc.createElement("div");
|
|
129
|
+
this.tooltip.className = TOOLTIP_CLASS;
|
|
130
|
+
doc.body.appendChild(this.highlight);
|
|
131
|
+
doc.body.appendChild(this.tooltip);
|
|
132
|
+
}
|
|
133
|
+
update(element, info) {
|
|
134
|
+
if (!this.mounted || !this.highlight || !this.tooltip)
|
|
135
|
+
return;
|
|
136
|
+
if (!element)
|
|
137
|
+
return this.hide();
|
|
138
|
+
const rect = element.getBoundingClientRect();
|
|
139
|
+
const position = {
|
|
140
|
+
top: rect.top,
|
|
141
|
+
left: rect.left,
|
|
142
|
+
width: rect.width,
|
|
143
|
+
height: rect.height,
|
|
144
|
+
};
|
|
145
|
+
this.applyHighlight(position);
|
|
146
|
+
this.applyTooltip(position, info);
|
|
147
|
+
}
|
|
148
|
+
hide() {
|
|
149
|
+
if (!this.highlight || !this.tooltip)
|
|
150
|
+
return;
|
|
151
|
+
this.highlight.style.display = "none";
|
|
152
|
+
this.tooltip.style.opacity = "0";
|
|
153
|
+
this.tooltip.style.transform = "translateY(-4px)";
|
|
154
|
+
}
|
|
155
|
+
destroy() {
|
|
156
|
+
if (!this.mounted)
|
|
157
|
+
return;
|
|
158
|
+
this.mounted = false;
|
|
159
|
+
this.hide();
|
|
160
|
+
this.clearSelections();
|
|
161
|
+
this.highlight?.remove();
|
|
162
|
+
this.tooltip?.remove();
|
|
163
|
+
// Do not remove style tag to allow re-use across mounts within the same page.
|
|
164
|
+
}
|
|
165
|
+
// Add a selection highlight with numbered badge
|
|
166
|
+
addSelection(id, element, componentName) {
|
|
167
|
+
if (!this.mounted)
|
|
168
|
+
return;
|
|
169
|
+
// Remove existing selection with same ID if any
|
|
170
|
+
this.removeSelection(id);
|
|
171
|
+
const rect = element.getBoundingClientRect();
|
|
172
|
+
// Create selection highlight
|
|
173
|
+
const highlight = this.doc.createElement("div");
|
|
174
|
+
highlight.className = `${OVERLAY_CLASS} ${SELECTION_CLASS}`;
|
|
175
|
+
highlight.style.top = `${rect.top}px`;
|
|
176
|
+
highlight.style.left = `${rect.left}px`;
|
|
177
|
+
highlight.style.width = `${rect.width}px`;
|
|
178
|
+
highlight.style.height = `${rect.height}px`;
|
|
179
|
+
highlight.dataset.selectionId = String(id);
|
|
180
|
+
// Create numbered badge
|
|
181
|
+
const badge = this.doc.createElement("div");
|
|
182
|
+
badge.className = SELECTION_BADGE_CLASS;
|
|
183
|
+
badge.textContent = String(id);
|
|
184
|
+
badge.style.top = `${rect.top - 12}px`;
|
|
185
|
+
badge.style.left = `${rect.left - 12}px`;
|
|
186
|
+
badge.dataset.selectionId = String(id);
|
|
187
|
+
this.doc.body.appendChild(highlight);
|
|
188
|
+
this.doc.body.appendChild(badge);
|
|
189
|
+
this.selections.set(id, { element, highlight, badge, componentName });
|
|
190
|
+
// Set up scroll/resize listener for this selection
|
|
191
|
+
this.updateSelectionPositions();
|
|
192
|
+
}
|
|
193
|
+
// Remove a specific selection
|
|
194
|
+
removeSelection(id) {
|
|
195
|
+
const selection = this.selections.get(id);
|
|
196
|
+
if (selection) {
|
|
197
|
+
selection.highlight.remove();
|
|
198
|
+
selection.badge.remove();
|
|
199
|
+
this.selections.delete(id);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Clear all selections
|
|
203
|
+
clearSelections() {
|
|
204
|
+
this.selections.forEach((selection) => {
|
|
205
|
+
selection.highlight.remove();
|
|
206
|
+
selection.badge.remove();
|
|
207
|
+
});
|
|
208
|
+
this.selections.clear();
|
|
209
|
+
}
|
|
210
|
+
// Update positions of all selection overlays (called on scroll/resize)
|
|
211
|
+
updateSelectionPositions() {
|
|
212
|
+
this.selections.forEach((selection, id) => {
|
|
213
|
+
const rect = selection.element.getBoundingClientRect();
|
|
214
|
+
selection.highlight.style.top = `${rect.top}px`;
|
|
215
|
+
selection.highlight.style.left = `${rect.left}px`;
|
|
216
|
+
selection.highlight.style.width = `${rect.width}px`;
|
|
217
|
+
selection.highlight.style.height = `${rect.height}px`;
|
|
218
|
+
selection.badge.style.top = `${rect.top - 12}px`;
|
|
219
|
+
selection.badge.style.left = `${rect.left - 12}px`;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
// Get all current selections
|
|
223
|
+
getSelections() {
|
|
224
|
+
return this.selections;
|
|
225
|
+
}
|
|
226
|
+
applyHighlight(pos) {
|
|
227
|
+
if (!this.highlight)
|
|
228
|
+
return;
|
|
229
|
+
this.highlight.style.display = "block";
|
|
230
|
+
this.highlight.style.top = `${pos.top}px`;
|
|
231
|
+
this.highlight.style.left = `${pos.left}px`;
|
|
232
|
+
this.highlight.style.width = `${pos.width}px`;
|
|
233
|
+
this.highlight.style.height = `${pos.height}px`;
|
|
234
|
+
}
|
|
235
|
+
applyTooltip(pos, info) {
|
|
236
|
+
if (!this.tooltip)
|
|
237
|
+
return;
|
|
238
|
+
// const name = info.componentName || "Unknown component";
|
|
239
|
+
const line = Number.isFinite(info.lineNumber) ? info.lineNumber : "?";
|
|
240
|
+
const column = Number.isFinite(info.columnNumber)
|
|
241
|
+
? `:${info.columnNumber}`
|
|
242
|
+
: "";
|
|
243
|
+
const file = info.fileName
|
|
244
|
+
? `${info.fileName}:${line}${column}`
|
|
245
|
+
: "Source not available - ensure dev mode";
|
|
246
|
+
this.tooltip.innerHTML = `
|
|
247
|
+
<span class="fixdog-name">${name}</span>
|
|
248
|
+
<span class="fixdog-file">${file}</span>
|
|
249
|
+
<span class="fixdog-hint">Click to log • Esc to cancel</span>
|
|
250
|
+
`;
|
|
251
|
+
this.tooltip.style.opacity = "1";
|
|
252
|
+
this.tooltip.style.transform = "translateY(0)";
|
|
253
|
+
// Positioning
|
|
254
|
+
this.positionTooltip(pos);
|
|
255
|
+
}
|
|
256
|
+
positionTooltip(pos) {
|
|
257
|
+
if (!this.tooltip)
|
|
258
|
+
return;
|
|
259
|
+
const gap = 8;
|
|
260
|
+
const viewportWidth = window.innerWidth;
|
|
261
|
+
const viewportHeight = window.innerHeight;
|
|
262
|
+
// Measure after ensuring content is set
|
|
263
|
+
const tooltipRect = this.tooltip.getBoundingClientRect();
|
|
264
|
+
const preferTop = this.shouldPlaceTop(pos, tooltipRect.height);
|
|
265
|
+
const top = preferTop
|
|
266
|
+
? pos.top - tooltipRect.height - gap
|
|
267
|
+
: pos.top + pos.height + gap;
|
|
268
|
+
const left = clamp(pos.left + pos.width / 2 - tooltipRect.width / 2, 8, viewportWidth - tooltipRect.width - 8);
|
|
269
|
+
this.tooltip.style.top = `${clamp(top, 8, viewportHeight - tooltipRect.height - 8)}px`;
|
|
270
|
+
this.tooltip.style.left = `${left}px`;
|
|
271
|
+
}
|
|
272
|
+
shouldPlaceTop(pos, tooltipHeight) {
|
|
273
|
+
const spaceAbove = pos.top;
|
|
274
|
+
const spaceBelow = window.innerHeight - (pos.top + pos.height);
|
|
275
|
+
const preference = this.options.tooltipPosition;
|
|
276
|
+
if (preference === "top")
|
|
277
|
+
return true;
|
|
278
|
+
if (preference === "bottom")
|
|
279
|
+
return false;
|
|
280
|
+
if (spaceAbove >= tooltipHeight + 12)
|
|
281
|
+
return true;
|
|
282
|
+
if (spaceBelow >= tooltipHeight + 12)
|
|
283
|
+
return false;
|
|
284
|
+
return spaceAbove >= spaceBelow; // fallback to the larger side
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const REACT_FIBER_PROP = "__reactFiber$";
|
|
289
|
+
const REACT_INTERNAL_PROP = "__reactInternalInstance$";
|
|
290
|
+
const REACT_CONTAINER_PROP = "__reactContainer$";
|
|
291
|
+
const STACK_TOP = "react-stack-top-frame";
|
|
292
|
+
const STACK_BOTTOM = "react_stack_bottom_frame";
|
|
293
|
+
// Debug mode - check global flag or set to true for development
|
|
294
|
+
const isDebugEnabled = () => {
|
|
295
|
+
return globalThis.__FIXDOG_DEBUG__ === true;
|
|
296
|
+
};
|
|
297
|
+
const debugLog = (...args) => {
|
|
298
|
+
if (isDebugEnabled())
|
|
299
|
+
console.log("[fixdog-sdk:debug]", ...args);
|
|
300
|
+
};
|
|
301
|
+
const possibleFiberKeys = (node) => {
|
|
302
|
+
const keys = [];
|
|
303
|
+
for (const key in node) {
|
|
304
|
+
if (key.startsWith(REACT_FIBER_PROP) ||
|
|
305
|
+
key.startsWith(REACT_INTERNAL_PROP) ||
|
|
306
|
+
key.startsWith(REACT_CONTAINER_PROP)) {
|
|
307
|
+
keys.push(key);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return keys;
|
|
311
|
+
};
|
|
312
|
+
const getFiberFromNode = (node) => {
|
|
313
|
+
let current = node;
|
|
314
|
+
while (current) {
|
|
315
|
+
const keys = possibleFiberKeys(current);
|
|
316
|
+
if (keys.length) {
|
|
317
|
+
const value = current[keys[0]]; // accessing React internals intentionally
|
|
318
|
+
if (value?.current)
|
|
319
|
+
return value.current; // container root form
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
322
|
+
const root = current.getRootNode();
|
|
323
|
+
if (root &&
|
|
324
|
+
root.host &&
|
|
325
|
+
current !== root.host) {
|
|
326
|
+
current = root.host;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
current = current.parentElement;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
};
|
|
333
|
+
const unwrapType = (type) => {
|
|
334
|
+
// handle memo/forwardRef/lazy wrappers
|
|
335
|
+
if (!type)
|
|
336
|
+
return type;
|
|
337
|
+
if (type.render)
|
|
338
|
+
return type.render; // forwardRef
|
|
339
|
+
if (type.type)
|
|
340
|
+
return type.type; // memo wraps actual type
|
|
341
|
+
if (type._payload && type._payload._result)
|
|
342
|
+
return type._payload._result; // lazy resolved
|
|
343
|
+
return type;
|
|
344
|
+
};
|
|
345
|
+
const getComponentName = (fiber, element) => {
|
|
346
|
+
const rawType = fiber?.type || fiber?.elementType;
|
|
347
|
+
const unwrapped = unwrapType(rawType);
|
|
348
|
+
const displayName = unwrapped?.displayName || unwrapped?.name;
|
|
349
|
+
if (displayName)
|
|
350
|
+
return displayName;
|
|
351
|
+
if (fiber?.tag === 5 && element)
|
|
352
|
+
return element.tagName.toLowerCase();
|
|
353
|
+
if (fiber?.tag === 7)
|
|
354
|
+
return "Fragment";
|
|
355
|
+
if (fiber?.tag === 4)
|
|
356
|
+
return "Portal";
|
|
357
|
+
return "AnonymousComponent";
|
|
358
|
+
};
|
|
359
|
+
// Read source from various possible locations
|
|
360
|
+
const readSource = (candidate) => {
|
|
361
|
+
if (!candidate)
|
|
362
|
+
return null;
|
|
363
|
+
// Standard React _debugSource
|
|
364
|
+
if (candidate._debugSource) {
|
|
365
|
+
return {
|
|
366
|
+
fileName: candidate._debugSource.fileName,
|
|
367
|
+
lineNumber: candidate._debugSource.lineNumber,
|
|
368
|
+
columnNumber: candidate._debugSource.columnNumber ?? 0,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// Some bundlers attach __source to the element
|
|
372
|
+
if (candidate.__source) {
|
|
373
|
+
return {
|
|
374
|
+
fileName: candidate.__source.fileName,
|
|
375
|
+
lineNumber: candidate.__source.lineNumber,
|
|
376
|
+
columnNumber: candidate.__source.columnNumber ?? 0,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
};
|
|
381
|
+
// Check for source info on the component type itself
|
|
382
|
+
const readSourceFromType = (type) => {
|
|
383
|
+
if (!type)
|
|
384
|
+
return null;
|
|
385
|
+
const unwrapped = unwrapType(type);
|
|
386
|
+
// Some transforms add __source to the function
|
|
387
|
+
if (unwrapped?.__source) {
|
|
388
|
+
return {
|
|
389
|
+
fileName: unwrapped.__source.fileName,
|
|
390
|
+
lineNumber: unwrapped.__source.lineNumber,
|
|
391
|
+
columnNumber: unwrapped.__source.columnNumber ?? 0,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// Check _source (used by some bundlers)
|
|
395
|
+
if (unwrapped?._source) {
|
|
396
|
+
return {
|
|
397
|
+
fileName: unwrapped._source.fileName,
|
|
398
|
+
lineNumber: unwrapped._source.lineNumber,
|
|
399
|
+
columnNumber: unwrapped._source.columnNumber ?? 0,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
};
|
|
404
|
+
const findDebugSource = (fiber) => {
|
|
405
|
+
const visited = new Set();
|
|
406
|
+
let current = fiber;
|
|
407
|
+
while (current && !visited.has(current)) {
|
|
408
|
+
visited.add(current);
|
|
409
|
+
// Check fiber's _debugSource
|
|
410
|
+
const direct = readSource(current) || readSource(current.alternate);
|
|
411
|
+
if (direct) {
|
|
412
|
+
debugLog("Found source via _debugSource on fiber");
|
|
413
|
+
return direct;
|
|
414
|
+
}
|
|
415
|
+
// Check type for source info
|
|
416
|
+
const typeSource = readSourceFromType(current.type) ||
|
|
417
|
+
readSourceFromType(current.elementType);
|
|
418
|
+
if (typeSource) {
|
|
419
|
+
debugLog("Found source via type");
|
|
420
|
+
return typeSource;
|
|
421
|
+
}
|
|
422
|
+
// Check owner
|
|
423
|
+
const owner = current._debugOwner;
|
|
424
|
+
const ownerSource = readSource(owner) || readSource(owner?.alternate);
|
|
425
|
+
if (ownerSource) {
|
|
426
|
+
debugLog("Found source via _debugOwner");
|
|
427
|
+
return ownerSource;
|
|
428
|
+
}
|
|
429
|
+
// Check owner's type
|
|
430
|
+
const ownerTypeSource = readSourceFromType(owner?.type) || readSourceFromType(owner?.elementType);
|
|
431
|
+
if (ownerTypeSource) {
|
|
432
|
+
debugLog("Found source via owner type");
|
|
433
|
+
return ownerTypeSource;
|
|
434
|
+
}
|
|
435
|
+
current = current.return || owner;
|
|
436
|
+
}
|
|
437
|
+
debugLog("No _debugSource found, will try stack trace");
|
|
438
|
+
return null;
|
|
439
|
+
};
|
|
440
|
+
const isInternalFrame = (frame) => {
|
|
441
|
+
const fn = frame.functionName || "";
|
|
442
|
+
const url = frame.url || "";
|
|
443
|
+
if (fn.includes(STACK_TOP) || fn.includes(STACK_BOTTOM))
|
|
444
|
+
return true;
|
|
445
|
+
if (fn === "exports.jsxDEV" || fn === "jsxDEV" || fn === "jsx")
|
|
446
|
+
return true;
|
|
447
|
+
if (url.includes("react-dom"))
|
|
448
|
+
return true;
|
|
449
|
+
if (url.includes("react-refresh"))
|
|
450
|
+
return true;
|
|
451
|
+
if (url.includes("react.development"))
|
|
452
|
+
return true;
|
|
453
|
+
if (url.includes("node_modules/react"))
|
|
454
|
+
return true;
|
|
455
|
+
return false;
|
|
456
|
+
};
|
|
457
|
+
// Extended regex to handle various stack trace formats including URLs with special characters
|
|
458
|
+
const FRAME_REGEX = /at\s+(?:(.*?)\s+\()?([^():\s]+(?::[^():\s]+)*):(\d+):(\d+)\)?/;
|
|
459
|
+
// Simpler regex for URL extraction - handles Next.js Turbopack URLs
|
|
460
|
+
const URL_REGEX = /(https?:\/\/[^:\s]+):(\d+):(\d+)/;
|
|
461
|
+
const parseStack = (stack) => {
|
|
462
|
+
if (!stack)
|
|
463
|
+
return [];
|
|
464
|
+
debugLog("Parsing stack:", stack);
|
|
465
|
+
const results = stack
|
|
466
|
+
.split("\n")
|
|
467
|
+
.map((line) => line.trim())
|
|
468
|
+
.map((line) => {
|
|
469
|
+
// Try URL regex first (more reliable for complex URLs)
|
|
470
|
+
const urlMatch = line.match(URL_REGEX);
|
|
471
|
+
if (urlMatch) {
|
|
472
|
+
const [, url, lineNum, colNum] = urlMatch;
|
|
473
|
+
// Extract function name if present
|
|
474
|
+
const fnMatch = line.match(/at\s+([^\s(]+)/);
|
|
475
|
+
const fn = fnMatch ? fnMatch[1] : null;
|
|
476
|
+
return {
|
|
477
|
+
functionName: fn,
|
|
478
|
+
url,
|
|
479
|
+
line: Number(lineNum),
|
|
480
|
+
column: Number(colNum),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
// Fallback to original regex
|
|
484
|
+
const match = line.match(FRAME_REGEX);
|
|
485
|
+
if (!match)
|
|
486
|
+
return null;
|
|
487
|
+
const [, fn, url, lineNum, colNum] = match;
|
|
488
|
+
return {
|
|
489
|
+
functionName: fn || null,
|
|
490
|
+
url: url || null,
|
|
491
|
+
line: lineNum ? Number(lineNum) : null,
|
|
492
|
+
column: colNum ? Number(colNum) : null,
|
|
493
|
+
};
|
|
494
|
+
})
|
|
495
|
+
.filter((f) => !!f && !!f.url && f.line != null && f.column != null);
|
|
496
|
+
debugLog("Parsed frames:", results);
|
|
497
|
+
return results;
|
|
498
|
+
};
|
|
499
|
+
// Generate possible source map URLs for a JS file
|
|
500
|
+
const getSourceMapUrls = (url) => {
|
|
501
|
+
const urls = [];
|
|
502
|
+
const withoutQuery = url.split("?")[0];
|
|
503
|
+
// Standard .map suffix
|
|
504
|
+
urls.push(`${withoutQuery}.map`);
|
|
505
|
+
// Next.js Turbopack might use different patterns
|
|
506
|
+
// Try adding .map before query params
|
|
507
|
+
if (url.includes("?")) {
|
|
508
|
+
urls.push(`${withoutQuery}.map${url.slice(url.indexOf("?"))}`);
|
|
509
|
+
}
|
|
510
|
+
return urls;
|
|
511
|
+
};
|
|
512
|
+
const sourceMapCache = new Map();
|
|
513
|
+
const failedSourceMaps = new Set();
|
|
514
|
+
const inlineSourceMapCache = new Map();
|
|
515
|
+
// Try to extract inline source map from JS file content
|
|
516
|
+
const extractInlineSourceMap = async (jsUrl) => {
|
|
517
|
+
if (inlineSourceMapCache.has(jsUrl))
|
|
518
|
+
return inlineSourceMapCache.get(jsUrl);
|
|
519
|
+
const promise = (async () => {
|
|
520
|
+
try {
|
|
521
|
+
debugLog("Fetching JS file to check for inline source map:", jsUrl);
|
|
522
|
+
const res = await fetch(jsUrl);
|
|
523
|
+
if (!res.ok)
|
|
524
|
+
return null;
|
|
525
|
+
const text = await res.text();
|
|
526
|
+
// Look for inline source map (base64 encoded)
|
|
527
|
+
const inlineMatch = text.match(/\/\/# sourceMappingURL=data:application\/json;(?:charset=utf-8;)?base64,([^\s]+)/);
|
|
528
|
+
if (inlineMatch) {
|
|
529
|
+
debugLog("Found inline source map in:", jsUrl);
|
|
530
|
+
const base64 = inlineMatch[1];
|
|
531
|
+
const decoded = atob(base64);
|
|
532
|
+
const map = JSON.parse(decoded);
|
|
533
|
+
return await new sourceMapJs.SourceMapConsumer(map);
|
|
534
|
+
}
|
|
535
|
+
// Also check for sourceMappingURL comment pointing to a file
|
|
536
|
+
const urlMatch = text.match(/\/\/# sourceMappingURL=([^\s]+)/);
|
|
537
|
+
if (urlMatch && !urlMatch[1].startsWith("data:")) {
|
|
538
|
+
const mapPath = urlMatch[1];
|
|
539
|
+
// Resolve relative to JS file
|
|
540
|
+
const baseUrl = jsUrl.substring(0, jsUrl.lastIndexOf("/") + 1);
|
|
541
|
+
const mapUrl = mapPath.startsWith("http") ? mapPath : baseUrl + mapPath;
|
|
542
|
+
debugLog("Found sourceMappingURL comment pointing to:", mapUrl);
|
|
543
|
+
const mapRes = await fetch(mapUrl);
|
|
544
|
+
if (mapRes.ok) {
|
|
545
|
+
const mapText = await mapRes.text();
|
|
546
|
+
const map = JSON.parse(mapText);
|
|
547
|
+
return await new sourceMapJs.SourceMapConsumer(map);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
catch (e) {
|
|
553
|
+
debugLog("Error extracting inline source map:", e);
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
})();
|
|
557
|
+
inlineSourceMapCache.set(jsUrl, promise);
|
|
558
|
+
return promise;
|
|
559
|
+
};
|
|
560
|
+
const fetchSourceMap = async (jsUrl) => {
|
|
561
|
+
// Check if we already tried and failed
|
|
562
|
+
if (failedSourceMaps.has(jsUrl))
|
|
563
|
+
return null;
|
|
564
|
+
if (sourceMapCache.has(jsUrl))
|
|
565
|
+
return sourceMapCache.get(jsUrl);
|
|
566
|
+
const promise = (async () => {
|
|
567
|
+
const mapUrls = getSourceMapUrls(jsUrl);
|
|
568
|
+
// First try standard .map URLs
|
|
569
|
+
for (const mapUrl of mapUrls) {
|
|
570
|
+
try {
|
|
571
|
+
debugLog("Fetching source map:", mapUrl);
|
|
572
|
+
const res = await fetch(mapUrl);
|
|
573
|
+
if (!res.ok) {
|
|
574
|
+
debugLog("Source map not found at:", mapUrl, res.status);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const text = await res.text();
|
|
578
|
+
// Check if it's valid JSON
|
|
579
|
+
let map;
|
|
580
|
+
try {
|
|
581
|
+
map = JSON.parse(text);
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
debugLog("Invalid source map JSON at:", mapUrl);
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
debugLog("Source map loaded successfully from:", mapUrl);
|
|
588
|
+
const consumer = await new sourceMapJs.SourceMapConsumer(map);
|
|
589
|
+
return consumer;
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
debugLog("Error fetching source map:", mapUrl, e);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Try inline source map as fallback
|
|
597
|
+
const inlineConsumer = await extractInlineSourceMap(jsUrl);
|
|
598
|
+
if (inlineConsumer) {
|
|
599
|
+
return inlineConsumer;
|
|
600
|
+
}
|
|
601
|
+
failedSourceMaps.add(jsUrl);
|
|
602
|
+
return null;
|
|
603
|
+
})();
|
|
604
|
+
sourceMapCache.set(jsUrl, promise);
|
|
605
|
+
return promise;
|
|
606
|
+
};
|
|
607
|
+
const resolveFromStack = async (fiber) => {
|
|
608
|
+
// Handle both Error object and string stack
|
|
609
|
+
let stackString;
|
|
610
|
+
if (fiber?._debugStack?.stack) {
|
|
611
|
+
stackString = fiber._debugStack.stack;
|
|
612
|
+
}
|
|
613
|
+
else if (typeof fiber?._debugStack === "string") {
|
|
614
|
+
stackString = fiber._debugStack;
|
|
615
|
+
}
|
|
616
|
+
if (!stackString) {
|
|
617
|
+
debugLog("No stack string found on fiber");
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
const frames = parseStack(stackString);
|
|
621
|
+
// Find first non-internal frame (this is the component that created the element)
|
|
622
|
+
for (const frame of frames) {
|
|
623
|
+
if (!frame.url || frame.line == null || frame.column == null)
|
|
624
|
+
continue;
|
|
625
|
+
if (isInternalFrame(frame)) {
|
|
626
|
+
debugLog("Skipping internal frame:", frame.functionName);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
debugLog("Processing frame:", frame);
|
|
630
|
+
const consumer = await fetchSourceMap(frame.url);
|
|
631
|
+
if (!consumer) {
|
|
632
|
+
debugLog("No source map for:", frame.url);
|
|
633
|
+
// Even without source map, we can use the bundled location
|
|
634
|
+
// This at least gives some info for debugging
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const original = consumer.originalPositionFor({
|
|
638
|
+
line: frame.line,
|
|
639
|
+
column: frame.column,
|
|
640
|
+
});
|
|
641
|
+
debugLog("Original position:", original);
|
|
642
|
+
if (original.source && original.line != null && original.column != null) {
|
|
643
|
+
// Clean up the source path
|
|
644
|
+
let fileName = original.source;
|
|
645
|
+
// Remove webpack/turbopack prefixes
|
|
646
|
+
fileName = fileName.replace(/^webpack:\/\/[^/]*\//, "");
|
|
647
|
+
fileName = fileName.replace(/^\[project\]\//, "");
|
|
648
|
+
fileName = fileName.replace(/^\.\//, "");
|
|
649
|
+
return {
|
|
650
|
+
fileName,
|
|
651
|
+
lineNumber: original.line,
|
|
652
|
+
columnNumber: original.column,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
debugLog("Could not resolve source from stack");
|
|
657
|
+
return null;
|
|
658
|
+
};
|
|
659
|
+
const resolveFromFiber = async (fiber, element) => {
|
|
660
|
+
if (!fiber)
|
|
661
|
+
return Promise.resolve(null);
|
|
662
|
+
const componentName = getComponentName(fiber, element);
|
|
663
|
+
debugLog("Resolving source for:", componentName);
|
|
664
|
+
// Try to find source from _debugSource first
|
|
665
|
+
const immediate = findDebugSource(fiber);
|
|
666
|
+
if (immediate) {
|
|
667
|
+
debugLog("Found immediate source:", immediate);
|
|
668
|
+
return Promise.resolve({
|
|
669
|
+
componentName,
|
|
670
|
+
fileName: immediate.fileName,
|
|
671
|
+
lineNumber: immediate.lineNumber,
|
|
672
|
+
columnNumber: immediate.columnNumber,
|
|
673
|
+
fiber,
|
|
674
|
+
element,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
// Fall back to stack trace parsing
|
|
678
|
+
const stackSource = await resolveFromStack(fiber);
|
|
679
|
+
if (stackSource) {
|
|
680
|
+
debugLog("Found stack source:", stackSource);
|
|
681
|
+
return {
|
|
682
|
+
componentName,
|
|
683
|
+
fileName: stackSource.fileName,
|
|
684
|
+
lineNumber: stackSource.lineNumber,
|
|
685
|
+
columnNumber: stackSource.columnNumber,
|
|
686
|
+
fiber,
|
|
687
|
+
element,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
// If we have a function name from the component, we can still return useful info
|
|
691
|
+
debugLog("No source found for:", componentName);
|
|
692
|
+
return {
|
|
693
|
+
componentName,
|
|
694
|
+
fileName: "Source not available - ensure dev build with source maps",
|
|
695
|
+
lineNumber: 0,
|
|
696
|
+
columnNumber: 0,
|
|
697
|
+
fiber,
|
|
698
|
+
element,
|
|
699
|
+
};
|
|
700
|
+
};
|
|
701
|
+
const getSourceFromElement = (element) => {
|
|
702
|
+
if (!element)
|
|
703
|
+
return Promise.resolve(null);
|
|
704
|
+
const fiber = getFiberFromNode(element);
|
|
705
|
+
if (!fiber) {
|
|
706
|
+
debugLog("No fiber found for element:", element);
|
|
707
|
+
return Promise.resolve(null);
|
|
708
|
+
}
|
|
709
|
+
debugLog("Found fiber for element:", element, fiber);
|
|
710
|
+
return resolveFromFiber(fiber, element);
|
|
711
|
+
};
|
|
712
|
+
/**
|
|
713
|
+
* Get the ancestor component chain from a fiber up to the root.
|
|
714
|
+
* Returns an array of component names, starting from the immediate parent.
|
|
715
|
+
* e.g., ['Header', 'Layout', 'App'] for a Button inside Header inside Layout inside App
|
|
716
|
+
*/
|
|
717
|
+
const getComponentAncestors = (fiber) => {
|
|
718
|
+
const ancestors = [];
|
|
719
|
+
let current = fiber?.return;
|
|
720
|
+
while (current) {
|
|
721
|
+
const name = getComponentName(current);
|
|
722
|
+
if (name && name !== 'AnonymousComponent') {
|
|
723
|
+
ancestors.push(name);
|
|
724
|
+
}
|
|
725
|
+
current = current.return;
|
|
726
|
+
}
|
|
727
|
+
return ancestors;
|
|
728
|
+
};
|
|
729
|
+
// Export for debugging purposes
|
|
730
|
+
const enableDebug = () => {
|
|
731
|
+
globalThis.__FIXDOG_DEBUG__ = true;
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
/*
|
|
735
|
+
* fixdog-sdk types
|
|
736
|
+
* Core interfaces shared across inspector modules.
|
|
737
|
+
*/
|
|
738
|
+
const DEFAULT_SHORTCUT = 'ctrl+shift+x';
|
|
739
|
+
const isProd = typeof process !== 'undefined' &&
|
|
740
|
+
typeof process.env !== 'undefined' &&
|
|
741
|
+
process.env.NODE_ENV === 'production';
|
|
742
|
+
const DEFAULT_OPTIONS = {
|
|
743
|
+
shortcut: DEFAULT_SHORTCUT,
|
|
744
|
+
useOptionClick: true,
|
|
745
|
+
enabled: !isProd,
|
|
746
|
+
zIndex: 999999,
|
|
747
|
+
borderColor: '#61dafb',
|
|
748
|
+
tooltipPosition: 'auto',
|
|
749
|
+
enableSidebar: true,
|
|
750
|
+
apiEndpoint: 'http://localhost:3000',
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const normalize = (token) => token.trim().toLowerCase();
|
|
754
|
+
const tokenToFlags = (tokens) => {
|
|
755
|
+
const flags = { ctrl: false, meta: false, alt: false, shift: false, key: null };
|
|
756
|
+
tokens.forEach((token) => {
|
|
757
|
+
switch (normalize(token)) {
|
|
758
|
+
case 'ctrl':
|
|
759
|
+
case 'control':
|
|
760
|
+
flags.ctrl = true;
|
|
761
|
+
break;
|
|
762
|
+
case 'cmd':
|
|
763
|
+
case 'command':
|
|
764
|
+
case 'meta':
|
|
765
|
+
flags.meta = true;
|
|
766
|
+
break;
|
|
767
|
+
case 'alt':
|
|
768
|
+
case 'option':
|
|
769
|
+
flags.alt = true;
|
|
770
|
+
break;
|
|
771
|
+
case 'shift':
|
|
772
|
+
flags.shift = true;
|
|
773
|
+
break;
|
|
774
|
+
default:
|
|
775
|
+
flags.key = normalize(token);
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
return flags;
|
|
779
|
+
};
|
|
780
|
+
const parseShortcut = (shortcut) => {
|
|
781
|
+
const value = shortcut || DEFAULT_SHORTCUT;
|
|
782
|
+
return tokenToFlags(value.split('+'));
|
|
783
|
+
};
|
|
784
|
+
const matchesShortcut = (event, shortcut) => {
|
|
785
|
+
const parsed = parseShortcut(shortcut);
|
|
786
|
+
const key = event.key?.toLowerCase();
|
|
787
|
+
return ((!!parsed.key ? key === parsed.key : true) &&
|
|
788
|
+
event.ctrlKey === parsed.ctrl &&
|
|
789
|
+
event.metaKey === parsed.meta &&
|
|
790
|
+
event.altKey === parsed.alt &&
|
|
791
|
+
event.shiftKey === parsed.shift);
|
|
792
|
+
};
|
|
793
|
+
const isOptionClick = (event, useOptionClick) => {
|
|
794
|
+
if (!useOptionClick)
|
|
795
|
+
return false;
|
|
796
|
+
return event.altKey && event.button === 0; // left click + alt/option
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// Dynamic import helper for the isolated sidebar runtime
|
|
800
|
+
// This loads a separate bundle with its own React instance to avoid version conflicts
|
|
801
|
+
async function loadSidebarRuntime() {
|
|
802
|
+
// Get the URL of the current module and resolve the sidebar bundle path
|
|
803
|
+
const currentModuleUrl = (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('inspector-DaRVppX9.cjs.js', document.baseURI).href));
|
|
804
|
+
const sidebarUrl = new URL("./sidebar-runtime.esm.js", currentModuleUrl).href;
|
|
805
|
+
try {
|
|
806
|
+
const module = await import(/* @vite-ignore */ sidebarUrl);
|
|
807
|
+
return module;
|
|
808
|
+
}
|
|
809
|
+
catch (error) {
|
|
810
|
+
console.error("[fixdog-sdk] Failed to load sidebar runtime:", error);
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const message = {
|
|
815
|
+
noFiber: "[fixdog-sdk] No React fiber found for element.",
|
|
816
|
+
disabled: "[fixdog-sdk] Inspector disabled (production build)."};
|
|
817
|
+
class Inspector {
|
|
818
|
+
async findInfoInElementAndAncestors(element) {
|
|
819
|
+
let current = element;
|
|
820
|
+
while (current) {
|
|
821
|
+
const info = await getSourceFromElement(current);
|
|
822
|
+
if (info)
|
|
823
|
+
return { element: current, info };
|
|
824
|
+
current = current.parentElement;
|
|
825
|
+
}
|
|
826
|
+
return { element: null, info: null };
|
|
827
|
+
}
|
|
828
|
+
async findInfoFromEvent(event) {
|
|
829
|
+
const path = event.composedPath?.() ?? [];
|
|
830
|
+
for (const node of path) {
|
|
831
|
+
if (node instanceof Element) {
|
|
832
|
+
const found = await this.findInfoInElementAndAncestors(node);
|
|
833
|
+
if (found.info)
|
|
834
|
+
return found;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
const target = event.target;
|
|
838
|
+
return this.findInfoInElementAndAncestors(target);
|
|
839
|
+
}
|
|
840
|
+
constructor(options = {}) {
|
|
841
|
+
this.state = { active: false, persistent: false };
|
|
842
|
+
this.lastElement = null;
|
|
843
|
+
this.lastInfo = null;
|
|
844
|
+
this.boundMouseMove = (e) => this.handleMouseMove(e);
|
|
845
|
+
this.boundClick = (e) => this.handleClick(e);
|
|
846
|
+
this.boundKeyDown = (e) => this.handleKeyDown(e);
|
|
847
|
+
this.boundScroll = () => this.reposition();
|
|
848
|
+
this.boundResize = () => this.reposition();
|
|
849
|
+
this.mounted = false;
|
|
850
|
+
this.requestId = 0;
|
|
851
|
+
// Multi-select state
|
|
852
|
+
this.selectedComponents = [];
|
|
853
|
+
this.nextComponentId = 1;
|
|
854
|
+
this.sidebarOpen = false;
|
|
855
|
+
this.sidebarInitialized = false;
|
|
856
|
+
this.boundClearSelections = () => this.handleClearSelections();
|
|
857
|
+
this.boundRemoveComponent = (e) => this.handleRemoveComponent(e);
|
|
858
|
+
this.options = {
|
|
859
|
+
...DEFAULT_OPTIONS,
|
|
860
|
+
...options,
|
|
861
|
+
enabled: typeof options.enabled === "boolean"
|
|
862
|
+
? options.enabled
|
|
863
|
+
: DEFAULT_OPTIONS.enabled,
|
|
864
|
+
};
|
|
865
|
+
this.overlayOptions = {
|
|
866
|
+
zIndex: this.options.zIndex ?? DEFAULT_OPTIONS.zIndex,
|
|
867
|
+
borderColor: this.options.borderColor ?? DEFAULT_OPTIONS.borderColor,
|
|
868
|
+
tooltipPosition: this.options.tooltipPosition ?? DEFAULT_OPTIONS.tooltipPosition,
|
|
869
|
+
};
|
|
870
|
+
this.overlay = new Overlay(this.overlayOptions);
|
|
871
|
+
// Initialize sidebar if enabled
|
|
872
|
+
if (this.options.enableSidebar && !this.sidebarInitialized) {
|
|
873
|
+
this.initializeSidebar();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async initializeSidebar() {
|
|
877
|
+
if (typeof window === "undefined")
|
|
878
|
+
return;
|
|
879
|
+
try {
|
|
880
|
+
// Dynamically load the sidebar runtime which has its own bundled React
|
|
881
|
+
// This ensures the sidebar works regardless of the host app's React version
|
|
882
|
+
const { initializeSidebarRuntime } = await loadSidebarRuntime();
|
|
883
|
+
initializeSidebarRuntime({
|
|
884
|
+
apiEndpoint: this.options.apiEndpoint ?? DEFAULT_OPTIONS.apiEndpoint,
|
|
885
|
+
});
|
|
886
|
+
this.sidebarInitialized = true;
|
|
887
|
+
console.log("[fixdog-sdk] Sidebar runtime loaded successfully (isolated React)");
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
console.error("[fixdog-sdk] Failed to initialize sidebar:", error);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
activate() {
|
|
894
|
+
if (!this.options.enabled) {
|
|
895
|
+
console.info(message.disabled);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
if (this.mounted)
|
|
899
|
+
return;
|
|
900
|
+
this.mounted = true;
|
|
901
|
+
this.overlay.mount();
|
|
902
|
+
window.addEventListener("mousemove", this.boundMouseMove, true);
|
|
903
|
+
window.addEventListener("click", this.boundClick, true);
|
|
904
|
+
window.addEventListener("keydown", this.boundKeyDown, true);
|
|
905
|
+
window.addEventListener("scroll", this.boundScroll, true);
|
|
906
|
+
window.addEventListener("resize", this.boundResize, true);
|
|
907
|
+
window.addEventListener("fixdog:clear-selections", this.boundClearSelections);
|
|
908
|
+
window.addEventListener("fixdog:remove-component", this.boundRemoveComponent);
|
|
909
|
+
this.state.active = true;
|
|
910
|
+
}
|
|
911
|
+
deactivate() {
|
|
912
|
+
if (!this.mounted)
|
|
913
|
+
return;
|
|
914
|
+
window.removeEventListener("mousemove", this.boundMouseMove, true);
|
|
915
|
+
window.removeEventListener("click", this.boundClick, true);
|
|
916
|
+
window.removeEventListener("keydown", this.boundKeyDown, true);
|
|
917
|
+
window.removeEventListener("scroll", this.boundScroll, true);
|
|
918
|
+
window.removeEventListener("resize", this.boundResize, true);
|
|
919
|
+
window.removeEventListener("fixdog:clear-selections", this.boundClearSelections);
|
|
920
|
+
window.removeEventListener("fixdog:remove-component", this.boundRemoveComponent);
|
|
921
|
+
this.overlay.hide();
|
|
922
|
+
this.clearSelections();
|
|
923
|
+
this.lastElement = null;
|
|
924
|
+
this.lastInfo = null;
|
|
925
|
+
this.state = { active: false, persistent: false };
|
|
926
|
+
this.mounted = false;
|
|
927
|
+
}
|
|
928
|
+
destroy() {
|
|
929
|
+
this.deactivate();
|
|
930
|
+
this.overlay.destroy();
|
|
931
|
+
}
|
|
932
|
+
async handleMouseMove(event) {
|
|
933
|
+
if (!this.state.active || !this.options.enabled)
|
|
934
|
+
return;
|
|
935
|
+
const currentId = ++this.requestId;
|
|
936
|
+
const { element: target, info } = await this.findInfoFromEvent(event);
|
|
937
|
+
const shouldInspect = this.state.persistent || event.altKey;
|
|
938
|
+
if (!shouldInspect || !target) {
|
|
939
|
+
this.overlay.hide();
|
|
940
|
+
this.lastElement = null;
|
|
941
|
+
this.lastInfo = null;
|
|
942
|
+
this.options.onHover?.(null);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (currentId !== this.requestId)
|
|
946
|
+
return; // stale
|
|
947
|
+
if (!info) {
|
|
948
|
+
this.overlay.hide();
|
|
949
|
+
this.options.onHover?.(null);
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
this.lastElement = target;
|
|
953
|
+
this.lastInfo = info;
|
|
954
|
+
this.overlay.update(target, info);
|
|
955
|
+
this.options.onHover?.(info);
|
|
956
|
+
}
|
|
957
|
+
handleClick(event) {
|
|
958
|
+
if (!this.state.active || !this.options.enabled)
|
|
959
|
+
return;
|
|
960
|
+
const shouldHandle = this.state.persistent ||
|
|
961
|
+
isOptionClick(event, this.options.useOptionClick ?? true);
|
|
962
|
+
if (!shouldHandle)
|
|
963
|
+
return;
|
|
964
|
+
const currentId = ++this.requestId;
|
|
965
|
+
this.findInfoFromEvent(event).then(({ element: target, info }) => {
|
|
966
|
+
if (!target)
|
|
967
|
+
return;
|
|
968
|
+
event.preventDefault();
|
|
969
|
+
event.stopPropagation();
|
|
970
|
+
if (currentId !== this.requestId)
|
|
971
|
+
return; // stale
|
|
972
|
+
if (!info) {
|
|
973
|
+
console.warn(message.noFiber);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
this.lastElement = target;
|
|
977
|
+
this.lastInfo = info;
|
|
978
|
+
this.overlay.update(target, info);
|
|
979
|
+
this.log(info);
|
|
980
|
+
// Open sidebar with element info
|
|
981
|
+
if (this.options.enableSidebar) {
|
|
982
|
+
this.openSidebar(info);
|
|
983
|
+
}
|
|
984
|
+
this.options.onClick?.(info);
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
createSelectedComponent(info) {
|
|
988
|
+
const elementInfo = {
|
|
989
|
+
componentName: info.componentName,
|
|
990
|
+
filePath: info.fileName,
|
|
991
|
+
line: info.lineNumber,
|
|
992
|
+
column: info.columnNumber,
|
|
993
|
+
componentTree: info.fiber ? getComponentAncestors(info.fiber) : undefined,
|
|
994
|
+
};
|
|
995
|
+
// Get bounding box if element is available
|
|
996
|
+
if (info.element) {
|
|
997
|
+
const rect = info.element.getBoundingClientRect();
|
|
998
|
+
elementInfo.box = {
|
|
999
|
+
x: rect.x,
|
|
1000
|
+
y: rect.y,
|
|
1001
|
+
width: rect.width,
|
|
1002
|
+
height: rect.height,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
// Create editor URL format
|
|
1006
|
+
const editorUrl = `${info.fileName}:${info.lineNumber}:${info.columnNumber}`;
|
|
1007
|
+
// Get next available ID (fills gaps)
|
|
1008
|
+
const id = this.getNextAvailableId();
|
|
1009
|
+
const selectedComponent = {
|
|
1010
|
+
id,
|
|
1011
|
+
editorUrl,
|
|
1012
|
+
elementInfo,
|
|
1013
|
+
elementRef: info.element ?? undefined,
|
|
1014
|
+
};
|
|
1015
|
+
return selectedComponent;
|
|
1016
|
+
}
|
|
1017
|
+
openSidebar(info) {
|
|
1018
|
+
const selectedComponent = this.createSelectedComponent(info);
|
|
1019
|
+
if (!this.sidebarOpen) {
|
|
1020
|
+
// First selection - open sidebar
|
|
1021
|
+
this.selectedComponents = [selectedComponent];
|
|
1022
|
+
this.sidebarOpen = true;
|
|
1023
|
+
// Add selection highlight to overlay (if element exists)
|
|
1024
|
+
if (info.element) {
|
|
1025
|
+
this.overlay.addSelection(selectedComponent.id, info.element, selectedComponent.elementInfo.componentName);
|
|
1026
|
+
}
|
|
1027
|
+
// Dispatch open sidebar event
|
|
1028
|
+
const event = new CustomEvent("fixdog:open-sidebar", {
|
|
1029
|
+
detail: {
|
|
1030
|
+
editorUrl: selectedComponent.editorUrl,
|
|
1031
|
+
elementInfo: selectedComponent.elementInfo,
|
|
1032
|
+
selectedComponent,
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
window.dispatchEvent(event);
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
// Sidebar already open - add to selection
|
|
1039
|
+
this.selectedComponents.push(selectedComponent);
|
|
1040
|
+
// Add selection highlight to overlay (if element exists)
|
|
1041
|
+
if (info.element) {
|
|
1042
|
+
this.overlay.addSelection(selectedComponent.id, info.element, selectedComponent.elementInfo.componentName);
|
|
1043
|
+
}
|
|
1044
|
+
// Dispatch add component event
|
|
1045
|
+
const event = new CustomEvent("fixdog:add-component", {
|
|
1046
|
+
detail: {
|
|
1047
|
+
selectedComponent,
|
|
1048
|
+
},
|
|
1049
|
+
});
|
|
1050
|
+
window.dispatchEvent(event);
|
|
1051
|
+
}
|
|
1052
|
+
// Store selected components globally for reference
|
|
1053
|
+
window.__FIXDOG_SELECTED_COMPONENTS__ = this.selectedComponents;
|
|
1054
|
+
}
|
|
1055
|
+
clearSelections() {
|
|
1056
|
+
this.selectedComponents = [];
|
|
1057
|
+
this.nextComponentId = 1;
|
|
1058
|
+
this.sidebarOpen = false;
|
|
1059
|
+
this.overlay.clearSelections();
|
|
1060
|
+
window.__FIXDOG_SELECTED_COMPONENTS__ = [];
|
|
1061
|
+
}
|
|
1062
|
+
handleClearSelections() {
|
|
1063
|
+
this.clearSelections();
|
|
1064
|
+
}
|
|
1065
|
+
handleRemoveComponent(event) {
|
|
1066
|
+
const { componentId } = event.detail;
|
|
1067
|
+
this.removeSelection(componentId);
|
|
1068
|
+
}
|
|
1069
|
+
// Calculate the next available ID (fill gaps first)
|
|
1070
|
+
getNextAvailableId() {
|
|
1071
|
+
if (this.selectedComponents.length === 0) {
|
|
1072
|
+
return 1;
|
|
1073
|
+
}
|
|
1074
|
+
// Get all current IDs sorted
|
|
1075
|
+
const usedIds = new Set(this.selectedComponents.map((c) => c.id));
|
|
1076
|
+
// Find the first gap starting from 1
|
|
1077
|
+
let nextId = 1;
|
|
1078
|
+
while (usedIds.has(nextId)) {
|
|
1079
|
+
nextId++;
|
|
1080
|
+
}
|
|
1081
|
+
return nextId;
|
|
1082
|
+
}
|
|
1083
|
+
// Public method to remove a specific selection
|
|
1084
|
+
removeSelection(componentId) {
|
|
1085
|
+
this.selectedComponents = this.selectedComponents.filter((c) => c.id !== componentId);
|
|
1086
|
+
this.overlay.removeSelection(componentId);
|
|
1087
|
+
window.__FIXDOG_SELECTED_COMPONENTS__ = this.selectedComponents;
|
|
1088
|
+
// Recalculate next ID to fill gaps
|
|
1089
|
+
this.nextComponentId = this.getNextAvailableId();
|
|
1090
|
+
// If no more selections, close sidebar
|
|
1091
|
+
if (this.selectedComponents.length === 0) {
|
|
1092
|
+
this.sidebarOpen = false;
|
|
1093
|
+
this.nextComponentId = 1;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
handleKeyDown(event) {
|
|
1097
|
+
if (!this.options.enabled)
|
|
1098
|
+
return;
|
|
1099
|
+
if (matchesShortcut(event, this.options.shortcut)) {
|
|
1100
|
+
event.preventDefault();
|
|
1101
|
+
event.stopPropagation();
|
|
1102
|
+
this.togglePersistent();
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (event.key === "Escape") {
|
|
1106
|
+
this.state.persistent = false;
|
|
1107
|
+
this.overlay.hide();
|
|
1108
|
+
// Don't clear selections on Escape - let sidebar handle that
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
togglePersistent() {
|
|
1112
|
+
this.state.persistent = !this.state.persistent;
|
|
1113
|
+
if (!this.state.persistent) {
|
|
1114
|
+
this.overlay.hide();
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
reposition() {
|
|
1118
|
+
if (this.lastElement && this.lastInfo) {
|
|
1119
|
+
this.overlay.update(this.lastElement, this.lastInfo);
|
|
1120
|
+
}
|
|
1121
|
+
// Also update selection positions
|
|
1122
|
+
this.overlay.updateSelectionPositions();
|
|
1123
|
+
}
|
|
1124
|
+
log(info) {
|
|
1125
|
+
const { componentName, fileName, lineNumber } = info;
|
|
1126
|
+
console.log("%c[fixdog-sdk]%c %s %c@ %s:%d", "color: #61dafb; font-weight: bold", "color: #98c379; font-weight: bold", componentName, "color: #888", fileName, lineNumber);
|
|
1127
|
+
console.log(info);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
|
1132
|
+
exports.Inspector = Inspector;
|
|
1133
|
+
exports.enableDebug = enableDebug;
|
|
1134
|
+
//# sourceMappingURL=inspector-DaRVppX9.cjs.js.map
|