@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 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
+ [![CI](https://github.com/linhay/react-debug-inspector/actions/workflows/ci.yml/badge.svg)](https://github.com/linhay/react-debug-inspector/actions/workflows/ci.yml)
4
+ [![NPM Version](https://img.shields.io/npm/v/@linhey/react-debug-inspector)](https://www.npmjs.com/package/@linhey/react-debug-inspector)
5
+ [![NPM Downloads](https://img.shields.io/npm/dm/@linhey/react-debug-inspector)](https://www.npmjs.com/package/@linhey/react-debug-inspector)
6
+ [![License](https://img.shields.io/npm/l/@linhey/react-debug-inspector)](https://github.com/linhay/react-debug-inspector/blob/main/LICENSE)
7
+ [![repo](https://img.shields.io/badge/github-linhay%2Freact--debug--inspector-181717?logo=github)](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
+ <!-- ![基本使用演示](./assets/demo-basic.gif) -->
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
@@ -1,7 +1,6 @@
1
1
  declare function babelPluginDebugLabel(): {
2
2
  visitor: {
3
- FunctionDeclaration(path: any): void;
4
- VariableDeclarator(path: any): void;
3
+ Program(programPath: any, state: any): void;
5
4
  };
6
5
  };
7
6
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  declare function babelPluginDebugLabel(): {
2
2
  visitor: {
3
- FunctionDeclaration(path: any): void;
4
- VariableDeclarator(path: any): void;
3
+ Program(programPath: any, state: any): void;
5
4
  };
6
5
  };
7
6
 
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
- FunctionDeclaration(path) {
33
- const name = path.node.id?.name;
34
- if (!name || name[0] !== name[0].toUpperCase()) return;
35
- injectAllJSX(path, name);
36
- },
37
- VariableDeclarator(path) {
38
- const name = path.node.id?.name;
39
- if (!name || name[0] !== name[0].toUpperCase()) return;
40
- const init = path.node.init;
41
- if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
42
- injectAllJSX(path.get("init"), name);
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 overlay = document.createElement("div");
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
- const tooltip = document.createElement("div");
241
+ tooltip = document.createElement("div");
114
242
  tooltip.style.cssText = `
115
243
  position: fixed;
116
- pointer-events: none;
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
- toggleBtn.onclick = () => {
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
- window.addEventListener("mousemove", (e) => {
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
- FunctionDeclaration(path) {
6
- const name = path.node.id?.name;
7
- if (!name || name[0] !== name[0].toUpperCase()) return;
8
- injectAllJSX(path, name);
9
- },
10
- VariableDeclarator(path) {
11
- const name = path.node.id?.name;
12
- if (!name || name[0] !== name[0].toUpperCase()) return;
13
- const init = path.node.init;
14
- if (init && (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression")) {
15
- injectAllJSX(path.get("init"), name);
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 overlay = document.createElement("div");
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
- const tooltip = document.createElement("div");
214
+ tooltip = document.createElement("div");
87
215
  tooltip.style.cssText = `
88
216
  position: fixed;
89
- pointer-events: none;
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
- toggleBtn.onclick = () => {
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
- window.addEventListener("mousemove", (e) => {
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.0.0",
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"