vite-plugin-source-locator 1.0.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -7
- package/dist/client/controller.js +26 -25
- 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.d.ts +2 -2
- package/dist/client/overlay.js +21 -80
- package/dist/client/preference.d.ts +1 -4
- package/dist/client/preference.js +5 -17
- 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.d.ts +2 -2
- package/dist/shared/index.js +20 -8
- 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.d.ts +2 -2
- package/dist/vite/editors.js +8 -61
- package/dist/vite/index.js +8 -7
- package/package.json +10 -4
package/README.md
CHANGED
|
@@ -42,11 +42,11 @@ No `main.tsx` wiring required. The plugin auto-injects the client overlay in dev
|
|
|
42
42
|
|
|
43
43
|
## Pick Mode
|
|
44
44
|
|
|
45
|
-
1. Click the badge (bottom-right): **Source Locator (
|
|
45
|
+
1. Click the badge (bottom-right): **Source Locator (auto) — click to pick**
|
|
46
46
|
2. Hover elements — blue highlight + file paths in tooltip
|
|
47
47
|
3. Click to open source — cycles **TSX → CSS → TSX** when both exist
|
|
48
48
|
4. **Esc** — cancel pick mode
|
|
49
|
-
5. **Shift+L** — cycle IDE
|
|
49
|
+
5. **Shift+L** — cycle IDE (session only, resets on refresh)
|
|
50
50
|
|
|
51
51
|
## Exports
|
|
52
52
|
|
|
@@ -63,7 +63,7 @@ sourceLocator({
|
|
|
63
63
|
enabled: true,
|
|
64
64
|
endpoint: '/__open-in-editor',
|
|
65
65
|
attribute: 'data-source',
|
|
66
|
-
ides: ['cursor', 'vscode', 'webstorm'],
|
|
66
|
+
ides: ['auto', 'cursor', 'vscode', 'webstorm'],
|
|
67
67
|
theme: 'light',
|
|
68
68
|
})
|
|
69
69
|
```
|
|
@@ -89,7 +89,7 @@ import { initSourceLocator } from 'vite-plugin-source-locator/client'
|
|
|
89
89
|
initSourceLocator({
|
|
90
90
|
endpoint: '/__open-in-editor',
|
|
91
91
|
attribute: 'data-source',
|
|
92
|
-
ides: ['cursor', 'vscode', 'webstorm'],
|
|
92
|
+
ides: ['auto', 'cursor', 'vscode', 'webstorm'],
|
|
93
93
|
theme: {
|
|
94
94
|
background: '#ffffff',
|
|
95
95
|
text: '#000000',
|
|
@@ -132,19 +132,60 @@ import { initSourceLocator } from 'vite-plugin-source-locator/client'
|
|
|
132
132
|
initSourceLocator({
|
|
133
133
|
endpoint: '/__open-in-editor',
|
|
134
134
|
attribute: 'data-source',
|
|
135
|
-
ides: ['cursor', 'vscode', 'webstorm'],
|
|
135
|
+
ides: ['auto', 'cursor', 'vscode', 'webstorm'],
|
|
136
136
|
theme: 'blue',
|
|
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
|
+
### Auto detection (default)
|
|
145
|
+
|
|
146
|
+
By default, `ides` includes `'auto'` as the first entry. In `auto` mode, the plugin detects your open IDE:
|
|
147
|
+
|
|
148
|
+
1. `LAUNCH_EDITOR` / `REACT_EDITOR` environment variable
|
|
149
|
+
2. Running editor process (VS Code, Cursor, WebStorm, etc.)
|
|
150
|
+
3. `VISUAL` / `EDITOR` environment variable
|
|
151
|
+
|
|
152
|
+
No CLI on `PATH` is required if the editor is already running.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// auto-detect open IDE (default)
|
|
156
|
+
sourceLocator()
|
|
157
|
+
|
|
158
|
+
// VS Code only
|
|
159
|
+
sourceLocator({ ides: ['vscode'] })
|
|
160
|
+
|
|
161
|
+
// auto + manual override with Shift+L (session only)
|
|
162
|
+
sourceLocator({ ides: ['auto', 'vscode'] })
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The first entry in `ides` is the default. Shift+L cycles through the list in memory (resets on page refresh).
|
|
166
|
+
|
|
167
|
+
### Explicit editor
|
|
168
|
+
|
|
169
|
+
When not using `auto`, the editor CLI should be on your `PATH`:
|
|
170
|
+
|
|
171
|
+
| IDE | CLI command |
|
|
172
|
+
|-----|-------------|
|
|
173
|
+
| Cursor | `cursor` |
|
|
174
|
+
| VS Code | `code` |
|
|
175
|
+
| WebStorm | `webstorm` |
|
|
176
|
+
|
|
177
|
+
Override with environment variables:
|
|
178
|
+
|
|
179
|
+
- `LAUNCH_EDITOR=code`
|
|
180
|
+
- `REACT_EDITOR=code`
|
|
181
|
+
|
|
140
182
|
## Adding a New IDE
|
|
141
183
|
|
|
142
184
|
1. Extend `LocatorIde` and `IDE_ORDER` in `src/shared/index.ts`
|
|
143
|
-
2.
|
|
185
|
+
2. Use a [launch-editor supported editor name](https://github.com/vitejs/launch-editor#supported-editors) as the new `LocatorIde` value
|
|
144
186
|
|
|
145
187
|
## Limitations
|
|
146
188
|
|
|
147
|
-
- macOS only for IDE opening
|
|
148
189
|
- Dev only — no production impact
|
|
149
190
|
- JSX/TSX only for `data-source` injection
|
|
150
191
|
- CSS line 1 only (no source-map line mapping yet)
|
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
import { nextClickTarget, parseSourceLocation, resolveTheme } from '../shared/index.js';
|
|
1
|
+
import { nextClickTarget, nextIde, parseSourceLocation, resolveTheme } from '../shared/index.js';
|
|
2
2
|
import { findCssSource } from './css-source.js';
|
|
3
3
|
import { createLocatorOverlayUi } from './overlay.js';
|
|
4
|
-
|
|
4
|
+
const EMPTY_SOURCES = Object.freeze({
|
|
5
|
+
tsx: undefined,
|
|
6
|
+
css: undefined,
|
|
7
|
+
clickTarget: 'tsx',
|
|
8
|
+
});
|
|
5
9
|
function getSourceEl(target, attribute, host) {
|
|
6
10
|
if (!target || host.contains(target) || target === host)
|
|
7
11
|
return null;
|
|
8
12
|
const el = target.closest(`[${attribute}]`);
|
|
9
13
|
return el instanceof HTMLElement ? el : null;
|
|
10
14
|
}
|
|
11
|
-
async function openSourceInEditor(source, config) {
|
|
15
|
+
async function openSourceInEditor(source, config, activeIde) {
|
|
12
16
|
const loc = parseSourceLocation(source);
|
|
13
17
|
const params = new URLSearchParams({
|
|
14
18
|
file: loc.file,
|
|
15
19
|
line: loc.line,
|
|
16
20
|
col: loc.col,
|
|
17
|
-
ide:
|
|
21
|
+
ide: activeIde,
|
|
18
22
|
});
|
|
19
23
|
await fetch(`${config.endpoint}?${params.toString()}`);
|
|
20
24
|
}
|
|
@@ -24,29 +28,30 @@ function readElementSources(el, attribute) {
|
|
|
24
28
|
return { tsx, css, clickTarget: 'tsx' };
|
|
25
29
|
}
|
|
26
30
|
function resolveOpenSource(sources) {
|
|
27
|
-
|
|
28
|
-
return sources.css;
|
|
29
|
-
return sources.tsx;
|
|
31
|
+
return sources[sources.clickTarget];
|
|
30
32
|
}
|
|
31
33
|
export function startPickController(root, host, config) {
|
|
32
34
|
let pickMode = false;
|
|
33
|
-
let sources =
|
|
34
|
-
|
|
35
|
+
let sources = EMPTY_SOURCES;
|
|
36
|
+
let activeIde = config.ides[0] ?? 'auto';
|
|
37
|
+
const ui = createLocatorOverlayUi(root, () => setPickMode(!pickMode), resolveTheme(config.theme), () => activeIde);
|
|
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) => {
|
|
@@ -63,10 +68,10 @@ export function startPickController(root, host, config) {
|
|
|
63
68
|
}
|
|
64
69
|
if (!e.shiftKey || e.key !== 'L')
|
|
65
70
|
return;
|
|
66
|
-
|
|
71
|
+
activeIde = nextIde(activeIde, config.ides);
|
|
67
72
|
if (!pickMode)
|
|
68
73
|
ui.refreshBadgeLabel();
|
|
69
|
-
ui.flashMessage(`IDE: ${
|
|
74
|
+
ui.flashMessage(`IDE: ${activeIde}`);
|
|
70
75
|
};
|
|
71
76
|
const onClick = async (e) => {
|
|
72
77
|
if (!pickMode || host.contains(e.target))
|
|
@@ -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
|
-
await openSourceInEditor(openSource, config);
|
|
89
|
-
sources
|
|
89
|
+
await openSourceInEditor(openSource, config, activeIde);
|
|
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.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ClickTarget, LocatorTheme } from '../shared/index.js';
|
|
2
|
-
export declare function createLocatorOverlayUi(root: ShadowRoot, onTogglePick: () => void, theme: LocatorTheme): {
|
|
1
|
+
import type { ClickTarget, LocatorIde, LocatorTheme } from '../shared/index.js';
|
|
2
|
+
export declare function createLocatorOverlayUi(root: ShadowRoot, onTogglePick: () => void, theme: LocatorTheme, getActiveIde: () => LocatorIde): {
|
|
3
3
|
mountBadge: () => void;
|
|
4
4
|
setPickActive: (active: boolean) => void;
|
|
5
5
|
refreshBadgeLabel: () => void;
|
package/dist/client/overlay.js
CHANGED
|
@@ -1,73 +1,12 @@
|
|
|
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
|
-
|
|
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
|
-
}
|
|
70
|
-
export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
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;
|
|
9
|
+
export function createLocatorOverlayUi(root, onTogglePick, theme, getActiveIde) {
|
|
71
10
|
let activeEl = null;
|
|
72
11
|
let flashTimeout = null;
|
|
73
12
|
let badgeEl = 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)
|
|
@@ -129,20 +70,20 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
|
129
70
|
document.body.style.cursor = active ? 'crosshair' : '';
|
|
130
71
|
if (!badgeEl)
|
|
131
72
|
return;
|
|
132
|
-
badgeEl.textContent = badgeLabel(active);
|
|
73
|
+
badgeEl.textContent = badgeLabel(active, getActiveIde());
|
|
133
74
|
applyBadgeColors(active);
|
|
134
75
|
if (!active)
|
|
135
76
|
removeTooltip();
|
|
136
77
|
};
|
|
137
78
|
const refreshBadgeLabel = () => {
|
|
138
79
|
if (badgeEl)
|
|
139
|
-
badgeEl.textContent = badgeLabel(false);
|
|
80
|
+
badgeEl.textContent = badgeLabel(false, getActiveIde());
|
|
140
81
|
};
|
|
141
82
|
const mountBadge = () => {
|
|
142
83
|
badgeEl = document.createElement('button');
|
|
143
84
|
badgeEl.id = UI_IDS.badge;
|
|
144
85
|
badgeEl.type = 'button';
|
|
145
|
-
badgeEl.textContent = badgeLabel(false);
|
|
86
|
+
badgeEl.textContent = badgeLabel(false, getActiveIde());
|
|
146
87
|
Object.assign(badgeEl.style, LAYOUT.badge, {
|
|
147
88
|
background: theme.badgeBackground,
|
|
148
89
|
color: theme.badgeText,
|
|
@@ -1,5 +1,2 @@
|
|
|
1
1
|
import type { LocatorIde } from '../shared/index.js';
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function setStoredIde(ide: LocatorIde): void;
|
|
4
|
-
export declare function cycleStoredIde(order: LocatorIde[]): LocatorIde;
|
|
5
|
-
export declare function badgeLabel(picking: boolean): string;
|
|
2
|
+
export declare function badgeLabel(picking: boolean, activeIde: LocatorIde): string;
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
return isLocatorIde(stored) ? stored : DEFAULT_IDE;
|
|
5
|
-
}
|
|
6
|
-
export function setStoredIde(ide) {
|
|
7
|
-
localStorage.setItem(STORAGE_KEY, ide);
|
|
8
|
-
}
|
|
9
|
-
export function cycleStoredIde(order) {
|
|
10
|
-
const next = nextIde(getStoredIde(), order);
|
|
11
|
-
setStoredIde(next);
|
|
12
|
-
return next;
|
|
13
|
-
}
|
|
14
|
-
export function badgeLabel(picking) {
|
|
15
|
-
const ide = getStoredIde();
|
|
1
|
+
const BADGE_PICKING_HINT = 'Esc to cancel';
|
|
2
|
+
const BADGE_IDLE_HINT = 'click to pick';
|
|
3
|
+
export function badgeLabel(picking, activeIde) {
|
|
16
4
|
if (picking)
|
|
17
|
-
return `Pick element (${
|
|
18
|
-
return `Source Locator (${
|
|
5
|
+
return `Pick element (${activeIde}) — ${BADGE_PICKING_HINT}`;
|
|
6
|
+
return `Source Locator (${activeIde}) — ${BADGE_IDLE_HINT}`;
|
|
19
7
|
}
|
|
@@ -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.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { LocatorTheme, LocatorThemeInput, LocatorThemeOverride, LocatorThemePreset, } from './theme.js';
|
|
2
2
|
export { resolveTheme } from './theme.js';
|
|
3
|
-
export type LocatorIde = 'cursor' | 'vscode' | 'webstorm';
|
|
3
|
+
export type LocatorIde = 'auto' | 'cursor' | 'vscode' | 'webstorm';
|
|
4
4
|
export type SourceLocation = {
|
|
5
5
|
file: string;
|
|
6
6
|
line: string;
|
|
@@ -9,10 +9,10 @@ export type SourceLocation = {
|
|
|
9
9
|
export type ClickTarget = 'tsx' | 'css';
|
|
10
10
|
export declare const SOURCE_ATTR = "data-source";
|
|
11
11
|
export declare const OPEN_ENDPOINT = "/__open-in-editor";
|
|
12
|
-
export declare const STORAGE_KEY = "locator-ide";
|
|
13
12
|
export declare const DEFAULT_IDE: LocatorIde;
|
|
14
13
|
export declare const IDE_ORDER: LocatorIde[];
|
|
15
14
|
export declare function isLocatorIde(value: string): value is LocatorIde;
|
|
15
|
+
export declare function resolveIde(value: string, allowed: LocatorIde[]): LocatorIde;
|
|
16
16
|
export declare function nextIde(current: LocatorIde, order?: LocatorIde[]): LocatorIde;
|
|
17
17
|
export declare function parseSourceLocation(raw: string): SourceLocation;
|
|
18
18
|
export declare function formatSourceLocation(loc: SourceLocation): string;
|
package/dist/shared/index.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
export { resolveTheme } from './theme.js';
|
|
2
2
|
export const SOURCE_ATTR = 'data-source';
|
|
3
3
|
export const OPEN_ENDPOINT = '/__open-in-editor';
|
|
4
|
-
export const
|
|
5
|
-
export const
|
|
6
|
-
export const IDE_ORDER = ['cursor', 'vscode', 'webstorm'];
|
|
4
|
+
export const DEFAULT_IDE = 'auto';
|
|
5
|
+
export const IDE_ORDER = ['auto', 'cursor', 'vscode', 'webstorm'];
|
|
7
6
|
export function isLocatorIde(value) {
|
|
8
7
|
return IDE_ORDER.includes(value);
|
|
9
8
|
}
|
|
9
|
+
export function resolveIde(value, allowed) {
|
|
10
|
+
const fallback = allowed[0] ?? DEFAULT_IDE;
|
|
11
|
+
if (!isLocatorIde(value))
|
|
12
|
+
return fallback;
|
|
13
|
+
if (!allowed.includes(value))
|
|
14
|
+
return fallback;
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
10
17
|
export function nextIde(current, order = IDE_ORDER) {
|
|
11
18
|
const index = order.indexOf(current);
|
|
12
19
|
const nextIndex = (index + 1) % order.length;
|
|
@@ -14,16 +21,21 @@ export function nextIde(current, order = IDE_ORDER) {
|
|
|
14
21
|
}
|
|
15
22
|
export function parseSourceLocation(raw) {
|
|
16
23
|
const parts = raw.split(':');
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
24
|
+
if (parts.length < 3)
|
|
25
|
+
return { file: raw, line: '1', col: '1' };
|
|
26
|
+
const col = parts.pop() ?? '1';
|
|
27
|
+
const line = parts.pop() ?? '1';
|
|
28
|
+
return { file: parts.join(':'), line, col };
|
|
21
29
|
}
|
|
22
30
|
export function formatSourceLocation(loc) {
|
|
23
31
|
return `${loc.file}:${loc.line}:${loc.col}`;
|
|
24
32
|
}
|
|
33
|
+
const CLICK_TARGET_TRANSITION = {
|
|
34
|
+
tsx: 'css',
|
|
35
|
+
css: 'tsx',
|
|
36
|
+
};
|
|
25
37
|
export function nextClickTarget(current, hasCss) {
|
|
26
38
|
if (!hasCss)
|
|
27
39
|
return 'tsx';
|
|
28
|
-
return current
|
|
40
|
+
return CLICK_TARGET_TRANSITION[current];
|
|
29
41
|
}
|
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.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { SourceLocation } from '../shared/index.js';
|
|
2
|
-
export declare function openInEditor(loc: SourceLocation, ideParam: string): void;
|
|
1
|
+
import type { LocatorIde, SourceLocation } from '../shared/index.js';
|
|
2
|
+
export declare function openInEditor(loc: SourceLocation, ideParam: string, allowed: LocatorIde[]): void;
|
package/dist/vite/editors.js
CHANGED
|
@@ -1,64 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
export function openInEditor(loc, ideParam) {
|
|
56
|
-
const ide = isLocatorIde(ideParam) ? ideParam : DEFAULT_IDE;
|
|
57
|
-
const editor = findEditor(ide);
|
|
58
|
-
const cli = resolveCli(editor.cliCandidates);
|
|
59
|
-
if (cli) {
|
|
60
|
-
editor.openWithCli(cli, loc);
|
|
1
|
+
import launch from 'launch-editor';
|
|
2
|
+
import { resolveIde } from '../shared/index.js';
|
|
3
|
+
export function openInEditor(loc, ideParam, allowed) {
|
|
4
|
+
const ide = resolveIde(ideParam, allowed);
|
|
5
|
+
const spec = `${loc.file}:${loc.line}:${loc.col}`;
|
|
6
|
+
if (ide === 'auto') {
|
|
7
|
+
launch(spec);
|
|
61
8
|
return;
|
|
62
9
|
}
|
|
63
|
-
|
|
10
|
+
launch(spec, ide);
|
|
64
11
|
}
|
package/dist/vite/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { isAbsolute, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import {
|
|
4
|
+
import { IDE_ORDER, OPEN_ENDPOINT, SOURCE_ATTR } from '../shared/index.js';
|
|
5
5
|
import { babelPluginAddSourceAttr } from './babel-plugin.js';
|
|
6
6
|
import { openInEditor } from './editors.js';
|
|
7
7
|
const VIRTUAL_CLIENT_ID = 'virtual:source-locator-client';
|
|
@@ -16,18 +16,19 @@ function resolveOptions(options = {}) {
|
|
|
16
16
|
theme: options.theme,
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
-
function readQuery(url) {
|
|
19
|
+
function readQuery(url, defaultIde) {
|
|
20
20
|
const parsed = new URL(url, 'http://localhost');
|
|
21
21
|
return {
|
|
22
22
|
file: parsed.searchParams.get('file'),
|
|
23
23
|
line: parsed.searchParams.get('line') ?? '1',
|
|
24
24
|
col: parsed.searchParams.get('col') ?? '1',
|
|
25
|
-
ide: parsed.searchParams.get('ide') ??
|
|
25
|
+
ide: parsed.searchParams.get('ide') ?? defaultIde,
|
|
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);
|
|
@@ -55,7 +56,7 @@ function sourceLocator(options = {}) {
|
|
|
55
56
|
},
|
|
56
57
|
configureServer(server) {
|
|
57
58
|
server.middlewares.use(config.endpoint, (req, res) => {
|
|
58
|
-
const { file, line, col, ide } = readQuery(req.url ?? '');
|
|
59
|
+
const { file, line, col, ide } = readQuery(req.url ?? '', config.ides[0] ?? 'auto');
|
|
59
60
|
if (!file) {
|
|
60
61
|
res.writeHead(400);
|
|
61
62
|
res.end('missing file');
|
|
@@ -68,7 +69,7 @@ function sourceLocator(options = {}) {
|
|
|
68
69
|
res.end('file not found');
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
|
-
openInEditor({ file: resolvedFile, line, col }, ide);
|
|
72
|
+
openInEditor({ file: resolvedFile, line, col }, ide, config.ides);
|
|
72
73
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
73
74
|
res.end('ok');
|
|
74
75
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-source-locator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
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
|
},
|