vite-plugin-source-locator 1.0.0 → 1.1.1
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/README.md +20 -2
- package/dist/client/controller.js +18 -17
- package/dist/client/css-source.js +6 -3
- package/dist/client/overlay-styles.d.ts +40 -0
- package/dist/client/overlay-styles.js +40 -0
- package/dist/client/overlay.js +17 -76
- package/dist/client/preference.js +4 -2
- package/dist/client/source-panel.d.ts +11 -0
- package/dist/client/source-panel.js +113 -0
- package/dist/client/tooltip-text.d.ts +2 -0
- package/dist/client/tooltip-text.js +28 -0
- package/dist/shared/index.js +10 -5
- package/dist/shared/theme.js +13 -13
- package/dist/vite/babel-plugin.d.ts +8 -7
- package/dist/vite/babel-plugin.js +3 -1
- package/dist/vite/editors.js +3 -60
- package/dist/vite/index.js +3 -2
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -137,14 +137,32 @@ initSourceLocator({
|
|
|
137
137
|
})
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
## IDE Setup
|
|
141
|
+
|
|
142
|
+
IDE opening works on **macOS, Windows, and Linux** via [`launch-editor`](https://github.com/vitejs/launch-editor).
|
|
143
|
+
|
|
144
|
+
The editor CLI must be available on your `PATH`:
|
|
145
|
+
|
|
146
|
+
| IDE | CLI command |
|
|
147
|
+
|-----|-------------|
|
|
148
|
+
| Cursor | `cursor` |
|
|
149
|
+
| VS Code | `code` |
|
|
150
|
+
| WebStorm | `webstorm` |
|
|
151
|
+
|
|
152
|
+
You can override the default editor with environment variables:
|
|
153
|
+
|
|
154
|
+
- `LAUNCH_EDITOR=cursor`
|
|
155
|
+
- `REACT_EDITOR=code`
|
|
156
|
+
|
|
157
|
+
Shift+L cycles through the `ides` list configured in plugin options.
|
|
158
|
+
|
|
140
159
|
## Adding a New IDE
|
|
141
160
|
|
|
142
161
|
1. Extend `LocatorIde` and `IDE_ORDER` in `src/shared/index.ts`
|
|
143
|
-
2.
|
|
162
|
+
2. Use a [launch-editor supported editor name](https://github.com/vitejs/launch-editor#supported-editors) as the new `LocatorIde` value
|
|
144
163
|
|
|
145
164
|
## Limitations
|
|
146
165
|
|
|
147
|
-
- macOS only for IDE opening
|
|
148
166
|
- Dev only — no production impact
|
|
149
167
|
- JSX/TSX only for `data-source` injection
|
|
150
168
|
- CSS line 1 only (no source-map line mapping yet)
|
|
@@ -2,6 +2,11 @@ import { nextClickTarget, parseSourceLocation, resolveTheme } from '../shared/in
|
|
|
2
2
|
import { findCssSource } from './css-source.js';
|
|
3
3
|
import { createLocatorOverlayUi } from './overlay.js';
|
|
4
4
|
import { cycleStoredIde, getStoredIde } from './preference.js';
|
|
5
|
+
const EMPTY_SOURCES = Object.freeze({
|
|
6
|
+
tsx: undefined,
|
|
7
|
+
css: undefined,
|
|
8
|
+
clickTarget: 'tsx',
|
|
9
|
+
});
|
|
5
10
|
function getSourceEl(target, attribute, host) {
|
|
6
11
|
if (!target || host.contains(target) || target === host)
|
|
7
12
|
return null;
|
|
@@ -24,29 +29,29 @@ function readElementSources(el, attribute) {
|
|
|
24
29
|
return { tsx, css, clickTarget: 'tsx' };
|
|
25
30
|
}
|
|
26
31
|
function resolveOpenSource(sources) {
|
|
27
|
-
|
|
28
|
-
return sources.css;
|
|
29
|
-
return sources.tsx;
|
|
32
|
+
return sources[sources.clickTarget];
|
|
30
33
|
}
|
|
31
34
|
export function startPickController(root, host, config) {
|
|
32
35
|
let pickMode = false;
|
|
33
|
-
let sources =
|
|
36
|
+
let sources = EMPTY_SOURCES;
|
|
34
37
|
const ui = createLocatorOverlayUi(root, () => setPickMode(!pickMode), resolveTheme(config.theme));
|
|
35
38
|
function setPickMode(active) {
|
|
36
39
|
pickMode = active;
|
|
37
40
|
ui.setPickActive(active);
|
|
38
41
|
if (!active)
|
|
39
|
-
sources =
|
|
42
|
+
sources = EMPTY_SOURCES;
|
|
40
43
|
}
|
|
44
|
+
const syncSources = (el) => {
|
|
45
|
+
if (el !== ui.getActiveEl())
|
|
46
|
+
sources = readElementSources(el, config.attribute);
|
|
47
|
+
};
|
|
41
48
|
const updateHover = (target, x, y) => {
|
|
42
49
|
const el = getSourceEl(target, config.attribute, host);
|
|
43
50
|
if (!el) {
|
|
44
51
|
ui.removeTooltip();
|
|
45
52
|
return;
|
|
46
53
|
}
|
|
47
|
-
|
|
48
|
-
sources = readElementSources(el, config.attribute);
|
|
49
|
-
}
|
|
54
|
+
syncSources(el);
|
|
50
55
|
ui.showSourceTooltip(el, sources.tsx, sources.css, sources.clickTarget, x, y);
|
|
51
56
|
};
|
|
52
57
|
const onMouseMove = (e) => {
|
|
@@ -74,19 +79,15 @@ export function startPickController(root, host, config) {
|
|
|
74
79
|
e.preventDefault();
|
|
75
80
|
e.stopPropagation();
|
|
76
81
|
const el = getSourceEl(e.target, config.attribute, host);
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (el !== ui.getActiveEl())
|
|
82
|
-
sources = readElementSources(el, config.attribute);
|
|
83
|
-
const openSource = resolveOpenSource(sources);
|
|
84
|
-
if (!openSource) {
|
|
82
|
+
if (el)
|
|
83
|
+
syncSources(el);
|
|
84
|
+
const openSource = el && resolveOpenSource(sources);
|
|
85
|
+
if (!el || !openSource) {
|
|
85
86
|
ui.flashMessage('No source for this element');
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
await openSourceInEditor(openSource, config);
|
|
89
|
-
sources
|
|
90
|
+
sources = { ...sources, clickTarget: nextClickTarget(sources.clickTarget, !!sources.css) };
|
|
90
91
|
ui.showSourceTooltip(el, sources.tsx, sources.css, sources.clickTarget, e.clientX, e.clientY);
|
|
91
92
|
};
|
|
92
93
|
document.addEventListener('keydown', onKeyDown);
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { formatSourceLocation } from '../shared/index.js';
|
|
2
|
+
const SELECTOR_ID_BONUS = 100;
|
|
3
|
+
const SELECTOR_CLASS_BONUS = 50;
|
|
4
|
+
const SELECTOR_CLASS_MATCH_BONUS = 200;
|
|
2
5
|
function scoreSelector(selector, element) {
|
|
3
6
|
let score = selector.length;
|
|
4
7
|
if (selector.includes('#'))
|
|
5
|
-
score +=
|
|
8
|
+
score += SELECTOR_ID_BONUS;
|
|
6
9
|
if (selector.includes('.'))
|
|
7
|
-
score +=
|
|
10
|
+
score += SELECTOR_CLASS_BONUS;
|
|
8
11
|
if (element instanceof HTMLElement) {
|
|
9
12
|
element.classList.forEach((cls) => {
|
|
10
13
|
if (selector.includes(`.${cls}`))
|
|
11
|
-
score +=
|
|
14
|
+
score += SELECTOR_CLASS_MATCH_BONUS;
|
|
12
15
|
});
|
|
13
16
|
}
|
|
14
17
|
return score;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const UI_IDS: {
|
|
2
|
+
readonly badge: "source-locator-badge";
|
|
3
|
+
readonly tooltip: "source-locator-tooltip";
|
|
4
|
+
readonly highlight: "source-locator-highlight";
|
|
5
|
+
};
|
|
6
|
+
export declare const LAYOUT: {
|
|
7
|
+
readonly badge: {
|
|
8
|
+
readonly position: "fixed";
|
|
9
|
+
readonly bottom: "12px";
|
|
10
|
+
readonly right: "12px";
|
|
11
|
+
readonly padding: "8px 12px";
|
|
12
|
+
readonly borderRadius: "999px";
|
|
13
|
+
readonly fontSize: "11px";
|
|
14
|
+
readonly fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
15
|
+
readonly zIndex: "99999";
|
|
16
|
+
readonly cursor: "pointer";
|
|
17
|
+
readonly boxShadow: "0 4px 16px rgba(0, 0, 0, 0.35)";
|
|
18
|
+
};
|
|
19
|
+
readonly tooltip: {
|
|
20
|
+
readonly position: "fixed";
|
|
21
|
+
readonly padding: "8px 12px";
|
|
22
|
+
readonly borderRadius: "8px";
|
|
23
|
+
readonly fontSize: "12px";
|
|
24
|
+
readonly fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace";
|
|
25
|
+
readonly zIndex: "99999";
|
|
26
|
+
readonly pointerEvents: "none";
|
|
27
|
+
readonly boxShadow: "0 8px 24px rgba(0, 0, 0, 0.45)";
|
|
28
|
+
readonly maxWidth: "420px";
|
|
29
|
+
readonly whiteSpace: "pre";
|
|
30
|
+
readonly lineHeight: "1.5";
|
|
31
|
+
};
|
|
32
|
+
readonly highlight: {
|
|
33
|
+
readonly position: "fixed";
|
|
34
|
+
readonly borderWidth: "2px";
|
|
35
|
+
readonly borderStyle: "solid";
|
|
36
|
+
readonly borderRadius: "4px";
|
|
37
|
+
readonly pointerEvents: "none";
|
|
38
|
+
readonly zIndex: "99998";
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const UI_IDS = {
|
|
2
|
+
badge: 'source-locator-badge',
|
|
3
|
+
tooltip: 'source-locator-tooltip',
|
|
4
|
+
highlight: 'source-locator-highlight',
|
|
5
|
+
};
|
|
6
|
+
export const LAYOUT = {
|
|
7
|
+
badge: {
|
|
8
|
+
position: 'fixed',
|
|
9
|
+
bottom: '12px',
|
|
10
|
+
right: '12px',
|
|
11
|
+
padding: '8px 12px',
|
|
12
|
+
borderRadius: '999px',
|
|
13
|
+
fontSize: '11px',
|
|
14
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
15
|
+
zIndex: '99999',
|
|
16
|
+
cursor: 'pointer',
|
|
17
|
+
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.35)',
|
|
18
|
+
},
|
|
19
|
+
tooltip: {
|
|
20
|
+
position: 'fixed',
|
|
21
|
+
padding: '8px 12px',
|
|
22
|
+
borderRadius: '8px',
|
|
23
|
+
fontSize: '12px',
|
|
24
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
25
|
+
zIndex: '99999',
|
|
26
|
+
pointerEvents: 'none',
|
|
27
|
+
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.45)',
|
|
28
|
+
maxWidth: '420px',
|
|
29
|
+
whiteSpace: 'pre',
|
|
30
|
+
lineHeight: '1.5',
|
|
31
|
+
},
|
|
32
|
+
highlight: {
|
|
33
|
+
position: 'fixed',
|
|
34
|
+
borderWidth: '2px',
|
|
35
|
+
borderStyle: 'solid',
|
|
36
|
+
borderRadius: '4px',
|
|
37
|
+
pointerEvents: 'none',
|
|
38
|
+
zIndex: '99998',
|
|
39
|
+
},
|
|
40
|
+
};
|
package/dist/client/overlay.js
CHANGED
|
@@ -1,72 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LAYOUT, UI_IDS } from './overlay-styles.js';
|
|
2
2
|
import { badgeLabel } from './preference.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
badge: {
|
|
10
|
-
position: 'fixed',
|
|
11
|
-
bottom: '12px',
|
|
12
|
-
right: '12px',
|
|
13
|
-
padding: '8px 12px',
|
|
14
|
-
borderRadius: '999px',
|
|
15
|
-
fontSize: '11px',
|
|
16
|
-
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
17
|
-
zIndex: '99999',
|
|
18
|
-
cursor: 'pointer',
|
|
19
|
-
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.35)',
|
|
20
|
-
},
|
|
21
|
-
tooltip: {
|
|
22
|
-
position: 'fixed',
|
|
23
|
-
padding: '8px 12px',
|
|
24
|
-
borderRadius: '8px',
|
|
25
|
-
fontSize: '12px',
|
|
26
|
-
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
27
|
-
zIndex: '99999',
|
|
28
|
-
pointerEvents: 'none',
|
|
29
|
-
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.45)',
|
|
30
|
-
maxWidth: '420px',
|
|
31
|
-
whiteSpace: 'pre',
|
|
32
|
-
lineHeight: '1.5',
|
|
33
|
-
},
|
|
34
|
-
highlight: {
|
|
35
|
-
position: 'fixed',
|
|
36
|
-
borderWidth: '2px',
|
|
37
|
-
borderStyle: 'solid',
|
|
38
|
-
borderRadius: '4px',
|
|
39
|
-
pointerEvents: 'none',
|
|
40
|
-
zIndex: '99998',
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
function formatSourceLabel(source, prefix) {
|
|
44
|
-
const { file, line } = parseSourceLocation(source);
|
|
45
|
-
const name = file.split('/').pop() ?? file;
|
|
46
|
-
const label = line !== '1' ? `${name}:${line}` : name;
|
|
47
|
-
if (prefix)
|
|
48
|
-
return `${prefix}: ${label}`;
|
|
49
|
-
return label;
|
|
50
|
-
}
|
|
51
|
-
function buildTooltipText(tsxSource, cssSource, clickTarget) {
|
|
52
|
-
const lines = [];
|
|
53
|
-
if (tsxSource)
|
|
54
|
-
lines.push(formatSourceLabel(tsxSource, 'TSX'));
|
|
55
|
-
if (cssSource)
|
|
56
|
-
lines.push(formatSourceLabel(cssSource, 'CSS'));
|
|
57
|
-
if (!cssSource) {
|
|
58
|
-
lines.push('Click → open TSX');
|
|
59
|
-
return lines.join('\n');
|
|
60
|
-
}
|
|
61
|
-
if (clickTarget === 'tsx') {
|
|
62
|
-
lines.push('Click → open TSX');
|
|
63
|
-
lines.push('Click again → open CSS');
|
|
64
|
-
return lines.join('\n');
|
|
65
|
-
}
|
|
66
|
-
lines.push('Click → open CSS');
|
|
67
|
-
lines.push('Click again → open TSX');
|
|
68
|
-
return lines.join('\n');
|
|
69
|
-
}
|
|
3
|
+
import { buildTooltipText } from './tooltip-text.js';
|
|
4
|
+
const HIGHLIGHT_PADDING = 2;
|
|
5
|
+
const TOOLTIP_CURSOR_OFFSET = 16;
|
|
6
|
+
const FLASH_DURATION_MS = 1500;
|
|
7
|
+
const FLASH_HORIZONTAL_OFFSET = 80;
|
|
8
|
+
const FLASH_BOTTOM_OFFSET = 80;
|
|
70
9
|
export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
71
10
|
let activeEl = null;
|
|
72
11
|
let flashTimeout = null;
|
|
@@ -83,10 +22,10 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
|
83
22
|
const highlight = document.createElement('div');
|
|
84
23
|
highlight.id = UI_IDS.highlight;
|
|
85
24
|
Object.assign(highlight.style, LAYOUT.highlight, {
|
|
86
|
-
top: `${rect.top -
|
|
87
|
-
left: `${rect.left -
|
|
88
|
-
width: `${rect.width +
|
|
89
|
-
height: `${rect.height +
|
|
25
|
+
top: `${rect.top - HIGHLIGHT_PADDING}px`,
|
|
26
|
+
left: `${rect.left - HIGHLIGHT_PADDING}px`,
|
|
27
|
+
width: `${rect.width + HIGHLIGHT_PADDING * 2}px`,
|
|
28
|
+
height: `${rect.height + HIGHLIGHT_PADDING * 2}px`,
|
|
90
29
|
borderColor: theme.highlightBorder,
|
|
91
30
|
background: theme.highlightBackground,
|
|
92
31
|
boxShadow: `0 0 0 1px ${theme.highlightShadow}`,
|
|
@@ -99,8 +38,8 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
|
99
38
|
tooltip.id = UI_IDS.tooltip;
|
|
100
39
|
tooltip.textContent = text;
|
|
101
40
|
Object.assign(tooltip.style, LAYOUT.tooltip, {
|
|
102
|
-
top: `${y +
|
|
103
|
-
left: `${x +
|
|
41
|
+
top: `${y + TOOLTIP_CURSOR_OFFSET}px`,
|
|
42
|
+
left: `${x + TOOLTIP_CURSOR_OFFSET}px`,
|
|
104
43
|
background: theme.tooltipBackground,
|
|
105
44
|
color: theme.tooltipText,
|
|
106
45
|
border: `1px solid ${theme.tooltipBorder}`,
|
|
@@ -116,8 +55,10 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
|
116
55
|
const flashMessage = (text) => {
|
|
117
56
|
if (flashTimeout)
|
|
118
57
|
clearTimeout(flashTimeout);
|
|
119
|
-
|
|
120
|
-
|
|
58
|
+
const x = window.innerWidth / 2 - FLASH_HORIZONTAL_OFFSET;
|
|
59
|
+
const y = window.innerHeight - FLASH_BOTTOM_OFFSET;
|
|
60
|
+
showTooltip(text, null, x, y);
|
|
61
|
+
flashTimeout = setTimeout(removeTooltip, FLASH_DURATION_MS);
|
|
121
62
|
};
|
|
122
63
|
const applyBadgeColors = (active) => {
|
|
123
64
|
if (!badgeEl)
|
|
@@ -11,9 +11,11 @@ export function cycleStoredIde(order) {
|
|
|
11
11
|
setStoredIde(next);
|
|
12
12
|
return next;
|
|
13
13
|
}
|
|
14
|
+
const BADGE_PICKING_HINT = 'Esc to cancel';
|
|
15
|
+
const BADGE_IDLE_HINT = 'click to pick';
|
|
14
16
|
export function badgeLabel(picking) {
|
|
15
17
|
const ide = getStoredIde();
|
|
16
18
|
if (picking)
|
|
17
|
-
return `Pick element (${ide}) —
|
|
18
|
-
return `Source Locator (${ide}) —
|
|
19
|
+
return `Pick element (${ide}) — ${BADGE_PICKING_HINT}`;
|
|
20
|
+
return `Source Locator (${ide}) — ${BADGE_IDLE_HINT}`;
|
|
19
21
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LocatorTheme } from '../shared/index.js';
|
|
2
|
+
export type SourcePanelPayload = {
|
|
3
|
+
file: string;
|
|
4
|
+
line: string;
|
|
5
|
+
col: string;
|
|
6
|
+
content: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function createSourcePanel(root: ShadowRoot, theme: LocatorTheme): {
|
|
9
|
+
showSourcePanel: (payload: SourcePanelPayload) => void;
|
|
10
|
+
removePanel: () => void | undefined;
|
|
11
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const PANEL_ID = 'source-locator-panel';
|
|
2
|
+
function fileBasename(file) {
|
|
3
|
+
return file.split('/').pop() ?? file;
|
|
4
|
+
}
|
|
5
|
+
function escapeHtml(text) {
|
|
6
|
+
return text
|
|
7
|
+
.replace(/&/g, '&')
|
|
8
|
+
.replace(/</g, '<')
|
|
9
|
+
.replace(/>/g, '>');
|
|
10
|
+
}
|
|
11
|
+
export function createSourcePanel(root, theme) {
|
|
12
|
+
const removePanel = () => root.getElementById(PANEL_ID)?.remove();
|
|
13
|
+
const showSourcePanel = (payload) => {
|
|
14
|
+
removePanel();
|
|
15
|
+
const lineNumber = Math.max(1, Number.parseInt(payload.line, 10) || 1);
|
|
16
|
+
const lines = payload.content.split('\n');
|
|
17
|
+
const panel = document.createElement('div');
|
|
18
|
+
panel.id = PANEL_ID;
|
|
19
|
+
Object.assign(panel.style, {
|
|
20
|
+
position: 'fixed',
|
|
21
|
+
left: '0',
|
|
22
|
+
right: '0',
|
|
23
|
+
bottom: '0',
|
|
24
|
+
height: '40vh',
|
|
25
|
+
minHeight: '200px',
|
|
26
|
+
maxHeight: '480px',
|
|
27
|
+
display: 'flex',
|
|
28
|
+
flexDirection: 'column',
|
|
29
|
+
background: theme.tooltipBackground,
|
|
30
|
+
borderTop: `2px solid ${theme.tooltipBorder}`,
|
|
31
|
+
boxShadow: '0 -8px 32px rgba(0, 0, 0, 0.45)',
|
|
32
|
+
zIndex: '100000',
|
|
33
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
34
|
+
fontSize: '12px',
|
|
35
|
+
});
|
|
36
|
+
const header = document.createElement('div');
|
|
37
|
+
Object.assign(header.style, {
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'space-between',
|
|
41
|
+
padding: '8px 12px',
|
|
42
|
+
borderBottom: `1px solid ${theme.tooltipBorder}`,
|
|
43
|
+
color: theme.tooltipText,
|
|
44
|
+
flexShrink: '0',
|
|
45
|
+
});
|
|
46
|
+
const title = document.createElement('span');
|
|
47
|
+
title.textContent = `${fileBasename(payload.file)}:${payload.line}:${payload.col}`;
|
|
48
|
+
const closeBtn = document.createElement('button');
|
|
49
|
+
closeBtn.type = 'button';
|
|
50
|
+
closeBtn.textContent = '×';
|
|
51
|
+
closeBtn.setAttribute('aria-label', 'Close source panel');
|
|
52
|
+
Object.assign(closeBtn.style, {
|
|
53
|
+
background: 'transparent',
|
|
54
|
+
border: 'none',
|
|
55
|
+
color: theme.badgeText,
|
|
56
|
+
fontSize: '20px',
|
|
57
|
+
lineHeight: '1',
|
|
58
|
+
cursor: 'pointer',
|
|
59
|
+
padding: '0 4px',
|
|
60
|
+
});
|
|
61
|
+
closeBtn.addEventListener('click', removePanel);
|
|
62
|
+
header.appendChild(title);
|
|
63
|
+
header.appendChild(closeBtn);
|
|
64
|
+
const body = document.createElement('div');
|
|
65
|
+
Object.assign(body.style, {
|
|
66
|
+
flex: '1',
|
|
67
|
+
overflow: 'auto',
|
|
68
|
+
margin: '0',
|
|
69
|
+
});
|
|
70
|
+
const pre = document.createElement('pre');
|
|
71
|
+
Object.assign(pre.style, {
|
|
72
|
+
margin: '0',
|
|
73
|
+
padding: '8px 0',
|
|
74
|
+
color: theme.tooltipText,
|
|
75
|
+
});
|
|
76
|
+
const highlightedLineId = 'source-locator-panel-highlight';
|
|
77
|
+
lines.forEach((lineText, index) => {
|
|
78
|
+
const lineEl = document.createElement('div');
|
|
79
|
+
const lineNum = index + 1;
|
|
80
|
+
const isTarget = lineNum === lineNumber;
|
|
81
|
+
Object.assign(lineEl.style, {
|
|
82
|
+
display: 'flex',
|
|
83
|
+
padding: '0 12px',
|
|
84
|
+
background: isTarget ? theme.highlightBackground : 'transparent',
|
|
85
|
+
borderLeft: isTarget ? `3px solid ${theme.highlightBorder}` : '3px solid transparent',
|
|
86
|
+
});
|
|
87
|
+
if (isTarget)
|
|
88
|
+
lineEl.id = highlightedLineId;
|
|
89
|
+
const num = document.createElement('span');
|
|
90
|
+
num.textContent = String(lineNum).padStart(4, ' ');
|
|
91
|
+
Object.assign(num.style, {
|
|
92
|
+
flexShrink: '0',
|
|
93
|
+
width: '48px',
|
|
94
|
+
color: theme.badgeText,
|
|
95
|
+
opacity: '0.7',
|
|
96
|
+
userSelect: 'none',
|
|
97
|
+
});
|
|
98
|
+
const code = document.createElement('span');
|
|
99
|
+
code.innerHTML = escapeHtml(lineText) || ' ';
|
|
100
|
+
lineEl.appendChild(num);
|
|
101
|
+
lineEl.appendChild(code);
|
|
102
|
+
pre.appendChild(lineEl);
|
|
103
|
+
});
|
|
104
|
+
body.appendChild(pre);
|
|
105
|
+
panel.appendChild(header);
|
|
106
|
+
panel.appendChild(body);
|
|
107
|
+
root.appendChild(panel);
|
|
108
|
+
requestAnimationFrame(() => {
|
|
109
|
+
root.getElementById(highlightedLineId)?.scrollIntoView({ block: 'center' });
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
return { showSourcePanel, removePanel };
|
|
113
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parseSourceLocation } from '../shared/index.js';
|
|
2
|
+
const DEFAULT_LINE = '1';
|
|
3
|
+
const CLICK_PROMPT_ORDER = {
|
|
4
|
+
tsx: ['TSX', 'CSS'],
|
|
5
|
+
css: ['CSS', 'TSX'],
|
|
6
|
+
};
|
|
7
|
+
function formatSourceLabel(source, prefix) {
|
|
8
|
+
const { file, line } = parseSourceLocation(source);
|
|
9
|
+
const name = file.split('/').pop() ?? file;
|
|
10
|
+
const label = line !== DEFAULT_LINE ? `${name}:${line}` : name;
|
|
11
|
+
if (prefix)
|
|
12
|
+
return `${prefix}: ${label}`;
|
|
13
|
+
return label;
|
|
14
|
+
}
|
|
15
|
+
export function buildTooltipText(tsxSource, cssSource, clickTarget) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
if (tsxSource)
|
|
18
|
+
lines.push(formatSourceLabel(tsxSource, 'TSX'));
|
|
19
|
+
if (cssSource)
|
|
20
|
+
lines.push(formatSourceLabel(cssSource, 'CSS'));
|
|
21
|
+
if (!cssSource) {
|
|
22
|
+
lines.push('Click → open TSX');
|
|
23
|
+
return lines.join('\n');
|
|
24
|
+
}
|
|
25
|
+
const [first, second] = CLICK_PROMPT_ORDER[clickTarget];
|
|
26
|
+
lines.push(`Click → open ${first}`, `Click again → open ${second}`);
|
|
27
|
+
return lines.join('\n');
|
|
28
|
+
}
|
package/dist/shared/index.js
CHANGED
|
@@ -14,16 +14,21 @@ export function nextIde(current, order = IDE_ORDER) {
|
|
|
14
14
|
}
|
|
15
15
|
export function parseSourceLocation(raw) {
|
|
16
16
|
const parts = raw.split(':');
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
17
|
+
if (parts.length < 3)
|
|
18
|
+
return { file: raw, line: '1', col: '1' };
|
|
19
|
+
const col = parts.pop() ?? '1';
|
|
20
|
+
const line = parts.pop() ?? '1';
|
|
21
|
+
return { file: parts.join(':'), line, col };
|
|
21
22
|
}
|
|
22
23
|
export function formatSourceLocation(loc) {
|
|
23
24
|
return `${loc.file}:${loc.line}:${loc.col}`;
|
|
24
25
|
}
|
|
26
|
+
const CLICK_TARGET_TRANSITION = {
|
|
27
|
+
tsx: 'css',
|
|
28
|
+
css: 'tsx',
|
|
29
|
+
};
|
|
25
30
|
export function nextClickTarget(current, hasCss) {
|
|
26
31
|
if (!hasCss)
|
|
27
32
|
return 'tsx';
|
|
28
|
-
return current
|
|
33
|
+
return CLICK_TARGET_TRANSITION[current];
|
|
29
34
|
}
|
package/dist/shared/theme.js
CHANGED
|
@@ -4,10 +4,14 @@ const PRESET_COLORS = {
|
|
|
4
4
|
dark: { background: '#000000', text: '#ffffff', accent: '#a3a3a3' },
|
|
5
5
|
blue: { background: '#1e3a8a', text: '#eff6ff', accent: '#60a5fa' },
|
|
6
6
|
};
|
|
7
|
+
const HEX_COLOR_LENGTH = 7;
|
|
8
|
+
const RGB_CHANNEL_MAX = 255;
|
|
9
|
+
const HIGHLIGHT_ALPHA = 0.12;
|
|
10
|
+
const SHADOW_ALPHA = 0.8;
|
|
7
11
|
function withAlpha(hex, alpha) {
|
|
8
|
-
if (!hex.startsWith('#') || hex.length !==
|
|
12
|
+
if (!hex.startsWith('#') || hex.length !== HEX_COLOR_LENGTH)
|
|
9
13
|
return hex;
|
|
10
|
-
const channel = Math.round(alpha *
|
|
14
|
+
const channel = Math.round(alpha * RGB_CHANNEL_MAX)
|
|
11
15
|
.toString(16)
|
|
12
16
|
.padStart(2, '0');
|
|
13
17
|
return `${hex}${channel}`;
|
|
@@ -24,19 +28,15 @@ function buildTheme(colors) {
|
|
|
24
28
|
tooltipText: text,
|
|
25
29
|
tooltipBorder: accent,
|
|
26
30
|
highlightBorder: accent,
|
|
27
|
-
highlightBackground: withAlpha(accent,
|
|
28
|
-
highlightShadow: withAlpha(background,
|
|
31
|
+
highlightBackground: withAlpha(accent, HIGHLIGHT_ALPHA),
|
|
32
|
+
highlightShadow: withAlpha(background, SHADOW_ALPHA),
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
|
-
function isThemePreset(value) {
|
|
32
|
-
return typeof value === 'string';
|
|
33
|
-
}
|
|
34
35
|
export function resolveTheme(input) {
|
|
36
|
+
const base = PRESET_COLORS.default;
|
|
35
37
|
if (!input)
|
|
36
|
-
return buildTheme(
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return buildTheme({ ...PRESET_COLORS.default, ...input });
|
|
38
|
+
return buildTheme(base);
|
|
39
|
+
if (typeof input === 'string')
|
|
40
|
+
return buildTheme(PRESET_COLORS[input] ?? base);
|
|
41
|
+
return buildTheme({ ...base, ...input });
|
|
42
42
|
}
|
|
@@ -3,16 +3,17 @@ import * as t from '@babel/types';
|
|
|
3
3
|
type BabelOptions = {
|
|
4
4
|
attribute: string;
|
|
5
5
|
};
|
|
6
|
+
type BabelState = {
|
|
7
|
+
file: {
|
|
8
|
+
opts: {
|
|
9
|
+
filename?: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
6
13
|
export declare function babelPluginAddSourceAttr(_: unknown, opts: BabelOptions): {
|
|
7
14
|
name: string;
|
|
8
15
|
visitor: {
|
|
9
|
-
JSXOpeningElement(path: NodePath<t.JSXOpeningElement>, state:
|
|
10
|
-
file: {
|
|
11
|
-
opts: {
|
|
12
|
-
filename?: string;
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
}): void;
|
|
16
|
+
JSXOpeningElement(path: NodePath<t.JSXOpeningElement>, state: BabelState): void;
|
|
16
17
|
};
|
|
17
18
|
};
|
|
18
19
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as t from '@babel/types';
|
|
2
|
+
const COLUMN_OFFSET = 1;
|
|
2
3
|
function hasSourceAttr(attributes, attribute) {
|
|
3
4
|
return attributes.some((attr) => t.isJSXAttribute(attr) && attr.name.name === attribute);
|
|
4
5
|
}
|
|
@@ -17,7 +18,8 @@ export function babelPluginAddSourceAttr(_, opts) {
|
|
|
17
18
|
return;
|
|
18
19
|
if (hasSourceAttr(path.node.attributes, attribute))
|
|
19
20
|
return;
|
|
20
|
-
|
|
21
|
+
// Babel columns are 0-indexed; editors expect 1-indexed columns.
|
|
22
|
+
const value = `${filename}:${loc.start.line}:${loc.start.column + COLUMN_OFFSET}`;
|
|
21
23
|
path.node.attributes.push(createSourceAttr(attribute, value));
|
|
22
24
|
},
|
|
23
25
|
},
|
package/dist/vite/editors.js
CHANGED
|
@@ -1,64 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import launch from 'launch-editor';
|
|
3
2
|
import { DEFAULT_IDE, isLocatorIde } from '../shared/index.js';
|
|
4
|
-
function createVscodeForkEditor(id, cliCandidates) {
|
|
5
|
-
return {
|
|
6
|
-
id,
|
|
7
|
-
cliCandidates,
|
|
8
|
-
openWithCli(cli, loc) {
|
|
9
|
-
execFileSync(cli, ['-r', '-g', `${loc.file}:${loc.line}:${loc.col}`], { stdio: 'ignore' });
|
|
10
|
-
},
|
|
11
|
-
openWithUrl(loc) {
|
|
12
|
-
execFileSync('open', [`${id}://file/${loc.file}:${loc.line}:${loc.col}`], { stdio: 'ignore' });
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
const EDITORS = [
|
|
17
|
-
createVscodeForkEditor('cursor', [
|
|
18
|
-
'/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
|
|
19
|
-
'cursor',
|
|
20
|
-
]),
|
|
21
|
-
createVscodeForkEditor('vscode', [
|
|
22
|
-
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
|
|
23
|
-
'code',
|
|
24
|
-
]),
|
|
25
|
-
{
|
|
26
|
-
id: 'webstorm',
|
|
27
|
-
cliCandidates: ['/Applications/WebStorm.app/Contents/MacOS/webstorm', 'webstorm'],
|
|
28
|
-
openWithCli(cli, loc) {
|
|
29
|
-
execFileSync(cli, ['--line', loc.line, loc.file], { stdio: 'ignore' });
|
|
30
|
-
},
|
|
31
|
-
openWithUrl(loc) {
|
|
32
|
-
const uri = `webstorm://open?file=${encodeURIComponent(loc.file)}&line=${loc.line}`;
|
|
33
|
-
execFileSync('open', [uri], { stdio: 'ignore' });
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
];
|
|
37
|
-
function canRun(command) {
|
|
38
|
-
try {
|
|
39
|
-
execFileSync('which', [command], { stdio: 'ignore' });
|
|
40
|
-
return true;
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
function resolveCli(candidates) {
|
|
47
|
-
const appPath = candidates.find((path) => path.includes('/') && existsSync(path));
|
|
48
|
-
if (appPath)
|
|
49
|
-
return appPath;
|
|
50
|
-
return candidates.filter((path) => !path.includes('/')).find((path) => canRun(path)) ?? null;
|
|
51
|
-
}
|
|
52
|
-
function findEditor(ide) {
|
|
53
|
-
return EDITORS.find((entry) => entry.id === ide) ?? EDITORS[0];
|
|
54
|
-
}
|
|
55
3
|
export function openInEditor(loc, ideParam) {
|
|
56
4
|
const ide = isLocatorIde(ideParam) ? ideParam : DEFAULT_IDE;
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
if (cli) {
|
|
60
|
-
editor.openWithCli(cli, loc);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
editor.openWithUrl(loc);
|
|
5
|
+
const spec = `${loc.file}:${loc.line}:${loc.col}`;
|
|
6
|
+
launch(spec, ide);
|
|
64
7
|
}
|
package/dist/vite/index.js
CHANGED
|
@@ -26,8 +26,9 @@ function readQuery(url) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
function resolveFilePath(file, root) {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const viteDevMatch = file.match(/^\/src\/(.+)$/);
|
|
30
|
+
if (viteDevMatch)
|
|
31
|
+
return resolve(root, 'src', viteDevMatch[1]);
|
|
31
32
|
if (isAbsolute(file))
|
|
32
33
|
return file;
|
|
33
34
|
return resolve(root, file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-source-locator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Dev-only Vite plugin: click UI elements to jump to source in your IDE",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,10 +25,11 @@
|
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc -p tsconfig.build.json",
|
|
28
|
-
"clean": "
|
|
28
|
+
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
29
|
+
"lint": "eslint .",
|
|
29
30
|
"test": "vitest run",
|
|
30
31
|
"test:watch": "vitest",
|
|
31
|
-
"prepublishOnly": "npm run build && npm run test"
|
|
32
|
+
"prepublishOnly": "npm run lint && npm run build && npm run test"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
35
|
"vite": "^5.0.0"
|
|
@@ -36,14 +37,19 @@
|
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"@babel/core": "^7.29.7",
|
|
38
39
|
"@babel/traverse": "^7.29.7",
|
|
39
|
-
"@babel/types": "^7.29.7"
|
|
40
|
+
"@babel/types": "^7.29.7",
|
|
41
|
+
"launch-editor": "^2.14.1"
|
|
40
42
|
},
|
|
41
43
|
"devDependencies": {
|
|
44
|
+
"@eslint/js": "^9.39.4",
|
|
42
45
|
"@types/babel__core": "^7.20.5",
|
|
43
46
|
"@types/babel__traverse": "^7.20.7",
|
|
44
47
|
"@types/node": "^20.12.12",
|
|
48
|
+
"eslint": "^9.39.4",
|
|
49
|
+
"globals": "^15.15.0",
|
|
45
50
|
"happy-dom": "^15.11.7",
|
|
46
51
|
"typescript": "^5.4.5",
|
|
52
|
+
"typescript-eslint": "^8.62.0",
|
|
47
53
|
"vite": "^5.2.11",
|
|
48
54
|
"vitest": "^2.1.8"
|
|
49
55
|
},
|