@stinsky/xray 0.1.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 +138 -0
- package/dist/index.cjs +505 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.mjs +479 -0
- package/dist/plugin.cjs +38 -0
- package/dist/plugin.d.cts +16 -0
- package/dist/plugin.d.ts +16 -0
- package/dist/plugin.mjs +13 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Stinsky
|
|
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
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# @stinsky/xray
|
|
2
|
+
|
|
3
|
+
Click-to-component for React 19. Hover any element to see its React component name and source file, click to open in your editor. The only click-to-source inspector that works with React 19, Next.js 15+, and Turbopack.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@stinsky/xray)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Click to open source** — click any element to open its source file in VS Code, WebStorm, IntelliJ, or any editor
|
|
11
|
+
- **Component name overlay** — hover to see the React component name + file path
|
|
12
|
+
- **Works with React 19** — uses compile-time AST injection, not `fiber._debugSource` (removed in React 19)
|
|
13
|
+
- **All bundlers** — Next.js (Turbopack & Webpack), Vite, Webpack, Rspack, esbuild
|
|
14
|
+
- **Zero production cost** — fully tree-shaken, zero bytes in your production bundle
|
|
15
|
+
- **Floating toggle button** — auto-positions next to the Next.js dev indicator, or bottom-left in other setups
|
|
16
|
+
- **Keyboard shortcut** — `Cmd+Shift+X` to toggle (customizable)
|
|
17
|
+
- **Scroll-aware** — rAF-based tracking, works with smooth scrolling libraries (Lenis, etc.)
|
|
18
|
+
- **Interaction blocking** — all clicks/pointer events blocked while inspecting, no accidental navigation
|
|
19
|
+
|
|
20
|
+
## Why not react-dev-inspector / click-to-react-component / LocatorJS?
|
|
21
|
+
|
|
22
|
+
React 19 removed `fiber._debugSource`, which broke every existing click-to-component tool. These packages rely on runtime fiber inspection to find source locations — an approach that no longer works.
|
|
23
|
+
|
|
24
|
+
`@stinsky/xray` uses [`code-inspector-plugin`](https://github.com/nicolo-ribaudo/code-inspector-plugin) to inject `data-insp-path` attributes at compile time via AST transformation. This is the only approach that works reliably across React 18, React 19, and all modern bundlers.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -D @stinsky/xray
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
### 1. Add the build plugin
|
|
35
|
+
|
|
36
|
+
The plugin injects source location attributes on every DOM element at compile time. It does nothing in production builds.
|
|
37
|
+
|
|
38
|
+
#### Next.js (Turbopack)
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// next.config.ts
|
|
42
|
+
import { xrayPlugin } from '@stinsky/xray/plugin'
|
|
43
|
+
|
|
44
|
+
const nextConfig = {
|
|
45
|
+
turbopack: {
|
|
46
|
+
rules: xrayPlugin({ bundler: 'turbopack' }),
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default nextConfig
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
#### Next.js (Webpack)
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// next.config.ts
|
|
57
|
+
import { xrayPlugin } from '@stinsky/xray/plugin'
|
|
58
|
+
|
|
59
|
+
const nextConfig = {
|
|
60
|
+
webpack: (config) => {
|
|
61
|
+
config.plugins.push(xrayPlugin({ bundler: 'webpack' }))
|
|
62
|
+
return config
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default nextConfig
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Vite
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
// vite.config.ts
|
|
73
|
+
import { xrayPlugin } from '@stinsky/xray/plugin'
|
|
74
|
+
|
|
75
|
+
export default {
|
|
76
|
+
plugins: [xrayPlugin({ bundler: 'vite' })],
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Webpack
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// webpack.config.js
|
|
84
|
+
const { xrayPlugin } = require('@stinsky/xray/plugin')
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
plugins: [xrayPlugin({ bundler: 'webpack' })],
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 2. Add the component
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
// app/layout.tsx (Next.js) or your root component
|
|
95
|
+
import { Xray } from '@stinsky/xray'
|
|
96
|
+
|
|
97
|
+
export default function Layout({ children }) {
|
|
98
|
+
return (
|
|
99
|
+
<html>
|
|
100
|
+
<body>
|
|
101
|
+
{children}
|
|
102
|
+
<Xray />
|
|
103
|
+
</body>
|
|
104
|
+
</html>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The component is fully tree-shaken in production — zero bytes in your bundle. Bundlers replace `process.env.NODE_ENV`, detect the dead code path, and eliminate the entire inspector along with all its dependencies.
|
|
110
|
+
|
|
111
|
+
## Usage
|
|
112
|
+
|
|
113
|
+
- **Toggle**: `Cmd+Shift+X` or click the floating button
|
|
114
|
+
- **Hover**: Shows component name + source file path
|
|
115
|
+
- **Click**: Opens the source file in your editor
|
|
116
|
+
|
|
117
|
+
All clicks and pointer events are blocked while the inspector is active, so you won't accidentally trigger links or buttons.
|
|
118
|
+
|
|
119
|
+
## Props
|
|
120
|
+
|
|
121
|
+
| Prop | Type | Default | Description |
|
|
122
|
+
|------|------|---------|-------------|
|
|
123
|
+
| `hotKey` | `{ metaKey?, ctrlKey?, altKey?, shiftKey?, key }` | `{ metaKey: true, shiftKey: true, key: 'x' }` | Keyboard shortcut to toggle |
|
|
124
|
+
| `port` | `number` | `5678` | `code-inspector-plugin` server port |
|
|
125
|
+
| `color` | `string` | `'#6366f1'` | Accent color for overlay, tooltip, and button |
|
|
126
|
+
| `showButton` | `boolean` | `true` | Show the floating toggle button |
|
|
127
|
+
| `followNextIndicator` | `boolean` | `true` | Position button next to the Next.js dev indicator |
|
|
128
|
+
|
|
129
|
+
## Plugin options
|
|
130
|
+
|
|
131
|
+
| Option | Type | Default | Description |
|
|
132
|
+
|--------|------|---------|-------------|
|
|
133
|
+
| `bundler` | `'webpack' \| 'vite' \| 'turbopack' \| 'rspack' \| 'esbuild'` | — | **Required.** Your bundler |
|
|
134
|
+
| `editor` | `string` | `'code'` | Editor to open files in (`code`, `webstorm`, `idea`, etc.) |
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var src_exports = {};
|
|
23
|
+
__export(src_exports, {
|
|
24
|
+
Xray: () => Xray
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(src_exports);
|
|
27
|
+
|
|
28
|
+
// src/xray.tsx
|
|
29
|
+
var import_react4 = require("react");
|
|
30
|
+
var import_react_dom = require("react-dom");
|
|
31
|
+
|
|
32
|
+
// src/use-badge.ts
|
|
33
|
+
var import_react = require("react");
|
|
34
|
+
var BADGE_SIZE = 36;
|
|
35
|
+
var BADGE_GAP = 4;
|
|
36
|
+
var DRAG_THRESHOLD = 10;
|
|
37
|
+
function findNextjsIndicator() {
|
|
38
|
+
const portal = document.querySelector("nextjs-portal");
|
|
39
|
+
if (!portal?.shadowRoot) return null;
|
|
40
|
+
return portal.shadowRoot.querySelector("[data-nextjs-toast]") ?? portal.shadowRoot.querySelector("div");
|
|
41
|
+
}
|
|
42
|
+
function useBadge({ badgeRef, show, followNextIndicator }) {
|
|
43
|
+
(0, import_react.useEffect)(() => {
|
|
44
|
+
if (!show) return;
|
|
45
|
+
const badge = badgeRef.current;
|
|
46
|
+
if (!badge) return;
|
|
47
|
+
badge.style.scale = "0";
|
|
48
|
+
let rafId;
|
|
49
|
+
let dragging = false;
|
|
50
|
+
let dragStartX = 0;
|
|
51
|
+
let dragStartY = 0;
|
|
52
|
+
let hidden = false;
|
|
53
|
+
let settling = false;
|
|
54
|
+
let positioned = false;
|
|
55
|
+
let settled = false;
|
|
56
|
+
let hideTimeout;
|
|
57
|
+
let reappearTimeout;
|
|
58
|
+
const getIndicator = () => followNextIndicator ? findNextjsIndicator() : null;
|
|
59
|
+
const positionBadge = () => {
|
|
60
|
+
if (!badge) return;
|
|
61
|
+
const indicator = getIndicator();
|
|
62
|
+
if (indicator) {
|
|
63
|
+
const rect = indicator.getBoundingClientRect();
|
|
64
|
+
const midX = rect.left + rect.width / 2;
|
|
65
|
+
const isRight = midX > window.innerWidth / 2;
|
|
66
|
+
badge.style.left = "";
|
|
67
|
+
badge.style.bottom = "";
|
|
68
|
+
badge.style.left = isRight ? `${rect.left - BADGE_SIZE - BADGE_GAP}px` : `${rect.right + BADGE_GAP}px`;
|
|
69
|
+
badge.style.top = `${rect.top + (rect.height - BADGE_SIZE) / 2}px`;
|
|
70
|
+
} else {
|
|
71
|
+
badge.style.top = "";
|
|
72
|
+
badge.style.bottom = "12px";
|
|
73
|
+
badge.style.left = "12px";
|
|
74
|
+
settled = true;
|
|
75
|
+
}
|
|
76
|
+
if (!positioned) {
|
|
77
|
+
positioned = true;
|
|
78
|
+
requestAnimationFrame(() => {
|
|
79
|
+
badge.style.scale = "1";
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const onPointerDown = (e) => {
|
|
84
|
+
const indicator = getIndicator();
|
|
85
|
+
if (!indicator) return;
|
|
86
|
+
const rect = indicator.getBoundingClientRect();
|
|
87
|
+
if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) {
|
|
88
|
+
dragging = true;
|
|
89
|
+
dragStartX = e.clientX;
|
|
90
|
+
dragStartY = e.clientY;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const onPointerMove = (e) => {
|
|
94
|
+
if (!dragging) return;
|
|
95
|
+
const dx = e.clientX - dragStartX;
|
|
96
|
+
const dy = e.clientY - dragStartY;
|
|
97
|
+
if (!hidden && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
|
|
98
|
+
hidden = true;
|
|
99
|
+
settling = true;
|
|
100
|
+
badge.style.transition = "scale 200ms ease";
|
|
101
|
+
badge.style.scale = "0";
|
|
102
|
+
hideTimeout = setTimeout(() => {
|
|
103
|
+
badge.style.display = "none";
|
|
104
|
+
}, 200);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const onPointerUp = () => {
|
|
108
|
+
if (!dragging) return;
|
|
109
|
+
dragging = false;
|
|
110
|
+
if (hidden) {
|
|
111
|
+
reappearTimeout = setTimeout(() => {
|
|
112
|
+
hidden = false;
|
|
113
|
+
settling = false;
|
|
114
|
+
settled = false;
|
|
115
|
+
badge.style.scale = "0";
|
|
116
|
+
badge.style.display = "";
|
|
117
|
+
badge.style.transition = "scale 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)";
|
|
118
|
+
void badge.offsetHeight;
|
|
119
|
+
badge.style.scale = "1";
|
|
120
|
+
}, 1e3);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
let lastKey = "";
|
|
124
|
+
const tick = () => {
|
|
125
|
+
if (settled) return;
|
|
126
|
+
if (!hidden && !settling) {
|
|
127
|
+
const indicator = getIndicator();
|
|
128
|
+
if (indicator) {
|
|
129
|
+
const rect = indicator.getBoundingClientRect();
|
|
130
|
+
const key = `${rect.top},${rect.left},${rect.right},${rect.bottom}`;
|
|
131
|
+
if (key !== lastKey) {
|
|
132
|
+
lastKey = key;
|
|
133
|
+
positionBadge();
|
|
134
|
+
}
|
|
135
|
+
} else if (lastKey !== "fallback") {
|
|
136
|
+
lastKey = "fallback";
|
|
137
|
+
positionBadge();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
rafId = requestAnimationFrame(tick);
|
|
141
|
+
};
|
|
142
|
+
rafId = requestAnimationFrame(tick);
|
|
143
|
+
window.addEventListener("pointerdown", onPointerDown, true);
|
|
144
|
+
window.addEventListener("pointermove", onPointerMove, true);
|
|
145
|
+
window.addEventListener("pointerup", onPointerUp, true);
|
|
146
|
+
return () => {
|
|
147
|
+
cancelAnimationFrame(rafId);
|
|
148
|
+
clearTimeout(hideTimeout);
|
|
149
|
+
clearTimeout(reappearTimeout);
|
|
150
|
+
window.removeEventListener("pointerdown", onPointerDown, true);
|
|
151
|
+
window.removeEventListener("pointermove", onPointerMove, true);
|
|
152
|
+
window.removeEventListener("pointerup", onPointerUp, true);
|
|
153
|
+
};
|
|
154
|
+
}, [badgeRef, show, followNextIndicator]);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/use-hotkey.ts
|
|
158
|
+
var import_react2 = require("react");
|
|
159
|
+
var DEFAULT_HOT_KEY = { metaKey: true, shiftKey: true, key: "x" };
|
|
160
|
+
function useHotkey(hotKey, onToggle) {
|
|
161
|
+
(0, import_react2.useEffect)(() => {
|
|
162
|
+
const handler = (e) => {
|
|
163
|
+
if (e.key === hotKey.key && !!e.metaKey === !!hotKey.metaKey && !!e.ctrlKey === !!hotKey.ctrlKey && !!e.altKey === !!hotKey.altKey && !!e.shiftKey === !!hotKey.shiftKey) {
|
|
164
|
+
onToggle();
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
window.addEventListener("keydown", handler);
|
|
168
|
+
return () => window.removeEventListener("keydown", handler);
|
|
169
|
+
}, [hotKey.key, hotKey.metaKey, hotKey.ctrlKey, hotKey.altKey, hotKey.shiftKey, onToggle]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/use-inspector.ts
|
|
173
|
+
var import_react3 = require("react");
|
|
174
|
+
function getFiberFromElement(el) {
|
|
175
|
+
const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
|
|
176
|
+
return key ? el[key] : null;
|
|
177
|
+
}
|
|
178
|
+
function getComponentName(fiber) {
|
|
179
|
+
if (!fiber.type || typeof fiber.type === "string") return null;
|
|
180
|
+
return fiber.type.displayName || fiber.type.name || null;
|
|
181
|
+
}
|
|
182
|
+
function getComponentInfo(el) {
|
|
183
|
+
let fiber = getFiberFromElement(el);
|
|
184
|
+
if (!fiber) return null;
|
|
185
|
+
let depth = 0;
|
|
186
|
+
while (fiber && depth < 30) {
|
|
187
|
+
const name = getComponentName(fiber);
|
|
188
|
+
if (name) return { name };
|
|
189
|
+
fiber = fiber.return;
|
|
190
|
+
depth++;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
function parseInspPath(attr) {
|
|
195
|
+
const parts = attr.split(":");
|
|
196
|
+
parts.pop();
|
|
197
|
+
const column = parts.pop();
|
|
198
|
+
const line = parts.pop();
|
|
199
|
+
const filePath = parts.join(":");
|
|
200
|
+
return { filePath, line, column };
|
|
201
|
+
}
|
|
202
|
+
function findInspPath(el) {
|
|
203
|
+
while (el) {
|
|
204
|
+
const attr = el.getAttribute?.("data-insp-path");
|
|
205
|
+
if (attr) return attr;
|
|
206
|
+
el = el.parentElement;
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
function useInspector({
|
|
211
|
+
enabled,
|
|
212
|
+
port,
|
|
213
|
+
overlayRef,
|
|
214
|
+
tooltipRef,
|
|
215
|
+
ignoreRefs
|
|
216
|
+
}) {
|
|
217
|
+
const currentTarget = (0, import_react3.useRef)(null);
|
|
218
|
+
(0, import_react3.useEffect)(() => {
|
|
219
|
+
if (!enabled) return;
|
|
220
|
+
const isIgnored = (e) => ignoreRefs.some(
|
|
221
|
+
(ref) => ref.current && (e.target === ref.current || ref.current.contains(e.target))
|
|
222
|
+
);
|
|
223
|
+
const blockEvent = (e) => {
|
|
224
|
+
if (isIgnored(e)) return;
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
e.stopPropagation();
|
|
227
|
+
};
|
|
228
|
+
const handleClick = (e) => {
|
|
229
|
+
if (isIgnored(e)) return;
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
e.stopPropagation();
|
|
232
|
+
const attr = findInspPath(e.target);
|
|
233
|
+
if (!attr) return;
|
|
234
|
+
const { filePath, line, column } = parseInspPath(attr);
|
|
235
|
+
const url = `http://localhost:${port}/?file=${encodeURIComponent(filePath)}&line=${line}&column=${column}`;
|
|
236
|
+
const xhr = new XMLHttpRequest();
|
|
237
|
+
xhr.open("GET", url, true);
|
|
238
|
+
xhr.send();
|
|
239
|
+
};
|
|
240
|
+
const events = ["mousedown", "mouseup", "pointerdown", "pointerup"];
|
|
241
|
+
window.addEventListener("click", handleClick, true);
|
|
242
|
+
events.forEach((e) => window.addEventListener(e, blockEvent, true));
|
|
243
|
+
return () => {
|
|
244
|
+
window.removeEventListener("click", handleClick, true);
|
|
245
|
+
events.forEach((e) => window.removeEventListener(e, blockEvent, true));
|
|
246
|
+
};
|
|
247
|
+
}, [enabled, port, ignoreRefs]);
|
|
248
|
+
(0, import_react3.useEffect)(() => {
|
|
249
|
+
if (!enabled) {
|
|
250
|
+
if (overlayRef.current) overlayRef.current.style.display = "none";
|
|
251
|
+
if (tooltipRef.current) tooltipRef.current.style.display = "none";
|
|
252
|
+
currentTarget.current = null;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
let mouseX = 0;
|
|
256
|
+
let mouseY = 0;
|
|
257
|
+
const hideOverlay = () => {
|
|
258
|
+
if (overlayRef.current) overlayRef.current.style.display = "none";
|
|
259
|
+
if (tooltipRef.current) tooltipRef.current.style.display = "none";
|
|
260
|
+
};
|
|
261
|
+
const isIgnoredElement = (target) => ignoreRefs.some(
|
|
262
|
+
(ref) => ref.current && (target === ref.current || ref.current.contains(target))
|
|
263
|
+
);
|
|
264
|
+
const updateTooltipContent = (target, info) => {
|
|
265
|
+
const tooltip = tooltipRef.current;
|
|
266
|
+
if (!tooltip) return;
|
|
267
|
+
tooltip.textContent = "";
|
|
268
|
+
const nameLine = document.createElement("div");
|
|
269
|
+
nameLine.textContent = info.name;
|
|
270
|
+
tooltip.appendChild(nameLine);
|
|
271
|
+
const attr = findInspPath(target);
|
|
272
|
+
if (attr) {
|
|
273
|
+
const { filePath, line } = parseInspPath(attr);
|
|
274
|
+
const pathLine = document.createElement("div");
|
|
275
|
+
pathLine.textContent = `${filePath}:${line}`;
|
|
276
|
+
pathLine.style.opacity = "0.7";
|
|
277
|
+
pathLine.style.fontSize = "10px";
|
|
278
|
+
tooltip.appendChild(pathLine);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const inspectTarget = (target) => {
|
|
282
|
+
if (!target || target === overlayRef.current || target === tooltipRef.current || isIgnoredElement(target) || target === document.documentElement || target === document.body) {
|
|
283
|
+
if (target === document.documentElement || target === document.body) {
|
|
284
|
+
hideOverlay();
|
|
285
|
+
currentTarget.current = null;
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (target === currentTarget.current) return;
|
|
290
|
+
currentTarget.current = target;
|
|
291
|
+
const info = getComponentInfo(target);
|
|
292
|
+
if (!info) {
|
|
293
|
+
hideOverlay();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
updateTooltipContent(target, info);
|
|
297
|
+
};
|
|
298
|
+
const updateOverlayPosition = () => {
|
|
299
|
+
const target = currentTarget.current;
|
|
300
|
+
if (!target || !target.isConnected) {
|
|
301
|
+
hideOverlay();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const rect = target.getBoundingClientRect();
|
|
305
|
+
if (overlayRef.current) {
|
|
306
|
+
const s = overlayRef.current.style;
|
|
307
|
+
s.display = "block";
|
|
308
|
+
s.top = `${rect.top}px`;
|
|
309
|
+
s.left = `${rect.left}px`;
|
|
310
|
+
s.width = `${rect.width}px`;
|
|
311
|
+
s.height = `${rect.height}px`;
|
|
312
|
+
}
|
|
313
|
+
if (tooltipRef.current) {
|
|
314
|
+
const s = tooltipRef.current.style;
|
|
315
|
+
s.display = "block";
|
|
316
|
+
const gap = 12;
|
|
317
|
+
let top = mouseY + gap;
|
|
318
|
+
let left = mouseX + gap;
|
|
319
|
+
if (top + tooltipRef.current.offsetHeight > window.innerHeight) {
|
|
320
|
+
top = mouseY - tooltipRef.current.offsetHeight - gap;
|
|
321
|
+
}
|
|
322
|
+
if (left + tooltipRef.current.offsetWidth > window.innerWidth) {
|
|
323
|
+
left = mouseX - tooltipRef.current.offsetWidth - gap;
|
|
324
|
+
}
|
|
325
|
+
s.top = `${Math.max(0, top)}px`;
|
|
326
|
+
s.left = `${Math.max(0, left)}px`;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const handleMouseMove = (e) => {
|
|
330
|
+
mouseX = e.clientX;
|
|
331
|
+
mouseY = e.clientY;
|
|
332
|
+
inspectTarget(e.target);
|
|
333
|
+
updateOverlayPosition();
|
|
334
|
+
};
|
|
335
|
+
const handleMouseLeave = () => {
|
|
336
|
+
hideOverlay();
|
|
337
|
+
currentTarget.current = null;
|
|
338
|
+
};
|
|
339
|
+
let rafId;
|
|
340
|
+
const tick = () => {
|
|
341
|
+
const el = document.elementFromPoint(mouseX, mouseY);
|
|
342
|
+
if (el) inspectTarget(el);
|
|
343
|
+
updateOverlayPosition();
|
|
344
|
+
rafId = requestAnimationFrame(tick);
|
|
345
|
+
};
|
|
346
|
+
rafId = requestAnimationFrame(tick);
|
|
347
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
348
|
+
document.addEventListener("mouseleave", handleMouseLeave);
|
|
349
|
+
return () => {
|
|
350
|
+
cancelAnimationFrame(rafId);
|
|
351
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
352
|
+
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
353
|
+
};
|
|
354
|
+
}, [enabled, overlayRef, tooltipRef, ignoreRefs]);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/xray.tsx
|
|
358
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
359
|
+
var DEFAULT_PORT = 5678;
|
|
360
|
+
var DEFAULT_COLOR = "#6366f1";
|
|
361
|
+
function colorWithAlpha(hex, alpha) {
|
|
362
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
363
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
364
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
365
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
366
|
+
}
|
|
367
|
+
function XrayImpl({
|
|
368
|
+
hotKey = DEFAULT_HOT_KEY,
|
|
369
|
+
port = DEFAULT_PORT,
|
|
370
|
+
color = DEFAULT_COLOR,
|
|
371
|
+
showButton = true,
|
|
372
|
+
followNextIndicator = true
|
|
373
|
+
} = {}) {
|
|
374
|
+
const [enabled, setEnabled] = (0, import_react4.useState)(false);
|
|
375
|
+
const overlayRef = (0, import_react4.useRef)(null);
|
|
376
|
+
const tooltipRef = (0, import_react4.useRef)(null);
|
|
377
|
+
const badgeRef = (0, import_react4.useRef)(null);
|
|
378
|
+
const toggle = (0, import_react4.useCallback)(() => setEnabled((prev) => !prev), []);
|
|
379
|
+
useHotkey(hotKey, toggle);
|
|
380
|
+
useBadge({ badgeRef, show: showButton, followNextIndicator });
|
|
381
|
+
useInspector({
|
|
382
|
+
enabled,
|
|
383
|
+
port,
|
|
384
|
+
overlayRef,
|
|
385
|
+
tooltipRef,
|
|
386
|
+
ignoreRefs: [badgeRef]
|
|
387
|
+
});
|
|
388
|
+
return (0, import_react_dom.createPortal)(
|
|
389
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
390
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
391
|
+
"div",
|
|
392
|
+
{
|
|
393
|
+
ref: overlayRef,
|
|
394
|
+
style: {
|
|
395
|
+
position: "fixed",
|
|
396
|
+
pointerEvents: "none",
|
|
397
|
+
zIndex: 99998,
|
|
398
|
+
border: `2px solid ${color}`,
|
|
399
|
+
borderRadius: "3px",
|
|
400
|
+
backgroundColor: colorWithAlpha(color, 0.08),
|
|
401
|
+
transition: "none",
|
|
402
|
+
display: "none"
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
),
|
|
406
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
407
|
+
"div",
|
|
408
|
+
{
|
|
409
|
+
ref: tooltipRef,
|
|
410
|
+
style: {
|
|
411
|
+
position: "fixed",
|
|
412
|
+
pointerEvents: "none",
|
|
413
|
+
zIndex: 99999,
|
|
414
|
+
backgroundColor: color,
|
|
415
|
+
color: "white",
|
|
416
|
+
fontSize: "11px",
|
|
417
|
+
fontFamily: "monospace",
|
|
418
|
+
fontWeight: 600,
|
|
419
|
+
padding: "2px 7px",
|
|
420
|
+
borderRadius: "4px",
|
|
421
|
+
whiteSpace: "nowrap",
|
|
422
|
+
display: "none",
|
|
423
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.25)"
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
),
|
|
427
|
+
showButton && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
428
|
+
"div",
|
|
429
|
+
{
|
|
430
|
+
ref: badgeRef,
|
|
431
|
+
style: {
|
|
432
|
+
position: "fixed",
|
|
433
|
+
zIndex: 2147483646,
|
|
434
|
+
width: "36px",
|
|
435
|
+
height: "36px",
|
|
436
|
+
transformOrigin: "center center"
|
|
437
|
+
},
|
|
438
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
439
|
+
"button",
|
|
440
|
+
{
|
|
441
|
+
onClick: (e) => {
|
|
442
|
+
e.stopPropagation();
|
|
443
|
+
toggle();
|
|
444
|
+
},
|
|
445
|
+
style: {
|
|
446
|
+
width: "36px",
|
|
447
|
+
height: "36px",
|
|
448
|
+
display: "flex",
|
|
449
|
+
alignItems: "center",
|
|
450
|
+
justifyContent: "center",
|
|
451
|
+
background: "rgba(0, 0, 0, 0.8)",
|
|
452
|
+
backdropFilter: "blur(48px)",
|
|
453
|
+
border: "none",
|
|
454
|
+
borderRadius: "50%",
|
|
455
|
+
boxShadow: enabled ? `0 0 0 1px ${color}, inset 0 0 0 1px ${colorWithAlpha(color, 0.4)}, 0 16px 32px -8px rgba(0, 0, 0, 0.24)` : "0 0 0 1px #171717, inset 0 0 0 1px hsla(0, 0%, 100%, 0.14), 0 16px 32px -8px rgba(0, 0, 0, 0.24)",
|
|
456
|
+
opacity: enabled ? 1 : 0.4,
|
|
457
|
+
transition: "opacity 200ms ease, box-shadow 200ms ease",
|
|
458
|
+
cursor: "pointer",
|
|
459
|
+
userSelect: "none",
|
|
460
|
+
padding: 0
|
|
461
|
+
},
|
|
462
|
+
onMouseEnter: (e) => {
|
|
463
|
+
e.currentTarget.style.opacity = "1";
|
|
464
|
+
},
|
|
465
|
+
onMouseLeave: (e) => {
|
|
466
|
+
if (!enabled) e.currentTarget.style.opacity = "0.4";
|
|
467
|
+
},
|
|
468
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
469
|
+
"svg",
|
|
470
|
+
{
|
|
471
|
+
width: "20",
|
|
472
|
+
height: "20",
|
|
473
|
+
viewBox: "0 0 24 24",
|
|
474
|
+
fill: "none",
|
|
475
|
+
stroke: enabled ? color : "white",
|
|
476
|
+
strokeWidth: "2",
|
|
477
|
+
strokeLinecap: "round",
|
|
478
|
+
strokeLinejoin: "round",
|
|
479
|
+
style: {
|
|
480
|
+
transition: "stroke 200ms ease, transform 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)",
|
|
481
|
+
transform: enabled ? "rotate(90deg)" : "rotate(0deg)"
|
|
482
|
+
},
|
|
483
|
+
children: [
|
|
484
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "10", strokeOpacity: "0.5" }),
|
|
485
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "12", cy: "12", r: "4" }),
|
|
486
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "2", x2: "12", y2: "6" }),
|
|
487
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "12", y1: "18", x2: "12", y2: "22" }),
|
|
488
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "2", y1: "12", x2: "6", y2: "12" }),
|
|
489
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("line", { x1: "18", y1: "12", x2: "22", y2: "12" })
|
|
490
|
+
]
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
)
|
|
497
|
+
] }),
|
|
498
|
+
document.body
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
var Xray = process.env.NODE_ENV === "development" ? XrayImpl : () => null;
|
|
502
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
503
|
+
0 && (module.exports = {
|
|
504
|
+
Xray
|
|
505
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface HotKey {
|
|
2
|
+
metaKey?: boolean;
|
|
3
|
+
ctrlKey?: boolean;
|
|
4
|
+
altKey?: boolean;
|
|
5
|
+
shiftKey?: boolean;
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface XrayProps {
|
|
10
|
+
/** Keyboard shortcut to toggle. Default: Cmd+Shift+X */
|
|
11
|
+
hotKey?: HotKey;
|
|
12
|
+
/** code-inspector-plugin server port. Default: 5678 */
|
|
13
|
+
port?: number;
|
|
14
|
+
/** Accent color for overlay/tooltip/button. Default: '#6366f1' (indigo) */
|
|
15
|
+
color?: string;
|
|
16
|
+
/** Whether to show the floating toggle button. Default: true */
|
|
17
|
+
showButton?: boolean;
|
|
18
|
+
/** Whether to position next to Next.js dev indicator. Default: true */
|
|
19
|
+
followNextIndicator?: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare const Xray: (props?: XrayProps) => React.ReactNode;
|
|
22
|
+
|
|
23
|
+
export { Xray, type XrayProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface HotKey {
|
|
2
|
+
metaKey?: boolean;
|
|
3
|
+
ctrlKey?: boolean;
|
|
4
|
+
altKey?: boolean;
|
|
5
|
+
shiftKey?: boolean;
|
|
6
|
+
key: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface XrayProps {
|
|
10
|
+
/** Keyboard shortcut to toggle. Default: Cmd+Shift+X */
|
|
11
|
+
hotKey?: HotKey;
|
|
12
|
+
/** code-inspector-plugin server port. Default: 5678 */
|
|
13
|
+
port?: number;
|
|
14
|
+
/** Accent color for overlay/tooltip/button. Default: '#6366f1' (indigo) */
|
|
15
|
+
color?: string;
|
|
16
|
+
/** Whether to show the floating toggle button. Default: true */
|
|
17
|
+
showButton?: boolean;
|
|
18
|
+
/** Whether to position next to Next.js dev indicator. Default: true */
|
|
19
|
+
followNextIndicator?: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare const Xray: (props?: XrayProps) => React.ReactNode;
|
|
22
|
+
|
|
23
|
+
export { Xray, type XrayProps };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/xray.tsx
|
|
4
|
+
import { useCallback, useRef as useRef2, useState } from "react";
|
|
5
|
+
import { createPortal } from "react-dom";
|
|
6
|
+
|
|
7
|
+
// src/use-badge.ts
|
|
8
|
+
import { useEffect } from "react";
|
|
9
|
+
var BADGE_SIZE = 36;
|
|
10
|
+
var BADGE_GAP = 4;
|
|
11
|
+
var DRAG_THRESHOLD = 10;
|
|
12
|
+
function findNextjsIndicator() {
|
|
13
|
+
const portal = document.querySelector("nextjs-portal");
|
|
14
|
+
if (!portal?.shadowRoot) return null;
|
|
15
|
+
return portal.shadowRoot.querySelector("[data-nextjs-toast]") ?? portal.shadowRoot.querySelector("div");
|
|
16
|
+
}
|
|
17
|
+
function useBadge({ badgeRef, show, followNextIndicator }) {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!show) return;
|
|
20
|
+
const badge = badgeRef.current;
|
|
21
|
+
if (!badge) return;
|
|
22
|
+
badge.style.scale = "0";
|
|
23
|
+
let rafId;
|
|
24
|
+
let dragging = false;
|
|
25
|
+
let dragStartX = 0;
|
|
26
|
+
let dragStartY = 0;
|
|
27
|
+
let hidden = false;
|
|
28
|
+
let settling = false;
|
|
29
|
+
let positioned = false;
|
|
30
|
+
let settled = false;
|
|
31
|
+
let hideTimeout;
|
|
32
|
+
let reappearTimeout;
|
|
33
|
+
const getIndicator = () => followNextIndicator ? findNextjsIndicator() : null;
|
|
34
|
+
const positionBadge = () => {
|
|
35
|
+
if (!badge) return;
|
|
36
|
+
const indicator = getIndicator();
|
|
37
|
+
if (indicator) {
|
|
38
|
+
const rect = indicator.getBoundingClientRect();
|
|
39
|
+
const midX = rect.left + rect.width / 2;
|
|
40
|
+
const isRight = midX > window.innerWidth / 2;
|
|
41
|
+
badge.style.left = "";
|
|
42
|
+
badge.style.bottom = "";
|
|
43
|
+
badge.style.left = isRight ? `${rect.left - BADGE_SIZE - BADGE_GAP}px` : `${rect.right + BADGE_GAP}px`;
|
|
44
|
+
badge.style.top = `${rect.top + (rect.height - BADGE_SIZE) / 2}px`;
|
|
45
|
+
} else {
|
|
46
|
+
badge.style.top = "";
|
|
47
|
+
badge.style.bottom = "12px";
|
|
48
|
+
badge.style.left = "12px";
|
|
49
|
+
settled = true;
|
|
50
|
+
}
|
|
51
|
+
if (!positioned) {
|
|
52
|
+
positioned = true;
|
|
53
|
+
requestAnimationFrame(() => {
|
|
54
|
+
badge.style.scale = "1";
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const onPointerDown = (e) => {
|
|
59
|
+
const indicator = getIndicator();
|
|
60
|
+
if (!indicator) return;
|
|
61
|
+
const rect = indicator.getBoundingClientRect();
|
|
62
|
+
if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) {
|
|
63
|
+
dragging = true;
|
|
64
|
+
dragStartX = e.clientX;
|
|
65
|
+
dragStartY = e.clientY;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const onPointerMove = (e) => {
|
|
69
|
+
if (!dragging) return;
|
|
70
|
+
const dx = e.clientX - dragStartX;
|
|
71
|
+
const dy = e.clientY - dragStartY;
|
|
72
|
+
if (!hidden && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
|
|
73
|
+
hidden = true;
|
|
74
|
+
settling = true;
|
|
75
|
+
badge.style.transition = "scale 200ms ease";
|
|
76
|
+
badge.style.scale = "0";
|
|
77
|
+
hideTimeout = setTimeout(() => {
|
|
78
|
+
badge.style.display = "none";
|
|
79
|
+
}, 200);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const onPointerUp = () => {
|
|
83
|
+
if (!dragging) return;
|
|
84
|
+
dragging = false;
|
|
85
|
+
if (hidden) {
|
|
86
|
+
reappearTimeout = setTimeout(() => {
|
|
87
|
+
hidden = false;
|
|
88
|
+
settling = false;
|
|
89
|
+
settled = false;
|
|
90
|
+
badge.style.scale = "0";
|
|
91
|
+
badge.style.display = "";
|
|
92
|
+
badge.style.transition = "scale 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)";
|
|
93
|
+
void badge.offsetHeight;
|
|
94
|
+
badge.style.scale = "1";
|
|
95
|
+
}, 1e3);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
let lastKey = "";
|
|
99
|
+
const tick = () => {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
if (!hidden && !settling) {
|
|
102
|
+
const indicator = getIndicator();
|
|
103
|
+
if (indicator) {
|
|
104
|
+
const rect = indicator.getBoundingClientRect();
|
|
105
|
+
const key = `${rect.top},${rect.left},${rect.right},${rect.bottom}`;
|
|
106
|
+
if (key !== lastKey) {
|
|
107
|
+
lastKey = key;
|
|
108
|
+
positionBadge();
|
|
109
|
+
}
|
|
110
|
+
} else if (lastKey !== "fallback") {
|
|
111
|
+
lastKey = "fallback";
|
|
112
|
+
positionBadge();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
rafId = requestAnimationFrame(tick);
|
|
116
|
+
};
|
|
117
|
+
rafId = requestAnimationFrame(tick);
|
|
118
|
+
window.addEventListener("pointerdown", onPointerDown, true);
|
|
119
|
+
window.addEventListener("pointermove", onPointerMove, true);
|
|
120
|
+
window.addEventListener("pointerup", onPointerUp, true);
|
|
121
|
+
return () => {
|
|
122
|
+
cancelAnimationFrame(rafId);
|
|
123
|
+
clearTimeout(hideTimeout);
|
|
124
|
+
clearTimeout(reappearTimeout);
|
|
125
|
+
window.removeEventListener("pointerdown", onPointerDown, true);
|
|
126
|
+
window.removeEventListener("pointermove", onPointerMove, true);
|
|
127
|
+
window.removeEventListener("pointerup", onPointerUp, true);
|
|
128
|
+
};
|
|
129
|
+
}, [badgeRef, show, followNextIndicator]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/use-hotkey.ts
|
|
133
|
+
import { useEffect as useEffect2 } from "react";
|
|
134
|
+
var DEFAULT_HOT_KEY = { metaKey: true, shiftKey: true, key: "x" };
|
|
135
|
+
function useHotkey(hotKey, onToggle) {
|
|
136
|
+
useEffect2(() => {
|
|
137
|
+
const handler = (e) => {
|
|
138
|
+
if (e.key === hotKey.key && !!e.metaKey === !!hotKey.metaKey && !!e.ctrlKey === !!hotKey.ctrlKey && !!e.altKey === !!hotKey.altKey && !!e.shiftKey === !!hotKey.shiftKey) {
|
|
139
|
+
onToggle();
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
window.addEventListener("keydown", handler);
|
|
143
|
+
return () => window.removeEventListener("keydown", handler);
|
|
144
|
+
}, [hotKey.key, hotKey.metaKey, hotKey.ctrlKey, hotKey.altKey, hotKey.shiftKey, onToggle]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/use-inspector.ts
|
|
148
|
+
import { useEffect as useEffect3, useRef } from "react";
|
|
149
|
+
function getFiberFromElement(el) {
|
|
150
|
+
const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
|
|
151
|
+
return key ? el[key] : null;
|
|
152
|
+
}
|
|
153
|
+
function getComponentName(fiber) {
|
|
154
|
+
if (!fiber.type || typeof fiber.type === "string") return null;
|
|
155
|
+
return fiber.type.displayName || fiber.type.name || null;
|
|
156
|
+
}
|
|
157
|
+
function getComponentInfo(el) {
|
|
158
|
+
let fiber = getFiberFromElement(el);
|
|
159
|
+
if (!fiber) return null;
|
|
160
|
+
let depth = 0;
|
|
161
|
+
while (fiber && depth < 30) {
|
|
162
|
+
const name = getComponentName(fiber);
|
|
163
|
+
if (name) return { name };
|
|
164
|
+
fiber = fiber.return;
|
|
165
|
+
depth++;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
function parseInspPath(attr) {
|
|
170
|
+
const parts = attr.split(":");
|
|
171
|
+
parts.pop();
|
|
172
|
+
const column = parts.pop();
|
|
173
|
+
const line = parts.pop();
|
|
174
|
+
const filePath = parts.join(":");
|
|
175
|
+
return { filePath, line, column };
|
|
176
|
+
}
|
|
177
|
+
function findInspPath(el) {
|
|
178
|
+
while (el) {
|
|
179
|
+
const attr = el.getAttribute?.("data-insp-path");
|
|
180
|
+
if (attr) return attr;
|
|
181
|
+
el = el.parentElement;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
function useInspector({
|
|
186
|
+
enabled,
|
|
187
|
+
port,
|
|
188
|
+
overlayRef,
|
|
189
|
+
tooltipRef,
|
|
190
|
+
ignoreRefs
|
|
191
|
+
}) {
|
|
192
|
+
const currentTarget = useRef(null);
|
|
193
|
+
useEffect3(() => {
|
|
194
|
+
if (!enabled) return;
|
|
195
|
+
const isIgnored = (e) => ignoreRefs.some(
|
|
196
|
+
(ref) => ref.current && (e.target === ref.current || ref.current.contains(e.target))
|
|
197
|
+
);
|
|
198
|
+
const blockEvent = (e) => {
|
|
199
|
+
if (isIgnored(e)) return;
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
e.stopPropagation();
|
|
202
|
+
};
|
|
203
|
+
const handleClick = (e) => {
|
|
204
|
+
if (isIgnored(e)) return;
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
e.stopPropagation();
|
|
207
|
+
const attr = findInspPath(e.target);
|
|
208
|
+
if (!attr) return;
|
|
209
|
+
const { filePath, line, column } = parseInspPath(attr);
|
|
210
|
+
const url = `http://localhost:${port}/?file=${encodeURIComponent(filePath)}&line=${line}&column=${column}`;
|
|
211
|
+
const xhr = new XMLHttpRequest();
|
|
212
|
+
xhr.open("GET", url, true);
|
|
213
|
+
xhr.send();
|
|
214
|
+
};
|
|
215
|
+
const events = ["mousedown", "mouseup", "pointerdown", "pointerup"];
|
|
216
|
+
window.addEventListener("click", handleClick, true);
|
|
217
|
+
events.forEach((e) => window.addEventListener(e, blockEvent, true));
|
|
218
|
+
return () => {
|
|
219
|
+
window.removeEventListener("click", handleClick, true);
|
|
220
|
+
events.forEach((e) => window.removeEventListener(e, blockEvent, true));
|
|
221
|
+
};
|
|
222
|
+
}, [enabled, port, ignoreRefs]);
|
|
223
|
+
useEffect3(() => {
|
|
224
|
+
if (!enabled) {
|
|
225
|
+
if (overlayRef.current) overlayRef.current.style.display = "none";
|
|
226
|
+
if (tooltipRef.current) tooltipRef.current.style.display = "none";
|
|
227
|
+
currentTarget.current = null;
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
let mouseX = 0;
|
|
231
|
+
let mouseY = 0;
|
|
232
|
+
const hideOverlay = () => {
|
|
233
|
+
if (overlayRef.current) overlayRef.current.style.display = "none";
|
|
234
|
+
if (tooltipRef.current) tooltipRef.current.style.display = "none";
|
|
235
|
+
};
|
|
236
|
+
const isIgnoredElement = (target) => ignoreRefs.some(
|
|
237
|
+
(ref) => ref.current && (target === ref.current || ref.current.contains(target))
|
|
238
|
+
);
|
|
239
|
+
const updateTooltipContent = (target, info) => {
|
|
240
|
+
const tooltip = tooltipRef.current;
|
|
241
|
+
if (!tooltip) return;
|
|
242
|
+
tooltip.textContent = "";
|
|
243
|
+
const nameLine = document.createElement("div");
|
|
244
|
+
nameLine.textContent = info.name;
|
|
245
|
+
tooltip.appendChild(nameLine);
|
|
246
|
+
const attr = findInspPath(target);
|
|
247
|
+
if (attr) {
|
|
248
|
+
const { filePath, line } = parseInspPath(attr);
|
|
249
|
+
const pathLine = document.createElement("div");
|
|
250
|
+
pathLine.textContent = `${filePath}:${line}`;
|
|
251
|
+
pathLine.style.opacity = "0.7";
|
|
252
|
+
pathLine.style.fontSize = "10px";
|
|
253
|
+
tooltip.appendChild(pathLine);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const inspectTarget = (target) => {
|
|
257
|
+
if (!target || target === overlayRef.current || target === tooltipRef.current || isIgnoredElement(target) || target === document.documentElement || target === document.body) {
|
|
258
|
+
if (target === document.documentElement || target === document.body) {
|
|
259
|
+
hideOverlay();
|
|
260
|
+
currentTarget.current = null;
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (target === currentTarget.current) return;
|
|
265
|
+
currentTarget.current = target;
|
|
266
|
+
const info = getComponentInfo(target);
|
|
267
|
+
if (!info) {
|
|
268
|
+
hideOverlay();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
updateTooltipContent(target, info);
|
|
272
|
+
};
|
|
273
|
+
const updateOverlayPosition = () => {
|
|
274
|
+
const target = currentTarget.current;
|
|
275
|
+
if (!target || !target.isConnected) {
|
|
276
|
+
hideOverlay();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const rect = target.getBoundingClientRect();
|
|
280
|
+
if (overlayRef.current) {
|
|
281
|
+
const s = overlayRef.current.style;
|
|
282
|
+
s.display = "block";
|
|
283
|
+
s.top = `${rect.top}px`;
|
|
284
|
+
s.left = `${rect.left}px`;
|
|
285
|
+
s.width = `${rect.width}px`;
|
|
286
|
+
s.height = `${rect.height}px`;
|
|
287
|
+
}
|
|
288
|
+
if (tooltipRef.current) {
|
|
289
|
+
const s = tooltipRef.current.style;
|
|
290
|
+
s.display = "block";
|
|
291
|
+
const gap = 12;
|
|
292
|
+
let top = mouseY + gap;
|
|
293
|
+
let left = mouseX + gap;
|
|
294
|
+
if (top + tooltipRef.current.offsetHeight > window.innerHeight) {
|
|
295
|
+
top = mouseY - tooltipRef.current.offsetHeight - gap;
|
|
296
|
+
}
|
|
297
|
+
if (left + tooltipRef.current.offsetWidth > window.innerWidth) {
|
|
298
|
+
left = mouseX - tooltipRef.current.offsetWidth - gap;
|
|
299
|
+
}
|
|
300
|
+
s.top = `${Math.max(0, top)}px`;
|
|
301
|
+
s.left = `${Math.max(0, left)}px`;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const handleMouseMove = (e) => {
|
|
305
|
+
mouseX = e.clientX;
|
|
306
|
+
mouseY = e.clientY;
|
|
307
|
+
inspectTarget(e.target);
|
|
308
|
+
updateOverlayPosition();
|
|
309
|
+
};
|
|
310
|
+
const handleMouseLeave = () => {
|
|
311
|
+
hideOverlay();
|
|
312
|
+
currentTarget.current = null;
|
|
313
|
+
};
|
|
314
|
+
let rafId;
|
|
315
|
+
const tick = () => {
|
|
316
|
+
const el = document.elementFromPoint(mouseX, mouseY);
|
|
317
|
+
if (el) inspectTarget(el);
|
|
318
|
+
updateOverlayPosition();
|
|
319
|
+
rafId = requestAnimationFrame(tick);
|
|
320
|
+
};
|
|
321
|
+
rafId = requestAnimationFrame(tick);
|
|
322
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
323
|
+
document.addEventListener("mouseleave", handleMouseLeave);
|
|
324
|
+
return () => {
|
|
325
|
+
cancelAnimationFrame(rafId);
|
|
326
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
327
|
+
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
328
|
+
};
|
|
329
|
+
}, [enabled, overlayRef, tooltipRef, ignoreRefs]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/xray.tsx
|
|
333
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
334
|
+
var DEFAULT_PORT = 5678;
|
|
335
|
+
var DEFAULT_COLOR = "#6366f1";
|
|
336
|
+
function colorWithAlpha(hex, alpha) {
|
|
337
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
338
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
339
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
340
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
341
|
+
}
|
|
342
|
+
function XrayImpl({
|
|
343
|
+
hotKey = DEFAULT_HOT_KEY,
|
|
344
|
+
port = DEFAULT_PORT,
|
|
345
|
+
color = DEFAULT_COLOR,
|
|
346
|
+
showButton = true,
|
|
347
|
+
followNextIndicator = true
|
|
348
|
+
} = {}) {
|
|
349
|
+
const [enabled, setEnabled] = useState(false);
|
|
350
|
+
const overlayRef = useRef2(null);
|
|
351
|
+
const tooltipRef = useRef2(null);
|
|
352
|
+
const badgeRef = useRef2(null);
|
|
353
|
+
const toggle = useCallback(() => setEnabled((prev) => !prev), []);
|
|
354
|
+
useHotkey(hotKey, toggle);
|
|
355
|
+
useBadge({ badgeRef, show: showButton, followNextIndicator });
|
|
356
|
+
useInspector({
|
|
357
|
+
enabled,
|
|
358
|
+
port,
|
|
359
|
+
overlayRef,
|
|
360
|
+
tooltipRef,
|
|
361
|
+
ignoreRefs: [badgeRef]
|
|
362
|
+
});
|
|
363
|
+
return createPortal(
|
|
364
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
365
|
+
/* @__PURE__ */ jsx(
|
|
366
|
+
"div",
|
|
367
|
+
{
|
|
368
|
+
ref: overlayRef,
|
|
369
|
+
style: {
|
|
370
|
+
position: "fixed",
|
|
371
|
+
pointerEvents: "none",
|
|
372
|
+
zIndex: 99998,
|
|
373
|
+
border: `2px solid ${color}`,
|
|
374
|
+
borderRadius: "3px",
|
|
375
|
+
backgroundColor: colorWithAlpha(color, 0.08),
|
|
376
|
+
transition: "none",
|
|
377
|
+
display: "none"
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
),
|
|
381
|
+
/* @__PURE__ */ jsx(
|
|
382
|
+
"div",
|
|
383
|
+
{
|
|
384
|
+
ref: tooltipRef,
|
|
385
|
+
style: {
|
|
386
|
+
position: "fixed",
|
|
387
|
+
pointerEvents: "none",
|
|
388
|
+
zIndex: 99999,
|
|
389
|
+
backgroundColor: color,
|
|
390
|
+
color: "white",
|
|
391
|
+
fontSize: "11px",
|
|
392
|
+
fontFamily: "monospace",
|
|
393
|
+
fontWeight: 600,
|
|
394
|
+
padding: "2px 7px",
|
|
395
|
+
borderRadius: "4px",
|
|
396
|
+
whiteSpace: "nowrap",
|
|
397
|
+
display: "none",
|
|
398
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.25)"
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
),
|
|
402
|
+
showButton && /* @__PURE__ */ jsx(
|
|
403
|
+
"div",
|
|
404
|
+
{
|
|
405
|
+
ref: badgeRef,
|
|
406
|
+
style: {
|
|
407
|
+
position: "fixed",
|
|
408
|
+
zIndex: 2147483646,
|
|
409
|
+
width: "36px",
|
|
410
|
+
height: "36px",
|
|
411
|
+
transformOrigin: "center center"
|
|
412
|
+
},
|
|
413
|
+
children: /* @__PURE__ */ jsx(
|
|
414
|
+
"button",
|
|
415
|
+
{
|
|
416
|
+
onClick: (e) => {
|
|
417
|
+
e.stopPropagation();
|
|
418
|
+
toggle();
|
|
419
|
+
},
|
|
420
|
+
style: {
|
|
421
|
+
width: "36px",
|
|
422
|
+
height: "36px",
|
|
423
|
+
display: "flex",
|
|
424
|
+
alignItems: "center",
|
|
425
|
+
justifyContent: "center",
|
|
426
|
+
background: "rgba(0, 0, 0, 0.8)",
|
|
427
|
+
backdropFilter: "blur(48px)",
|
|
428
|
+
border: "none",
|
|
429
|
+
borderRadius: "50%",
|
|
430
|
+
boxShadow: enabled ? `0 0 0 1px ${color}, inset 0 0 0 1px ${colorWithAlpha(color, 0.4)}, 0 16px 32px -8px rgba(0, 0, 0, 0.24)` : "0 0 0 1px #171717, inset 0 0 0 1px hsla(0, 0%, 100%, 0.14), 0 16px 32px -8px rgba(0, 0, 0, 0.24)",
|
|
431
|
+
opacity: enabled ? 1 : 0.4,
|
|
432
|
+
transition: "opacity 200ms ease, box-shadow 200ms ease",
|
|
433
|
+
cursor: "pointer",
|
|
434
|
+
userSelect: "none",
|
|
435
|
+
padding: 0
|
|
436
|
+
},
|
|
437
|
+
onMouseEnter: (e) => {
|
|
438
|
+
e.currentTarget.style.opacity = "1";
|
|
439
|
+
},
|
|
440
|
+
onMouseLeave: (e) => {
|
|
441
|
+
if (!enabled) e.currentTarget.style.opacity = "0.4";
|
|
442
|
+
},
|
|
443
|
+
children: /* @__PURE__ */ jsxs(
|
|
444
|
+
"svg",
|
|
445
|
+
{
|
|
446
|
+
width: "20",
|
|
447
|
+
height: "20",
|
|
448
|
+
viewBox: "0 0 24 24",
|
|
449
|
+
fill: "none",
|
|
450
|
+
stroke: enabled ? color : "white",
|
|
451
|
+
strokeWidth: "2",
|
|
452
|
+
strokeLinecap: "round",
|
|
453
|
+
strokeLinejoin: "round",
|
|
454
|
+
style: {
|
|
455
|
+
transition: "stroke 200ms ease, transform 300ms cubic-bezier(0.23, 0.88, 0.26, 0.92)",
|
|
456
|
+
transform: enabled ? "rotate(90deg)" : "rotate(0deg)"
|
|
457
|
+
},
|
|
458
|
+
children: [
|
|
459
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "10", strokeOpacity: "0.5" }),
|
|
460
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "4" }),
|
|
461
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "2", x2: "12", y2: "6" }),
|
|
462
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "18", x2: "12", y2: "22" }),
|
|
463
|
+
/* @__PURE__ */ jsx("line", { x1: "2", y1: "12", x2: "6", y2: "12" }),
|
|
464
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "12", x2: "22", y2: "12" })
|
|
465
|
+
]
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
] }),
|
|
473
|
+
document.body
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
var Xray = process.env.NODE_ENV === "development" ? XrayImpl : () => null;
|
|
477
|
+
export {
|
|
478
|
+
Xray
|
|
479
|
+
};
|
package/dist/plugin.cjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/plugin.ts
|
|
21
|
+
var plugin_exports = {};
|
|
22
|
+
__export(plugin_exports, {
|
|
23
|
+
xrayPlugin: () => xrayPlugin
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(plugin_exports);
|
|
26
|
+
var import_code_inspector_plugin = require("code-inspector-plugin");
|
|
27
|
+
function xrayPlugin(options) {
|
|
28
|
+
return (0, import_code_inspector_plugin.codeInspectorPlugin)({
|
|
29
|
+
bundler: options.bundler,
|
|
30
|
+
editor: options.editor ?? "code",
|
|
31
|
+
hotKeys: false,
|
|
32
|
+
showSwitch: false
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
36
|
+
0 && (module.exports = {
|
|
37
|
+
xrayPlugin
|
|
38
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface XrayPluginOptions {
|
|
2
|
+
/** Which bundler you're using. Required. */
|
|
3
|
+
bundler: 'webpack' | 'vite' | 'turbopack' | 'rspack' | 'esbuild';
|
|
4
|
+
/** Editor to open files in. Default: 'code' (VS Code) */
|
|
5
|
+
editor?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Build plugin that injects `data-insp-path` attributes on every DOM element.
|
|
9
|
+
* Wraps `code-inspector-plugin` with xray defaults.
|
|
10
|
+
*
|
|
11
|
+
* Returns a plugin object for webpack/vite/rspack/esbuild,
|
|
12
|
+
* or a rules object for turbopack (Next.js).
|
|
13
|
+
*/
|
|
14
|
+
declare function xrayPlugin(options: XrayPluginOptions): any;
|
|
15
|
+
|
|
16
|
+
export { type XrayPluginOptions, xrayPlugin };
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface XrayPluginOptions {
|
|
2
|
+
/** Which bundler you're using. Required. */
|
|
3
|
+
bundler: 'webpack' | 'vite' | 'turbopack' | 'rspack' | 'esbuild';
|
|
4
|
+
/** Editor to open files in. Default: 'code' (VS Code) */
|
|
5
|
+
editor?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Build plugin that injects `data-insp-path` attributes on every DOM element.
|
|
9
|
+
* Wraps `code-inspector-plugin` with xray defaults.
|
|
10
|
+
*
|
|
11
|
+
* Returns a plugin object for webpack/vite/rspack/esbuild,
|
|
12
|
+
* or a rules object for turbopack (Next.js).
|
|
13
|
+
*/
|
|
14
|
+
declare function xrayPlugin(options: XrayPluginOptions): any;
|
|
15
|
+
|
|
16
|
+
export { type XrayPluginOptions, xrayPlugin };
|
package/dist/plugin.mjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { codeInspectorPlugin } from "code-inspector-plugin";
|
|
3
|
+
function xrayPlugin(options) {
|
|
4
|
+
return codeInspectorPlugin({
|
|
5
|
+
bundler: options.bundler,
|
|
6
|
+
editor: options.editor ?? "code",
|
|
7
|
+
hotKeys: false,
|
|
8
|
+
showSwitch: false
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
xrayPlugin
|
|
13
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stinsky/xray",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React dev inspector — hover to see component names, click to open source in your editor. Works with React 19 + Turbopack.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./plugin": {
|
|
14
|
+
"types": "./dist/plugin.d.ts",
|
|
15
|
+
"import": "./dist/plugin.mjs",
|
|
16
|
+
"require": "./dist/plugin.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.cjs",
|
|
20
|
+
"module": "./dist/index.mjs",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"prepublishOnly": "tsup",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18",
|
|
33
|
+
"react-dom": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"code-inspector-plugin": "^0.19.1"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.5.0",
|
|
40
|
+
"@types/react": "^19.0.0",
|
|
41
|
+
"@types/react-dom": "^19.0.0",
|
|
42
|
+
"react": "^19.0.0",
|
|
43
|
+
"react-dom": "^19.0.0",
|
|
44
|
+
"tsup": "^8.4.0",
|
|
45
|
+
"typescript": "^5.7.0"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"react",
|
|
49
|
+
"inspector",
|
|
50
|
+
"devtools",
|
|
51
|
+
"click-to-component",
|
|
52
|
+
"click-to-source",
|
|
53
|
+
"nextjs",
|
|
54
|
+
"turbopack",
|
|
55
|
+
"vite",
|
|
56
|
+
"webpack"
|
|
57
|
+
],
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "https://github.com/stinsky/xray"
|
|
61
|
+
}
|
|
62
|
+
}
|