@linhey/react-debug-inspector 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +71 -2
- package/dist/index.d.mts +1 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +224 -20
- package/dist/index.mjs +224 -20
- package/package.json +23 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 linhay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
# @linhey/react-debug-inspector 🎯
|
|
2
2
|
|
|
3
|
+
[](https://github.com/linhay/react-debug-inspector/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@linhey/react-debug-inspector)
|
|
5
|
+
[](https://www.npmjs.com/package/@linhey/react-debug-inspector)
|
|
6
|
+
[](https://github.com/linhay/react-debug-inspector/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/linhay/react-debug-inspector)
|
|
8
|
+
|
|
3
9
|
一个轻量级的 React 调试辅助工具,让你在浏览器中直接识别组件、标签及其对应的源码行号。
|
|
4
10
|
|
|
11
|
+
## 🎬 演示
|
|
12
|
+
|
|
13
|
+
### 在线演示
|
|
14
|
+
|
|
15
|
+
🌐 **[在线体验 Demo](https://linhay.github.io/react-debug-inspector/)**
|
|
16
|
+
|
|
17
|
+
> GitHub Pages 配置说明见 [GITHUB_PAGES.md](./pages/GITHUB_PAGES.md)
|
|
18
|
+
|
|
19
|
+
### 快速预览
|
|
20
|
+
|
|
21
|
+
1. 点击右下角的 🎯 按钮进入检查模式
|
|
22
|
+
2. 鼠标悬停在任何元素上查看调试信息
|
|
23
|
+
3. 点击元素复制完整的调试标识
|
|
24
|
+
4. 按 `Esc` 键退出检查模式
|
|
25
|
+
|
|
26
|
+
<!-- TODO: 添加演示 GIF -->
|
|
27
|
+
<!--  -->
|
|
28
|
+
|
|
29
|
+
> 📹 查看 [录制指南](./RECORDING_GUIDE.md) 了解如何制作演示视频
|
|
30
|
+
|
|
5
31
|
## 特性
|
|
6
32
|
|
|
7
33
|
- 🚀 **零配置注入**:通过 Babel 插件自动为每个 JSX 元素添加 `data-debug` 属性。
|
|
8
34
|
- 🎯 **瞄准模式**:悬浮显示元素标识,单击一键复制。
|
|
35
|
+
- 📁 **完整路径**:显示文件路径、组件名、标签名和行号,精确定位源码。
|
|
9
36
|
- 🌳 **零污染**:生产环境构建时自动剔除,不增加包体积。
|
|
10
37
|
- ⌨️ **快捷键支持**:支持 Esc 退出及 Alt+右键 快速复制。
|
|
11
38
|
|
|
@@ -49,11 +76,53 @@ if (import.meta.env.DEV) {
|
|
|
49
76
|
## 交互说明
|
|
50
77
|
|
|
51
78
|
- **🎯 按钮**:位于右下角,点击进入/退出审查模式。
|
|
52
|
-
- **悬浮模式**:进入模式后,鼠标指向的元素将被高亮,并显示
|
|
53
|
-
-
|
|
79
|
+
- **悬浮模式**:进入模式后,鼠标指向的元素将被高亮,并显示 `文件名 › 组件 › 标签:行号`。
|
|
80
|
+
- **单击左键**:复制完整标识(`文件路径:组件:标签:行号`)到剪贴板并退出。
|
|
54
81
|
- **Esc**:退出模式。
|
|
55
82
|
- **Alt + 右键**:非模式下亦可直接复制标识。
|
|
56
83
|
|
|
84
|
+
## 输出格式
|
|
85
|
+
|
|
86
|
+
复制到剪贴板的格式为:`src/components/Button.tsx:Button:button:42`
|
|
87
|
+
|
|
88
|
+
- `src/components/Button.tsx` - 文件相对路径
|
|
89
|
+
- `Button` - 组件名
|
|
90
|
+
- `button` - HTML 标签名
|
|
91
|
+
- `42` - 源码行号
|
|
92
|
+
|
|
93
|
+
## 开发
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# 安装依赖
|
|
97
|
+
npm install
|
|
98
|
+
|
|
99
|
+
# 运行单元测试
|
|
100
|
+
npm test
|
|
101
|
+
|
|
102
|
+
# 运行 E2E 测试
|
|
103
|
+
npm run test:e2e
|
|
104
|
+
|
|
105
|
+
# 运行所有测试
|
|
106
|
+
npm run test:all
|
|
107
|
+
|
|
108
|
+
# 构建
|
|
109
|
+
npm run build
|
|
110
|
+
|
|
111
|
+
# 开发模式
|
|
112
|
+
npm run dev
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 发布
|
|
116
|
+
|
|
117
|
+
本项目使用 GitHub Actions 自动化发布流程。详见 [GITHUB_ACTIONS.md](./GITHUB_ACTIONS.md)。
|
|
118
|
+
|
|
119
|
+
### 快速发布
|
|
120
|
+
|
|
121
|
+
1. 进入 GitHub Actions 页面
|
|
122
|
+
2. 选择 "Version Bump and Release" 工作流
|
|
123
|
+
3. 点击 "Run workflow",选择版本类型(patch/minor/major)
|
|
124
|
+
4. 自动完成测试、构建、发布到 NPM
|
|
125
|
+
|
|
57
126
|
## License
|
|
58
127
|
|
|
59
128
|
MIT
|
package/dist/index.d.mts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -29,28 +29,42 @@ module.exports = __toCommonJS(index_exports);
|
|
|
29
29
|
function babelPluginDebugLabel() {
|
|
30
30
|
return {
|
|
31
31
|
visitor: {
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
Program(programPath, state) {
|
|
33
|
+
const filename = state.file.opts.filename || state.filename || "";
|
|
34
|
+
const cwd = state.cwd || (typeof process !== "undefined" ? process.cwd() : "");
|
|
35
|
+
const getRelativePath = (absolutePath) => {
|
|
36
|
+
if (!absolutePath) return "unknown";
|
|
37
|
+
if (absolutePath.startsWith(cwd)) {
|
|
38
|
+
return absolutePath.slice(cwd.length + 1);
|
|
39
|
+
}
|
|
40
|
+
return absolutePath;
|
|
41
|
+
};
|
|
42
|
+
const relativePath = getRelativePath(filename);
|
|
43
|
+
programPath.traverse({
|
|
44
|
+
FunctionDeclaration(path) {
|
|
45
|
+
const name = path.node.id?.name;
|
|
46
|
+
if (!name || name[0] !== name[0].toUpperCase()) return;
|
|
47
|
+
injectAllJSX(path, name, relativePath);
|
|
48
|
+
},
|
|
49
|
+
VariableDeclarator(path) {
|
|
50
|
+
const name = path.node.id?.name;
|
|
51
|
+
if (!name || name[0] !== name[0].toUpperCase()) return;
|
|
52
|
+
const init = path.node.init;
|
|
53
|
+
if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
|
|
54
|
+
injectAllJSX(path.get("init"), name, relativePath);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
};
|
|
47
|
-
function injectAllJSX(path, componentName) {
|
|
61
|
+
function injectAllJSX(path, componentName, filePath) {
|
|
48
62
|
path.traverse({
|
|
49
63
|
JSXElement(jsxPath) {
|
|
50
64
|
const { openingElement } = jsxPath.node;
|
|
51
65
|
const tagName = getTagName(openingElement.name);
|
|
52
66
|
const line = openingElement.loc?.start.line || "0";
|
|
53
|
-
const debugId = `${componentName}:${tagName}:${line}`;
|
|
67
|
+
const debugId = `${filePath}:${componentName}:${tagName}:${line}`;
|
|
54
68
|
const alreadyHas = openingElement.attributes.some(
|
|
55
69
|
(attr) => attr.type === "JSXAttribute" && attr.name?.name === "data-debug"
|
|
56
70
|
);
|
|
@@ -76,6 +90,16 @@ function babelPluginDebugLabel() {
|
|
|
76
90
|
function initInspector() {
|
|
77
91
|
if (typeof window === "undefined") return;
|
|
78
92
|
let isInspecting = false;
|
|
93
|
+
let hasUserPosition = false;
|
|
94
|
+
let isDragging = false;
|
|
95
|
+
let pointerMoved = false;
|
|
96
|
+
let dragOffsetX = 0;
|
|
97
|
+
let dragOffsetY = 0;
|
|
98
|
+
let dragStartX = 0;
|
|
99
|
+
let dragStartY = 0;
|
|
100
|
+
let overlay = null;
|
|
101
|
+
let tooltip = null;
|
|
102
|
+
const edgeOffset = 24;
|
|
79
103
|
const toggleBtn = document.createElement("button");
|
|
80
104
|
toggleBtn.innerHTML = "\u{1F3AF}";
|
|
81
105
|
toggleBtn.title = "\u5F00\u542F\u7EC4\u4EF6\u5B9A\u4F4D\u5668";
|
|
@@ -99,7 +123,111 @@ function initInspector() {
|
|
|
99
123
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
100
124
|
`;
|
|
101
125
|
document.body.appendChild(toggleBtn);
|
|
102
|
-
const
|
|
126
|
+
const applyAnchor = (anchor) => {
|
|
127
|
+
if (hasUserPosition) return;
|
|
128
|
+
toggleBtn.style.top = anchor.startsWith("top") ? `${edgeOffset}px` : "";
|
|
129
|
+
toggleBtn.style.bottom = anchor.startsWith("bottom") ? `${edgeOffset}px` : "";
|
|
130
|
+
if (anchor.endsWith("left")) {
|
|
131
|
+
toggleBtn.style.left = `${edgeOffset}px`;
|
|
132
|
+
toggleBtn.style.right = "";
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
toggleBtn.style.right = `${edgeOffset}px`;
|
|
136
|
+
toggleBtn.style.left = "";
|
|
137
|
+
};
|
|
138
|
+
const getVisibleDialogs = () => {
|
|
139
|
+
if (typeof document === "undefined") return [];
|
|
140
|
+
const candidates = Array.from(
|
|
141
|
+
document.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]')
|
|
142
|
+
);
|
|
143
|
+
return candidates.filter((node) => {
|
|
144
|
+
if (node.getAttribute("aria-hidden") === "true") return false;
|
|
145
|
+
if (node.getAttribute("data-aria-hidden") === "true") return false;
|
|
146
|
+
const style = window.getComputedStyle(node);
|
|
147
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
const ensureToggleHost = (host) => {
|
|
151
|
+
if (toggleBtn.parentElement !== host) {
|
|
152
|
+
host.appendChild(toggleBtn);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const isIgnorableObstacle = (el) => {
|
|
156
|
+
if (el === toggleBtn || el === overlay || el === tooltip) return true;
|
|
157
|
+
if (el instanceof HTMLElement) {
|
|
158
|
+
if (el.contains(toggleBtn) || toggleBtn.contains(el)) return true;
|
|
159
|
+
if (overlay && el.contains(overlay)) return true;
|
|
160
|
+
if (tooltip && el.contains(tooltip)) return true;
|
|
161
|
+
if (el === document.body || el === document.documentElement) return true;
|
|
162
|
+
if (el.getAttribute("data-inspector-ignore") === "true") return true;
|
|
163
|
+
const style = window.getComputedStyle(el);
|
|
164
|
+
if (style.pointerEvents === "none") return true;
|
|
165
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return true;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
};
|
|
169
|
+
const sampleAnchorObstructionScore = () => {
|
|
170
|
+
const pickStack = typeof document.elementsFromPoint === "function" ? (x, y) => document.elementsFromPoint(x, y) : (x, y) => {
|
|
171
|
+
const one = document.elementFromPoint?.(x, y);
|
|
172
|
+
return one ? [one] : [];
|
|
173
|
+
};
|
|
174
|
+
const rect = toggleBtn.getBoundingClientRect();
|
|
175
|
+
const points = [
|
|
176
|
+
[rect.left + rect.width / 2, rect.top + rect.height / 2],
|
|
177
|
+
[rect.left + 2, rect.top + 2],
|
|
178
|
+
[rect.right - 2, rect.top + 2],
|
|
179
|
+
[rect.left + 2, rect.bottom - 2],
|
|
180
|
+
[rect.right - 2, rect.bottom - 2]
|
|
181
|
+
];
|
|
182
|
+
let score = 0;
|
|
183
|
+
for (const [x, y] of points) {
|
|
184
|
+
const stack = pickStack(x, y);
|
|
185
|
+
const blocker = stack.find((el) => !isIgnorableObstacle(el));
|
|
186
|
+
if (blocker) score += 1;
|
|
187
|
+
}
|
|
188
|
+
return score;
|
|
189
|
+
};
|
|
190
|
+
const pickBestAnchor = (preferLeft = false) => {
|
|
191
|
+
const candidates = preferLeft ? ["bottom-left", "top-left", "bottom-right", "top-right"] : ["bottom-right", "top-right", "bottom-left", "top-left"];
|
|
192
|
+
let best = candidates[0];
|
|
193
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
194
|
+
for (const anchor of candidates) {
|
|
195
|
+
applyAnchor(anchor);
|
|
196
|
+
const score = sampleAnchorObstructionScore();
|
|
197
|
+
if (score < bestScore) {
|
|
198
|
+
bestScore = score;
|
|
199
|
+
best = anchor;
|
|
200
|
+
}
|
|
201
|
+
if (score === 0) break;
|
|
202
|
+
}
|
|
203
|
+
applyAnchor(best);
|
|
204
|
+
};
|
|
205
|
+
const updateAnchorForDialogs = () => {
|
|
206
|
+
if (typeof document === "undefined") return;
|
|
207
|
+
const dialogs = getVisibleDialogs();
|
|
208
|
+
if (dialogs.length > 0) {
|
|
209
|
+
ensureToggleHost(dialogs[dialogs.length - 1]);
|
|
210
|
+
pickBestAnchor(true);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
ensureToggleHost(document.body);
|
|
214
|
+
pickBestAnchor(false);
|
|
215
|
+
};
|
|
216
|
+
let anchorUpdatePending = false;
|
|
217
|
+
const scheduleAnchorUpdate = () => {
|
|
218
|
+
if (typeof window === "undefined") return;
|
|
219
|
+
if (anchorUpdatePending) return;
|
|
220
|
+
anchorUpdatePending = true;
|
|
221
|
+
const schedule = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : (cb) => window.setTimeout(() => cb(Date.now()), 16);
|
|
222
|
+
schedule(() => {
|
|
223
|
+
anchorUpdatePending = false;
|
|
224
|
+
updateAnchorForDialogs();
|
|
225
|
+
});
|
|
226
|
+
};
|
|
227
|
+
const dialogObserver = new MutationObserver(() => {
|
|
228
|
+
scheduleAnchorUpdate();
|
|
229
|
+
});
|
|
230
|
+
overlay = document.createElement("div");
|
|
103
231
|
overlay.style.cssText = `
|
|
104
232
|
position: fixed;
|
|
105
233
|
pointer-events: none;
|
|
@@ -110,10 +238,10 @@ function initInspector() {
|
|
|
110
238
|
transition: all 0.1s ease-out;
|
|
111
239
|
`;
|
|
112
240
|
document.body.appendChild(overlay);
|
|
113
|
-
|
|
241
|
+
tooltip = document.createElement("div");
|
|
114
242
|
tooltip.style.cssText = `
|
|
115
243
|
position: fixed;
|
|
116
|
-
pointer-events:
|
|
244
|
+
pointer-events: auto;
|
|
117
245
|
z-index: 9999999;
|
|
118
246
|
background: #1e293b;
|
|
119
247
|
color: #38bdf8;
|
|
@@ -126,8 +254,19 @@ function initInspector() {
|
|
|
126
254
|
display: none;
|
|
127
255
|
white-space: nowrap;
|
|
128
256
|
transition: all 0.1s ease-out;
|
|
257
|
+
cursor: help;
|
|
129
258
|
`;
|
|
130
259
|
document.body.appendChild(tooltip);
|
|
260
|
+
dialogObserver.observe(document.body, {
|
|
261
|
+
childList: true,
|
|
262
|
+
subtree: true,
|
|
263
|
+
attributes: true,
|
|
264
|
+
attributeFilter: ["style", "class", "open", "aria-hidden"]
|
|
265
|
+
});
|
|
266
|
+
window.addEventListener("beforeunload", () => dialogObserver.disconnect(), { once: true });
|
|
267
|
+
window.addEventListener("resize", scheduleAnchorUpdate);
|
|
268
|
+
window.addEventListener("scroll", scheduleAnchorUpdate, true);
|
|
269
|
+
updateAnchorForDialogs();
|
|
131
270
|
const stopInspecting = () => {
|
|
132
271
|
isInspecting = false;
|
|
133
272
|
toggleBtn.style.transform = "scale(1)";
|
|
@@ -136,7 +275,44 @@ function initInspector() {
|
|
|
136
275
|
overlay.style.display = "none";
|
|
137
276
|
tooltip.style.display = "none";
|
|
138
277
|
};
|
|
139
|
-
|
|
278
|
+
const onPointerMove = (e) => {
|
|
279
|
+
if (!isDragging) return;
|
|
280
|
+
const deltaX = Math.abs(e.clientX - dragStartX);
|
|
281
|
+
const deltaY = Math.abs(e.clientY - dragStartY);
|
|
282
|
+
if (deltaX > 3 || deltaY > 3) {
|
|
283
|
+
pointerMoved = true;
|
|
284
|
+
}
|
|
285
|
+
const left = Math.max(8, Math.min(window.innerWidth - 52, e.clientX - dragOffsetX));
|
|
286
|
+
const top = Math.max(8, Math.min(window.innerHeight - 52, e.clientY - dragOffsetY));
|
|
287
|
+
toggleBtn.style.left = `${left}px`;
|
|
288
|
+
toggleBtn.style.top = `${top}px`;
|
|
289
|
+
toggleBtn.style.right = "";
|
|
290
|
+
toggleBtn.style.bottom = "";
|
|
291
|
+
hasUserPosition = true;
|
|
292
|
+
};
|
|
293
|
+
const onPointerUp = () => {
|
|
294
|
+
isDragging = false;
|
|
295
|
+
window.removeEventListener("mousemove", onPointerMove);
|
|
296
|
+
window.removeEventListener("mouseup", onPointerUp);
|
|
297
|
+
};
|
|
298
|
+
toggleBtn.addEventListener("mousedown", (e) => {
|
|
299
|
+
const rect = toggleBtn.getBoundingClientRect();
|
|
300
|
+
isDragging = true;
|
|
301
|
+
pointerMoved = false;
|
|
302
|
+
dragStartX = e.clientX;
|
|
303
|
+
dragStartY = e.clientY;
|
|
304
|
+
dragOffsetX = e.clientX - rect.left;
|
|
305
|
+
dragOffsetY = e.clientY - rect.top;
|
|
306
|
+
window.addEventListener("mousemove", onPointerMove);
|
|
307
|
+
window.addEventListener("mouseup", onPointerUp);
|
|
308
|
+
});
|
|
309
|
+
toggleBtn.onclick = (e) => {
|
|
310
|
+
if (pointerMoved) {
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
e.stopPropagation();
|
|
313
|
+
pointerMoved = false;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
140
316
|
isInspecting = !isInspecting;
|
|
141
317
|
if (isInspecting) {
|
|
142
318
|
toggleBtn.style.transform = "scale(0.9)";
|
|
@@ -146,11 +322,21 @@ function initInspector() {
|
|
|
146
322
|
stopInspecting();
|
|
147
323
|
}
|
|
148
324
|
};
|
|
149
|
-
|
|
325
|
+
const formatDebugId = (debugId) => {
|
|
326
|
+
const parts = debugId.split(":");
|
|
327
|
+
if (parts.length === 4) {
|
|
328
|
+
const [filePath, componentName, tagName, line] = parts;
|
|
329
|
+
const fileName = filePath.split("/").pop() || filePath;
|
|
330
|
+
return `${fileName} \u203A ${componentName} \u203A ${tagName}:${line}`;
|
|
331
|
+
}
|
|
332
|
+
return debugId.replace(/:/g, " \u203A ");
|
|
333
|
+
};
|
|
334
|
+
const inspectByPointer = (e) => {
|
|
150
335
|
if (!isInspecting) return;
|
|
151
336
|
const target = e.target;
|
|
152
337
|
if (target === toggleBtn || target === overlay || target === tooltip) return;
|
|
153
338
|
const debugEl = target.closest("[data-debug]");
|
|
339
|
+
if (debugEl && debugEl === lastHoveredDebugEl) return;
|
|
154
340
|
if (debugEl) {
|
|
155
341
|
const debugId = debugEl.getAttribute("data-debug") || "";
|
|
156
342
|
const rect = debugEl.getBoundingClientRect();
|
|
@@ -160,15 +346,33 @@ function initInspector() {
|
|
|
160
346
|
overlay.style.width = rect.width + "px";
|
|
161
347
|
overlay.style.height = rect.height + "px";
|
|
162
348
|
tooltip.style.display = "block";
|
|
163
|
-
tooltip.textContent = debugId;
|
|
349
|
+
tooltip.textContent = formatDebugId(debugId);
|
|
164
350
|
tooltip.style.color = "#38bdf8";
|
|
351
|
+
tooltip.title = debugId;
|
|
165
352
|
const tooltipY = rect.top < 30 ? rect.bottom + 4 : rect.top - 28;
|
|
166
353
|
tooltip.style.top = tooltipY + "px";
|
|
167
354
|
tooltip.style.left = rect.left + "px";
|
|
355
|
+
lastHoveredDebugEl = debugEl;
|
|
168
356
|
} else {
|
|
169
357
|
overlay.style.display = "none";
|
|
170
358
|
tooltip.style.display = "none";
|
|
359
|
+
lastHoveredDebugEl = null;
|
|
171
360
|
}
|
|
361
|
+
};
|
|
362
|
+
let pendingHoverFrame = false;
|
|
363
|
+
let latestHoverEvent = null;
|
|
364
|
+
let lastHoveredDebugEl = null;
|
|
365
|
+
document.addEventListener("mousemove", (e) => {
|
|
366
|
+
if (!isInspecting) return;
|
|
367
|
+
latestHoverEvent = e;
|
|
368
|
+
if (pendingHoverFrame) return;
|
|
369
|
+
pendingHoverFrame = true;
|
|
370
|
+
const schedule = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : (cb) => window.setTimeout(() => cb(Date.now()), 16);
|
|
371
|
+
schedule(() => {
|
|
372
|
+
pendingHoverFrame = false;
|
|
373
|
+
if (!latestHoverEvent) return;
|
|
374
|
+
inspectByPointer(latestHoverEvent);
|
|
375
|
+
});
|
|
172
376
|
});
|
|
173
377
|
window.addEventListener(
|
|
174
378
|
"click",
|
package/dist/index.mjs
CHANGED
|
@@ -2,28 +2,42 @@
|
|
|
2
2
|
function babelPluginDebugLabel() {
|
|
3
3
|
return {
|
|
4
4
|
visitor: {
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
Program(programPath, state) {
|
|
6
|
+
const filename = state.file.opts.filename || state.filename || "";
|
|
7
|
+
const cwd = state.cwd || (typeof process !== "undefined" ? process.cwd() : "");
|
|
8
|
+
const getRelativePath = (absolutePath) => {
|
|
9
|
+
if (!absolutePath) return "unknown";
|
|
10
|
+
if (absolutePath.startsWith(cwd)) {
|
|
11
|
+
return absolutePath.slice(cwd.length + 1);
|
|
12
|
+
}
|
|
13
|
+
return absolutePath;
|
|
14
|
+
};
|
|
15
|
+
const relativePath = getRelativePath(filename);
|
|
16
|
+
programPath.traverse({
|
|
17
|
+
FunctionDeclaration(path) {
|
|
18
|
+
const name = path.node.id?.name;
|
|
19
|
+
if (!name || name[0] !== name[0].toUpperCase()) return;
|
|
20
|
+
injectAllJSX(path, name, relativePath);
|
|
21
|
+
},
|
|
22
|
+
VariableDeclarator(path) {
|
|
23
|
+
const name = path.node.id?.name;
|
|
24
|
+
if (!name || name[0] !== name[0].toUpperCase()) return;
|
|
25
|
+
const init = path.node.init;
|
|
26
|
+
if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
|
|
27
|
+
injectAllJSX(path.get("init"), name, relativePath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
17
31
|
}
|
|
18
32
|
}
|
|
19
33
|
};
|
|
20
|
-
function injectAllJSX(path, componentName) {
|
|
34
|
+
function injectAllJSX(path, componentName, filePath) {
|
|
21
35
|
path.traverse({
|
|
22
36
|
JSXElement(jsxPath) {
|
|
23
37
|
const { openingElement } = jsxPath.node;
|
|
24
38
|
const tagName = getTagName(openingElement.name);
|
|
25
39
|
const line = openingElement.loc?.start.line || "0";
|
|
26
|
-
const debugId = `${componentName}:${tagName}:${line}`;
|
|
40
|
+
const debugId = `${filePath}:${componentName}:${tagName}:${line}`;
|
|
27
41
|
const alreadyHas = openingElement.attributes.some(
|
|
28
42
|
(attr) => attr.type === "JSXAttribute" && attr.name?.name === "data-debug"
|
|
29
43
|
);
|
|
@@ -49,6 +63,16 @@ function babelPluginDebugLabel() {
|
|
|
49
63
|
function initInspector() {
|
|
50
64
|
if (typeof window === "undefined") return;
|
|
51
65
|
let isInspecting = false;
|
|
66
|
+
let hasUserPosition = false;
|
|
67
|
+
let isDragging = false;
|
|
68
|
+
let pointerMoved = false;
|
|
69
|
+
let dragOffsetX = 0;
|
|
70
|
+
let dragOffsetY = 0;
|
|
71
|
+
let dragStartX = 0;
|
|
72
|
+
let dragStartY = 0;
|
|
73
|
+
let overlay = null;
|
|
74
|
+
let tooltip = null;
|
|
75
|
+
const edgeOffset = 24;
|
|
52
76
|
const toggleBtn = document.createElement("button");
|
|
53
77
|
toggleBtn.innerHTML = "\u{1F3AF}";
|
|
54
78
|
toggleBtn.title = "\u5F00\u542F\u7EC4\u4EF6\u5B9A\u4F4D\u5668";
|
|
@@ -72,7 +96,111 @@ function initInspector() {
|
|
|
72
96
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
73
97
|
`;
|
|
74
98
|
document.body.appendChild(toggleBtn);
|
|
75
|
-
const
|
|
99
|
+
const applyAnchor = (anchor) => {
|
|
100
|
+
if (hasUserPosition) return;
|
|
101
|
+
toggleBtn.style.top = anchor.startsWith("top") ? `${edgeOffset}px` : "";
|
|
102
|
+
toggleBtn.style.bottom = anchor.startsWith("bottom") ? `${edgeOffset}px` : "";
|
|
103
|
+
if (anchor.endsWith("left")) {
|
|
104
|
+
toggleBtn.style.left = `${edgeOffset}px`;
|
|
105
|
+
toggleBtn.style.right = "";
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
toggleBtn.style.right = `${edgeOffset}px`;
|
|
109
|
+
toggleBtn.style.left = "";
|
|
110
|
+
};
|
|
111
|
+
const getVisibleDialogs = () => {
|
|
112
|
+
if (typeof document === "undefined") return [];
|
|
113
|
+
const candidates = Array.from(
|
|
114
|
+
document.querySelectorAll('[role="dialog"], dialog[open], [aria-modal="true"]')
|
|
115
|
+
);
|
|
116
|
+
return candidates.filter((node) => {
|
|
117
|
+
if (node.getAttribute("aria-hidden") === "true") return false;
|
|
118
|
+
if (node.getAttribute("data-aria-hidden") === "true") return false;
|
|
119
|
+
const style = window.getComputedStyle(node);
|
|
120
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const ensureToggleHost = (host) => {
|
|
124
|
+
if (toggleBtn.parentElement !== host) {
|
|
125
|
+
host.appendChild(toggleBtn);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const isIgnorableObstacle = (el) => {
|
|
129
|
+
if (el === toggleBtn || el === overlay || el === tooltip) return true;
|
|
130
|
+
if (el instanceof HTMLElement) {
|
|
131
|
+
if (el.contains(toggleBtn) || toggleBtn.contains(el)) return true;
|
|
132
|
+
if (overlay && el.contains(overlay)) return true;
|
|
133
|
+
if (tooltip && el.contains(tooltip)) return true;
|
|
134
|
+
if (el === document.body || el === document.documentElement) return true;
|
|
135
|
+
if (el.getAttribute("data-inspector-ignore") === "true") return true;
|
|
136
|
+
const style = window.getComputedStyle(el);
|
|
137
|
+
if (style.pointerEvents === "none") return true;
|
|
138
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
};
|
|
142
|
+
const sampleAnchorObstructionScore = () => {
|
|
143
|
+
const pickStack = typeof document.elementsFromPoint === "function" ? (x, y) => document.elementsFromPoint(x, y) : (x, y) => {
|
|
144
|
+
const one = document.elementFromPoint?.(x, y);
|
|
145
|
+
return one ? [one] : [];
|
|
146
|
+
};
|
|
147
|
+
const rect = toggleBtn.getBoundingClientRect();
|
|
148
|
+
const points = [
|
|
149
|
+
[rect.left + rect.width / 2, rect.top + rect.height / 2],
|
|
150
|
+
[rect.left + 2, rect.top + 2],
|
|
151
|
+
[rect.right - 2, rect.top + 2],
|
|
152
|
+
[rect.left + 2, rect.bottom - 2],
|
|
153
|
+
[rect.right - 2, rect.bottom - 2]
|
|
154
|
+
];
|
|
155
|
+
let score = 0;
|
|
156
|
+
for (const [x, y] of points) {
|
|
157
|
+
const stack = pickStack(x, y);
|
|
158
|
+
const blocker = stack.find((el) => !isIgnorableObstacle(el));
|
|
159
|
+
if (blocker) score += 1;
|
|
160
|
+
}
|
|
161
|
+
return score;
|
|
162
|
+
};
|
|
163
|
+
const pickBestAnchor = (preferLeft = false) => {
|
|
164
|
+
const candidates = preferLeft ? ["bottom-left", "top-left", "bottom-right", "top-right"] : ["bottom-right", "top-right", "bottom-left", "top-left"];
|
|
165
|
+
let best = candidates[0];
|
|
166
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
167
|
+
for (const anchor of candidates) {
|
|
168
|
+
applyAnchor(anchor);
|
|
169
|
+
const score = sampleAnchorObstructionScore();
|
|
170
|
+
if (score < bestScore) {
|
|
171
|
+
bestScore = score;
|
|
172
|
+
best = anchor;
|
|
173
|
+
}
|
|
174
|
+
if (score === 0) break;
|
|
175
|
+
}
|
|
176
|
+
applyAnchor(best);
|
|
177
|
+
};
|
|
178
|
+
const updateAnchorForDialogs = () => {
|
|
179
|
+
if (typeof document === "undefined") return;
|
|
180
|
+
const dialogs = getVisibleDialogs();
|
|
181
|
+
if (dialogs.length > 0) {
|
|
182
|
+
ensureToggleHost(dialogs[dialogs.length - 1]);
|
|
183
|
+
pickBestAnchor(true);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
ensureToggleHost(document.body);
|
|
187
|
+
pickBestAnchor(false);
|
|
188
|
+
};
|
|
189
|
+
let anchorUpdatePending = false;
|
|
190
|
+
const scheduleAnchorUpdate = () => {
|
|
191
|
+
if (typeof window === "undefined") return;
|
|
192
|
+
if (anchorUpdatePending) return;
|
|
193
|
+
anchorUpdatePending = true;
|
|
194
|
+
const schedule = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : (cb) => window.setTimeout(() => cb(Date.now()), 16);
|
|
195
|
+
schedule(() => {
|
|
196
|
+
anchorUpdatePending = false;
|
|
197
|
+
updateAnchorForDialogs();
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
const dialogObserver = new MutationObserver(() => {
|
|
201
|
+
scheduleAnchorUpdate();
|
|
202
|
+
});
|
|
203
|
+
overlay = document.createElement("div");
|
|
76
204
|
overlay.style.cssText = `
|
|
77
205
|
position: fixed;
|
|
78
206
|
pointer-events: none;
|
|
@@ -83,10 +211,10 @@ function initInspector() {
|
|
|
83
211
|
transition: all 0.1s ease-out;
|
|
84
212
|
`;
|
|
85
213
|
document.body.appendChild(overlay);
|
|
86
|
-
|
|
214
|
+
tooltip = document.createElement("div");
|
|
87
215
|
tooltip.style.cssText = `
|
|
88
216
|
position: fixed;
|
|
89
|
-
pointer-events:
|
|
217
|
+
pointer-events: auto;
|
|
90
218
|
z-index: 9999999;
|
|
91
219
|
background: #1e293b;
|
|
92
220
|
color: #38bdf8;
|
|
@@ -99,8 +227,19 @@ function initInspector() {
|
|
|
99
227
|
display: none;
|
|
100
228
|
white-space: nowrap;
|
|
101
229
|
transition: all 0.1s ease-out;
|
|
230
|
+
cursor: help;
|
|
102
231
|
`;
|
|
103
232
|
document.body.appendChild(tooltip);
|
|
233
|
+
dialogObserver.observe(document.body, {
|
|
234
|
+
childList: true,
|
|
235
|
+
subtree: true,
|
|
236
|
+
attributes: true,
|
|
237
|
+
attributeFilter: ["style", "class", "open", "aria-hidden"]
|
|
238
|
+
});
|
|
239
|
+
window.addEventListener("beforeunload", () => dialogObserver.disconnect(), { once: true });
|
|
240
|
+
window.addEventListener("resize", scheduleAnchorUpdate);
|
|
241
|
+
window.addEventListener("scroll", scheduleAnchorUpdate, true);
|
|
242
|
+
updateAnchorForDialogs();
|
|
104
243
|
const stopInspecting = () => {
|
|
105
244
|
isInspecting = false;
|
|
106
245
|
toggleBtn.style.transform = "scale(1)";
|
|
@@ -109,7 +248,44 @@ function initInspector() {
|
|
|
109
248
|
overlay.style.display = "none";
|
|
110
249
|
tooltip.style.display = "none";
|
|
111
250
|
};
|
|
112
|
-
|
|
251
|
+
const onPointerMove = (e) => {
|
|
252
|
+
if (!isDragging) return;
|
|
253
|
+
const deltaX = Math.abs(e.clientX - dragStartX);
|
|
254
|
+
const deltaY = Math.abs(e.clientY - dragStartY);
|
|
255
|
+
if (deltaX > 3 || deltaY > 3) {
|
|
256
|
+
pointerMoved = true;
|
|
257
|
+
}
|
|
258
|
+
const left = Math.max(8, Math.min(window.innerWidth - 52, e.clientX - dragOffsetX));
|
|
259
|
+
const top = Math.max(8, Math.min(window.innerHeight - 52, e.clientY - dragOffsetY));
|
|
260
|
+
toggleBtn.style.left = `${left}px`;
|
|
261
|
+
toggleBtn.style.top = `${top}px`;
|
|
262
|
+
toggleBtn.style.right = "";
|
|
263
|
+
toggleBtn.style.bottom = "";
|
|
264
|
+
hasUserPosition = true;
|
|
265
|
+
};
|
|
266
|
+
const onPointerUp = () => {
|
|
267
|
+
isDragging = false;
|
|
268
|
+
window.removeEventListener("mousemove", onPointerMove);
|
|
269
|
+
window.removeEventListener("mouseup", onPointerUp);
|
|
270
|
+
};
|
|
271
|
+
toggleBtn.addEventListener("mousedown", (e) => {
|
|
272
|
+
const rect = toggleBtn.getBoundingClientRect();
|
|
273
|
+
isDragging = true;
|
|
274
|
+
pointerMoved = false;
|
|
275
|
+
dragStartX = e.clientX;
|
|
276
|
+
dragStartY = e.clientY;
|
|
277
|
+
dragOffsetX = e.clientX - rect.left;
|
|
278
|
+
dragOffsetY = e.clientY - rect.top;
|
|
279
|
+
window.addEventListener("mousemove", onPointerMove);
|
|
280
|
+
window.addEventListener("mouseup", onPointerUp);
|
|
281
|
+
});
|
|
282
|
+
toggleBtn.onclick = (e) => {
|
|
283
|
+
if (pointerMoved) {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
e.stopPropagation();
|
|
286
|
+
pointerMoved = false;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
113
289
|
isInspecting = !isInspecting;
|
|
114
290
|
if (isInspecting) {
|
|
115
291
|
toggleBtn.style.transform = "scale(0.9)";
|
|
@@ -119,11 +295,21 @@ function initInspector() {
|
|
|
119
295
|
stopInspecting();
|
|
120
296
|
}
|
|
121
297
|
};
|
|
122
|
-
|
|
298
|
+
const formatDebugId = (debugId) => {
|
|
299
|
+
const parts = debugId.split(":");
|
|
300
|
+
if (parts.length === 4) {
|
|
301
|
+
const [filePath, componentName, tagName, line] = parts;
|
|
302
|
+
const fileName = filePath.split("/").pop() || filePath;
|
|
303
|
+
return `${fileName} \u203A ${componentName} \u203A ${tagName}:${line}`;
|
|
304
|
+
}
|
|
305
|
+
return debugId.replace(/:/g, " \u203A ");
|
|
306
|
+
};
|
|
307
|
+
const inspectByPointer = (e) => {
|
|
123
308
|
if (!isInspecting) return;
|
|
124
309
|
const target = e.target;
|
|
125
310
|
if (target === toggleBtn || target === overlay || target === tooltip) return;
|
|
126
311
|
const debugEl = target.closest("[data-debug]");
|
|
312
|
+
if (debugEl && debugEl === lastHoveredDebugEl) return;
|
|
127
313
|
if (debugEl) {
|
|
128
314
|
const debugId = debugEl.getAttribute("data-debug") || "";
|
|
129
315
|
const rect = debugEl.getBoundingClientRect();
|
|
@@ -133,15 +319,33 @@ function initInspector() {
|
|
|
133
319
|
overlay.style.width = rect.width + "px";
|
|
134
320
|
overlay.style.height = rect.height + "px";
|
|
135
321
|
tooltip.style.display = "block";
|
|
136
|
-
tooltip.textContent = debugId;
|
|
322
|
+
tooltip.textContent = formatDebugId(debugId);
|
|
137
323
|
tooltip.style.color = "#38bdf8";
|
|
324
|
+
tooltip.title = debugId;
|
|
138
325
|
const tooltipY = rect.top < 30 ? rect.bottom + 4 : rect.top - 28;
|
|
139
326
|
tooltip.style.top = tooltipY + "px";
|
|
140
327
|
tooltip.style.left = rect.left + "px";
|
|
328
|
+
lastHoveredDebugEl = debugEl;
|
|
141
329
|
} else {
|
|
142
330
|
overlay.style.display = "none";
|
|
143
331
|
tooltip.style.display = "none";
|
|
332
|
+
lastHoveredDebugEl = null;
|
|
144
333
|
}
|
|
334
|
+
};
|
|
335
|
+
let pendingHoverFrame = false;
|
|
336
|
+
let latestHoverEvent = null;
|
|
337
|
+
let lastHoveredDebugEl = null;
|
|
338
|
+
document.addEventListener("mousemove", (e) => {
|
|
339
|
+
if (!isInspecting) return;
|
|
340
|
+
latestHoverEvent = e;
|
|
341
|
+
if (pendingHoverFrame) return;
|
|
342
|
+
pendingHoverFrame = true;
|
|
343
|
+
const schedule = typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : (cb) => window.setTimeout(() => cb(Date.now()), 16);
|
|
344
|
+
schedule(() => {
|
|
345
|
+
pendingHoverFrame = false;
|
|
346
|
+
if (!latestHoverEvent) return;
|
|
347
|
+
inspectByPointer(latestHoverEvent);
|
|
348
|
+
});
|
|
145
349
|
});
|
|
146
350
|
window.addEventListener(
|
|
147
351
|
"click",
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linhey/react-debug-inspector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A developer tool to inspect React components in browser and jump to source code.",
|
|
5
5
|
"author": "linhey",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/linhay/react-debug-inspector"
|
|
10
|
+
},
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.mjs",
|
|
9
13
|
"types": "./dist/index.d.ts",
|
|
@@ -12,13 +16,30 @@
|
|
|
12
16
|
],
|
|
13
17
|
"scripts": {
|
|
14
18
|
"dev": "tsup src/index.ts --format cjs,esm --watch --dts",
|
|
19
|
+
"dev:test-app": "vite --config vite.config.test.ts",
|
|
15
20
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:e2e": "playwright test",
|
|
23
|
+
"test:e2e:ui": "playwright test --ui",
|
|
24
|
+
"test:e2e:headed": "playwright test --headed",
|
|
25
|
+
"test:all": "npm run test && npm run test:e2e",
|
|
16
26
|
"prepublishOnly": "npm run build"
|
|
17
27
|
},
|
|
18
28
|
"devDependencies": {
|
|
19
29
|
"@babel/core": "^7.24.0",
|
|
30
|
+
"@playwright/test": "^1.58.2",
|
|
31
|
+
"@types/node": "^25.3.5",
|
|
32
|
+
"@types/react": "^19.2.14",
|
|
33
|
+
"@types/react-dom": "^19.2.3",
|
|
34
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
35
|
+
"jsdom": "^26.1.0",
|
|
36
|
+
"playwright": "^1.58.2",
|
|
37
|
+
"react": "^19.2.4",
|
|
38
|
+
"react-dom": "^19.2.4",
|
|
20
39
|
"tsup": "^8.0.2",
|
|
21
|
-
"typescript": "^5.4.2"
|
|
40
|
+
"typescript": "^5.4.2",
|
|
41
|
+
"vite": "^7.3.1",
|
|
42
|
+
"vitest": "^3.2.4"
|
|
22
43
|
},
|
|
23
44
|
"peerDependencies": {
|
|
24
45
|
"react": ">=16.8.0"
|