vite-plugin-source-locator 1.0.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 +154 -0
- package/dist/client/controller.d.ts +8 -0
- package/dist/client/controller.js +101 -0
- package/dist/client/css-source.d.ts +1 -0
- package/dist/client/css-source.js +57 -0
- package/dist/client/index.d.ts +10 -0
- package/dist/client/index.js +21 -0
- package/dist/client/overlay.d.ts +11 -0
- package/dist/client/overlay.js +166 -0
- package/dist/client/preference.d.ts +5 -0
- package/dist/client/preference.js +19 -0
- package/dist/shared/index.d.ts +19 -0
- package/dist/shared/index.js +29 -0
- package/dist/shared/theme.d.ts +21 -0
- package/dist/shared/theme.js +42 -0
- package/dist/vite/babel-plugin.d.ts +18 -0
- package/dist/vite/babel-plugin.js +25 -0
- package/dist/vite/editors.d.ts +2 -0
- package/dist/vite/editors.js +64 -0
- package/dist/vite/index.d.ts +21 -0
- package/dist/vite/index.js +106 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amir Benshimol
|
|
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,154 @@
|
|
|
1
|
+
# vite-plugin-source-locator
|
|
2
|
+
|
|
3
|
+
Dev-only tool for jumping from UI elements in the browser to source files in your IDE. Works as a drop-in Vite plugin for React apps.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
├── src/
|
|
9
|
+
│ ├── vite/ # Vite plugin, Babel plugin, editor integration
|
|
10
|
+
│ ├── client/ # Browser overlay (pick mode, tooltip, highlight)
|
|
11
|
+
│ └── shared/ # Types, constants, theme utilities
|
|
12
|
+
├── tests/ # Mirrors src/ layout
|
|
13
|
+
├── dist/ # Build output (published to npm)
|
|
14
|
+
└── .github/ # CI workflows
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -D vite-plugin-source-locator
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You also need `@vitejs/plugin-react` (or another setup that runs the Babel plugin in dev).
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// vite.config.ts
|
|
29
|
+
import { defineConfig } from 'vite'
|
|
30
|
+
import react from '@vitejs/plugin-react'
|
|
31
|
+
import { sourceLocator } from 'vite-plugin-source-locator/vite'
|
|
32
|
+
|
|
33
|
+
export default defineConfig(({ command }) => ({
|
|
34
|
+
plugins: [
|
|
35
|
+
react(command === 'serve' ? sourceLocator.babel() : undefined),
|
|
36
|
+
sourceLocator(),
|
|
37
|
+
],
|
|
38
|
+
}))
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
No `main.tsx` wiring required. The plugin auto-injects the client overlay in dev.
|
|
42
|
+
|
|
43
|
+
## Pick Mode
|
|
44
|
+
|
|
45
|
+
1. Click the badge (bottom-right): **Source Locator (cursor) — click to pick**
|
|
46
|
+
2. Hover elements — blue highlight + file paths in tooltip
|
|
47
|
+
3. Click to open source — cycles **TSX → CSS → TSX** when both exist
|
|
48
|
+
4. **Esc** — cancel pick mode
|
|
49
|
+
5. **Shift+L** — cycle IDE: cursor → vscode → webstorm
|
|
50
|
+
|
|
51
|
+
## Exports
|
|
52
|
+
|
|
53
|
+
| Subpath | Purpose |
|
|
54
|
+
|---------|---------|
|
|
55
|
+
| `vite-plugin-source-locator/vite` | Vite plugin + `sourceLocator.babel()` |
|
|
56
|
+
| `vite-plugin-source-locator/client` | Manual `initSourceLocator()` if auto-inject disabled |
|
|
57
|
+
| `vite-plugin-source-locator/shared` | Types, constants, parse/format utilities |
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
sourceLocator({
|
|
63
|
+
enabled: true,
|
|
64
|
+
endpoint: '/__open-in-editor',
|
|
65
|
+
attribute: 'data-source',
|
|
66
|
+
ides: ['cursor', 'vscode', 'webstorm'],
|
|
67
|
+
theme: 'light',
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Theme
|
|
72
|
+
|
|
73
|
+
Control overlay colors (badge, tooltip, highlight). Presets or custom colors:
|
|
74
|
+
|
|
75
|
+
| Preset | Look |
|
|
76
|
+
|--------|------|
|
|
77
|
+
| `'default'` | Dark slate + cyan accent |
|
|
78
|
+
| `'light'` | White background + blue accent |
|
|
79
|
+
| `'dark'` | Black background + white/gray text |
|
|
80
|
+
| `'blue'` | Navy background + light blue accent |
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// preset
|
|
84
|
+
sourceLocator({ theme: 'light' })
|
|
85
|
+
|
|
86
|
+
// custom (merged over default)
|
|
87
|
+
import { initSourceLocator } from 'vite-plugin-source-locator/client'
|
|
88
|
+
|
|
89
|
+
initSourceLocator({
|
|
90
|
+
endpoint: '/__open-in-editor',
|
|
91
|
+
attribute: 'data-source',
|
|
92
|
+
ides: ['cursor', 'vscode', 'webstorm'],
|
|
93
|
+
theme: {
|
|
94
|
+
background: '#ffffff',
|
|
95
|
+
text: '#000000',
|
|
96
|
+
accent: '#2563eb',
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Token | Used for |
|
|
102
|
+
|-------|----------|
|
|
103
|
+
| `background` | Badge & tooltip background |
|
|
104
|
+
| `text` | Tooltip text |
|
|
105
|
+
| `accent` | Borders, highlight, badge label |
|
|
106
|
+
|
|
107
|
+
## CSS Detection
|
|
108
|
+
|
|
109
|
+
When an element has styles from a project `.css` file (e.g. `index.css` custom classes like `.glass`), the tooltip shows:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
components/DashboardStats.tsx
|
|
113
|
+
CSS: index.css
|
|
114
|
+
Click → TSX | next: CSS
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Click cycles between opening the TSX and CSS file.
|
|
118
|
+
|
|
119
|
+
**Limitation:** Tailwind utility classes (`bg-slate-950`, etc.) are injected as inline `<style>` tags with no `.css` href — no CSS file is shown for those. CSS detection works for external `.css` stylesheets only. CSS opens at line 1 in v1.
|
|
120
|
+
|
|
121
|
+
## MFE Safety
|
|
122
|
+
|
|
123
|
+
- **Idempotent** — `window.__sourceLocator` guard: multiple bundles loading the locator mount only one overlay
|
|
124
|
+
- **Shadow DOM** — overlay styles isolated from host/MFE CSS
|
|
125
|
+
- **Virtual module** — client injected via `virtual:source-locator-client`, works from any consuming app
|
|
126
|
+
|
|
127
|
+
## Manual Init
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { initSourceLocator } from 'vite-plugin-source-locator/client'
|
|
131
|
+
|
|
132
|
+
initSourceLocator({
|
|
133
|
+
endpoint: '/__open-in-editor',
|
|
134
|
+
attribute: 'data-source',
|
|
135
|
+
ides: ['cursor', 'vscode', 'webstorm'],
|
|
136
|
+
theme: 'blue',
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Adding a New IDE
|
|
141
|
+
|
|
142
|
+
1. Extend `LocatorIde` and `IDE_ORDER` in `src/shared/index.ts`
|
|
143
|
+
2. Add entry to `EDITORS` in `src/vite/editors.ts`
|
|
144
|
+
|
|
145
|
+
## Limitations
|
|
146
|
+
|
|
147
|
+
- macOS only for IDE opening
|
|
148
|
+
- Dev only — no production impact
|
|
149
|
+
- JSX/TSX only for `data-source` injection
|
|
150
|
+
- CSS line 1 only (no source-map line mapping yet)
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LocatorIde, LocatorThemeInput } from '../shared/index.js';
|
|
2
|
+
export type ClientConfig = {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
attribute: string;
|
|
5
|
+
ides: LocatorIde[];
|
|
6
|
+
theme?: LocatorThemeInput;
|
|
7
|
+
};
|
|
8
|
+
export declare function startPickController(root: ShadowRoot, host: Element, config: ClientConfig): () => void;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { nextClickTarget, parseSourceLocation, resolveTheme } from '../shared/index.js';
|
|
2
|
+
import { findCssSource } from './css-source.js';
|
|
3
|
+
import { createLocatorOverlayUi } from './overlay.js';
|
|
4
|
+
import { cycleStoredIde, getStoredIde } from './preference.js';
|
|
5
|
+
function getSourceEl(target, attribute, host) {
|
|
6
|
+
if (!target || host.contains(target) || target === host)
|
|
7
|
+
return null;
|
|
8
|
+
const el = target.closest(`[${attribute}]`);
|
|
9
|
+
return el instanceof HTMLElement ? el : null;
|
|
10
|
+
}
|
|
11
|
+
async function openSourceInEditor(source, config) {
|
|
12
|
+
const loc = parseSourceLocation(source);
|
|
13
|
+
const params = new URLSearchParams({
|
|
14
|
+
file: loc.file,
|
|
15
|
+
line: loc.line,
|
|
16
|
+
col: loc.col,
|
|
17
|
+
ide: getStoredIde(),
|
|
18
|
+
});
|
|
19
|
+
await fetch(`${config.endpoint}?${params.toString()}`);
|
|
20
|
+
}
|
|
21
|
+
function readElementSources(el, attribute) {
|
|
22
|
+
const tsx = el.getAttribute(attribute) ?? undefined;
|
|
23
|
+
const css = findCssSource(el);
|
|
24
|
+
return { tsx, css, clickTarget: 'tsx' };
|
|
25
|
+
}
|
|
26
|
+
function resolveOpenSource(sources) {
|
|
27
|
+
if (sources.clickTarget === 'css')
|
|
28
|
+
return sources.css;
|
|
29
|
+
return sources.tsx;
|
|
30
|
+
}
|
|
31
|
+
export function startPickController(root, host, config) {
|
|
32
|
+
let pickMode = false;
|
|
33
|
+
let sources = { tsx: undefined, css: undefined, clickTarget: 'tsx' };
|
|
34
|
+
const ui = createLocatorOverlayUi(root, () => setPickMode(!pickMode), resolveTheme(config.theme));
|
|
35
|
+
function setPickMode(active) {
|
|
36
|
+
pickMode = active;
|
|
37
|
+
ui.setPickActive(active);
|
|
38
|
+
if (!active)
|
|
39
|
+
sources = { tsx: undefined, css: undefined, clickTarget: 'tsx' };
|
|
40
|
+
}
|
|
41
|
+
const updateHover = (target, x, y) => {
|
|
42
|
+
const el = getSourceEl(target, config.attribute, host);
|
|
43
|
+
if (!el) {
|
|
44
|
+
ui.removeTooltip();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (el !== ui.getActiveEl()) {
|
|
48
|
+
sources = readElementSources(el, config.attribute);
|
|
49
|
+
}
|
|
50
|
+
ui.showSourceTooltip(el, sources.tsx, sources.css, sources.clickTarget, x, y);
|
|
51
|
+
};
|
|
52
|
+
const onMouseMove = (e) => {
|
|
53
|
+
if (!pickMode) {
|
|
54
|
+
ui.removeTooltip();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
updateHover(e.target, e.clientX, e.clientY);
|
|
58
|
+
};
|
|
59
|
+
const onKeyDown = (e) => {
|
|
60
|
+
if (e.key === 'Escape' && pickMode) {
|
|
61
|
+
setPickMode(false);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!e.shiftKey || e.key !== 'L')
|
|
65
|
+
return;
|
|
66
|
+
const next = cycleStoredIde(config.ides);
|
|
67
|
+
if (!pickMode)
|
|
68
|
+
ui.refreshBadgeLabel();
|
|
69
|
+
ui.flashMessage(`IDE: ${next}`);
|
|
70
|
+
};
|
|
71
|
+
const onClick = async (e) => {
|
|
72
|
+
if (!pickMode || host.contains(e.target))
|
|
73
|
+
return;
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
const el = getSourceEl(e.target, config.attribute, host);
|
|
77
|
+
if (!el) {
|
|
78
|
+
ui.flashMessage('No source for this element');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (el !== ui.getActiveEl())
|
|
82
|
+
sources = readElementSources(el, config.attribute);
|
|
83
|
+
const openSource = resolveOpenSource(sources);
|
|
84
|
+
if (!openSource) {
|
|
85
|
+
ui.flashMessage('No source for this element');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await openSourceInEditor(openSource, config);
|
|
89
|
+
sources.clickTarget = nextClickTarget(sources.clickTarget, !!sources.css);
|
|
90
|
+
ui.showSourceTooltip(el, sources.tsx, sources.css, sources.clickTarget, e.clientX, e.clientY);
|
|
91
|
+
};
|
|
92
|
+
document.addEventListener('keydown', onKeyDown);
|
|
93
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
94
|
+
document.addEventListener('click', onClick, true);
|
|
95
|
+
ui.mountBadge();
|
|
96
|
+
return () => {
|
|
97
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
98
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
99
|
+
document.removeEventListener('click', onClick, true);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findCssSource(element: Element): string | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { formatSourceLocation } from '../shared/index.js';
|
|
2
|
+
function scoreSelector(selector, element) {
|
|
3
|
+
let score = selector.length;
|
|
4
|
+
if (selector.includes('#'))
|
|
5
|
+
score += 100;
|
|
6
|
+
if (selector.includes('.'))
|
|
7
|
+
score += 50;
|
|
8
|
+
if (element instanceof HTMLElement) {
|
|
9
|
+
element.classList.forEach((cls) => {
|
|
10
|
+
if (selector.includes(`.${cls}`))
|
|
11
|
+
score += 200;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return score;
|
|
15
|
+
}
|
|
16
|
+
function isProjectStylesheet(href) {
|
|
17
|
+
return href.includes('.css') && (href.includes('localhost') || href.includes('/src/'));
|
|
18
|
+
}
|
|
19
|
+
function hrefToFile(href) {
|
|
20
|
+
const url = new URL(href, window.location.origin);
|
|
21
|
+
return decodeURIComponent(url.pathname);
|
|
22
|
+
}
|
|
23
|
+
export function findCssSource(element) {
|
|
24
|
+
let bestScore = -1;
|
|
25
|
+
let bestFile;
|
|
26
|
+
Array.from(document.styleSheets).forEach((sheet) => {
|
|
27
|
+
const href = sheet.href ?? '';
|
|
28
|
+
if (!href || !isProjectStylesheet(href))
|
|
29
|
+
return;
|
|
30
|
+
let rules;
|
|
31
|
+
try {
|
|
32
|
+
rules = sheet.cssRules;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
Array.from(rules).forEach((rule) => {
|
|
38
|
+
if (!(rule instanceof CSSStyleRule))
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
if (!element.matches(rule.selectorText))
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const score = scoreSelector(rule.selectorText, element);
|
|
48
|
+
if (score <= bestScore)
|
|
49
|
+
return;
|
|
50
|
+
bestScore = score;
|
|
51
|
+
bestFile = hrefToFile(href);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
if (!bestFile)
|
|
55
|
+
return undefined;
|
|
56
|
+
return formatSourceLocation({ file: bestFile, line: '1', col: '1' });
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ClientConfig } from './controller.js';
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
__sourceLocator?: {
|
|
5
|
+
dispose: () => void;
|
|
6
|
+
};
|
|
7
|
+
__SOURCE_LOCATOR_CONFIG__?: ClientConfig;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export declare function initSourceLocator(config: ClientConfig): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { startPickController } from './controller.js';
|
|
2
|
+
const HOST_ID = 'source-locator-host';
|
|
3
|
+
export function initSourceLocator(config) {
|
|
4
|
+
if (window.__sourceLocator || document.getElementById(HOST_ID))
|
|
5
|
+
return;
|
|
6
|
+
const host = document.createElement('div');
|
|
7
|
+
host.id = HOST_ID;
|
|
8
|
+
const root = host.attachShadow({ mode: 'open' });
|
|
9
|
+
document.body.appendChild(host);
|
|
10
|
+
const disposeController = startPickController(root, host, config);
|
|
11
|
+
window.__sourceLocator = {
|
|
12
|
+
dispose: () => {
|
|
13
|
+
disposeController();
|
|
14
|
+
host.remove();
|
|
15
|
+
delete window.__sourceLocator;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const injected = window.__SOURCE_LOCATOR_CONFIG__;
|
|
20
|
+
if (injected)
|
|
21
|
+
initSourceLocator(injected);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ClickTarget, LocatorTheme } from '../shared/index.js';
|
|
2
|
+
export declare function createLocatorOverlayUi(root: ShadowRoot, onTogglePick: () => void, theme: LocatorTheme): {
|
|
3
|
+
mountBadge: () => void;
|
|
4
|
+
setPickActive: (active: boolean) => void;
|
|
5
|
+
refreshBadgeLabel: () => void;
|
|
6
|
+
showSourceTooltip: (el: Element, tsxSource: string | undefined, cssSource: string | undefined, clickTarget: ClickTarget, x: number, y: number) => void;
|
|
7
|
+
flashMessage: (text: string) => void;
|
|
8
|
+
removeTooltip: () => void;
|
|
9
|
+
getActiveEl: () => Element | null;
|
|
10
|
+
};
|
|
11
|
+
export type LocatorOverlayUi = ReturnType<typeof createLocatorOverlayUi>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { parseSourceLocation } from '../shared/index.js';
|
|
2
|
+
import { badgeLabel } from './preference.js';
|
|
3
|
+
const UI_IDS = {
|
|
4
|
+
badge: 'source-locator-badge',
|
|
5
|
+
tooltip: 'source-locator-tooltip',
|
|
6
|
+
highlight: 'source-locator-highlight',
|
|
7
|
+
};
|
|
8
|
+
const LAYOUT = {
|
|
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
|
+
}
|
|
70
|
+
export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
71
|
+
let activeEl = null;
|
|
72
|
+
let flashTimeout = null;
|
|
73
|
+
let badgeEl = null;
|
|
74
|
+
const removeHighlight = () => root.getElementById(UI_IDS.highlight)?.remove();
|
|
75
|
+
const removeTooltip = () => {
|
|
76
|
+
root.getElementById(UI_IDS.tooltip)?.remove();
|
|
77
|
+
activeEl = null;
|
|
78
|
+
removeHighlight();
|
|
79
|
+
};
|
|
80
|
+
const showHighlight = (el) => {
|
|
81
|
+
removeHighlight();
|
|
82
|
+
const rect = el.getBoundingClientRect();
|
|
83
|
+
const highlight = document.createElement('div');
|
|
84
|
+
highlight.id = UI_IDS.highlight;
|
|
85
|
+
Object.assign(highlight.style, LAYOUT.highlight, {
|
|
86
|
+
top: `${rect.top - 2}px`,
|
|
87
|
+
left: `${rect.left - 2}px`,
|
|
88
|
+
width: `${rect.width + 4}px`,
|
|
89
|
+
height: `${rect.height + 4}px`,
|
|
90
|
+
borderColor: theme.highlightBorder,
|
|
91
|
+
background: theme.highlightBackground,
|
|
92
|
+
boxShadow: `0 0 0 1px ${theme.highlightShadow}`,
|
|
93
|
+
});
|
|
94
|
+
root.appendChild(highlight);
|
|
95
|
+
};
|
|
96
|
+
const showTooltip = (text, el, x, y) => {
|
|
97
|
+
removeTooltip();
|
|
98
|
+
const tooltip = document.createElement('div');
|
|
99
|
+
tooltip.id = UI_IDS.tooltip;
|
|
100
|
+
tooltip.textContent = text;
|
|
101
|
+
Object.assign(tooltip.style, LAYOUT.tooltip, {
|
|
102
|
+
top: `${y + 16}px`,
|
|
103
|
+
left: `${x + 16}px`,
|
|
104
|
+
background: theme.tooltipBackground,
|
|
105
|
+
color: theme.tooltipText,
|
|
106
|
+
border: `1px solid ${theme.tooltipBorder}`,
|
|
107
|
+
});
|
|
108
|
+
root.appendChild(tooltip);
|
|
109
|
+
activeEl = el;
|
|
110
|
+
if (el)
|
|
111
|
+
showHighlight(el);
|
|
112
|
+
};
|
|
113
|
+
const showSourceTooltip = (el, tsxSource, cssSource, clickTarget, x, y) => {
|
|
114
|
+
showTooltip(buildTooltipText(tsxSource, cssSource, clickTarget), el, x, y);
|
|
115
|
+
};
|
|
116
|
+
const flashMessage = (text) => {
|
|
117
|
+
if (flashTimeout)
|
|
118
|
+
clearTimeout(flashTimeout);
|
|
119
|
+
showTooltip(text, null, window.innerWidth / 2 - 80, window.innerHeight - 80);
|
|
120
|
+
flashTimeout = setTimeout(removeTooltip, 1500);
|
|
121
|
+
};
|
|
122
|
+
const applyBadgeColors = (active) => {
|
|
123
|
+
if (!badgeEl)
|
|
124
|
+
return;
|
|
125
|
+
badgeEl.style.background = active ? theme.badgeActiveBackground : theme.badgeBackground;
|
|
126
|
+
badgeEl.style.color = active ? theme.badgeActiveText : theme.badgeText;
|
|
127
|
+
};
|
|
128
|
+
const setPickActive = (active) => {
|
|
129
|
+
document.body.style.cursor = active ? 'crosshair' : '';
|
|
130
|
+
if (!badgeEl)
|
|
131
|
+
return;
|
|
132
|
+
badgeEl.textContent = badgeLabel(active);
|
|
133
|
+
applyBadgeColors(active);
|
|
134
|
+
if (!active)
|
|
135
|
+
removeTooltip();
|
|
136
|
+
};
|
|
137
|
+
const refreshBadgeLabel = () => {
|
|
138
|
+
if (badgeEl)
|
|
139
|
+
badgeEl.textContent = badgeLabel(false);
|
|
140
|
+
};
|
|
141
|
+
const mountBadge = () => {
|
|
142
|
+
badgeEl = document.createElement('button');
|
|
143
|
+
badgeEl.id = UI_IDS.badge;
|
|
144
|
+
badgeEl.type = 'button';
|
|
145
|
+
badgeEl.textContent = badgeLabel(false);
|
|
146
|
+
Object.assign(badgeEl.style, LAYOUT.badge, {
|
|
147
|
+
background: theme.badgeBackground,
|
|
148
|
+
color: theme.badgeText,
|
|
149
|
+
border: `1px solid ${theme.badgeBorder}`,
|
|
150
|
+
});
|
|
151
|
+
badgeEl.addEventListener('click', (event) => {
|
|
152
|
+
event.stopPropagation();
|
|
153
|
+
onTogglePick();
|
|
154
|
+
});
|
|
155
|
+
root.appendChild(badgeEl);
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
mountBadge,
|
|
159
|
+
setPickActive,
|
|
160
|
+
refreshBadgeLabel,
|
|
161
|
+
showSourceTooltip,
|
|
162
|
+
flashMessage,
|
|
163
|
+
removeTooltip,
|
|
164
|
+
getActiveEl: () => activeEl,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LocatorIde } from '../shared/index.js';
|
|
2
|
+
export declare function getStoredIde(): LocatorIde;
|
|
3
|
+
export declare function setStoredIde(ide: LocatorIde): void;
|
|
4
|
+
export declare function cycleStoredIde(order: LocatorIde[]): LocatorIde;
|
|
5
|
+
export declare function badgeLabel(picking: boolean): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DEFAULT_IDE, STORAGE_KEY, isLocatorIde, nextIde } from '../shared/index.js';
|
|
2
|
+
export function getStoredIde() {
|
|
3
|
+
const stored = localStorage.getItem(STORAGE_KEY) ?? DEFAULT_IDE;
|
|
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();
|
|
16
|
+
if (picking)
|
|
17
|
+
return `Pick element (${ide}) — Esc to cancel`;
|
|
18
|
+
return `Source Locator (${ide}) — click to pick`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type { LocatorTheme, LocatorThemeInput, LocatorThemeOverride, LocatorThemePreset, } from './theme.js';
|
|
2
|
+
export { resolveTheme } from './theme.js';
|
|
3
|
+
export type LocatorIde = 'cursor' | 'vscode' | 'webstorm';
|
|
4
|
+
export type SourceLocation = {
|
|
5
|
+
file: string;
|
|
6
|
+
line: string;
|
|
7
|
+
col: string;
|
|
8
|
+
};
|
|
9
|
+
export type ClickTarget = 'tsx' | 'css';
|
|
10
|
+
export declare const SOURCE_ATTR = "data-source";
|
|
11
|
+
export declare const OPEN_ENDPOINT = "/__open-in-editor";
|
|
12
|
+
export declare const STORAGE_KEY = "locator-ide";
|
|
13
|
+
export declare const DEFAULT_IDE: LocatorIde;
|
|
14
|
+
export declare const IDE_ORDER: LocatorIde[];
|
|
15
|
+
export declare function isLocatorIde(value: string): value is LocatorIde;
|
|
16
|
+
export declare function nextIde(current: LocatorIde, order?: LocatorIde[]): LocatorIde;
|
|
17
|
+
export declare function parseSourceLocation(raw: string): SourceLocation;
|
|
18
|
+
export declare function formatSourceLocation(loc: SourceLocation): string;
|
|
19
|
+
export declare function nextClickTarget(current: ClickTarget, hasCss: boolean): ClickTarget;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export { resolveTheme } from './theme.js';
|
|
2
|
+
export const SOURCE_ATTR = 'data-source';
|
|
3
|
+
export const OPEN_ENDPOINT = '/__open-in-editor';
|
|
4
|
+
export const STORAGE_KEY = 'locator-ide';
|
|
5
|
+
export const DEFAULT_IDE = 'cursor';
|
|
6
|
+
export const IDE_ORDER = ['cursor', 'vscode', 'webstorm'];
|
|
7
|
+
export function isLocatorIde(value) {
|
|
8
|
+
return IDE_ORDER.includes(value);
|
|
9
|
+
}
|
|
10
|
+
export function nextIde(current, order = IDE_ORDER) {
|
|
11
|
+
const index = order.indexOf(current);
|
|
12
|
+
const nextIndex = (index + 1) % order.length;
|
|
13
|
+
return order[nextIndex] ?? DEFAULT_IDE;
|
|
14
|
+
}
|
|
15
|
+
export function parseSourceLocation(raw) {
|
|
16
|
+
const parts = raw.split(':');
|
|
17
|
+
const col = parts.pop();
|
|
18
|
+
const line = parts.pop();
|
|
19
|
+
const file = parts.join(':');
|
|
20
|
+
return { file, line, col };
|
|
21
|
+
}
|
|
22
|
+
export function formatSourceLocation(loc) {
|
|
23
|
+
return `${loc.file}:${loc.line}:${loc.col}`;
|
|
24
|
+
}
|
|
25
|
+
export function nextClickTarget(current, hasCss) {
|
|
26
|
+
if (!hasCss)
|
|
27
|
+
return 'tsx';
|
|
28
|
+
return current === 'tsx' ? 'css' : 'tsx';
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type LocatorThemePreset = 'default' | 'light' | 'dark' | 'blue';
|
|
2
|
+
export type LocatorThemeOverride = {
|
|
3
|
+
background?: string;
|
|
4
|
+
text?: string;
|
|
5
|
+
accent?: string;
|
|
6
|
+
};
|
|
7
|
+
export type LocatorThemeInput = LocatorThemePreset | LocatorThemeOverride;
|
|
8
|
+
export type LocatorTheme = {
|
|
9
|
+
badgeBackground: string;
|
|
10
|
+
badgeText: string;
|
|
11
|
+
badgeActiveBackground: string;
|
|
12
|
+
badgeActiveText: string;
|
|
13
|
+
badgeBorder: string;
|
|
14
|
+
tooltipBackground: string;
|
|
15
|
+
tooltipText: string;
|
|
16
|
+
tooltipBorder: string;
|
|
17
|
+
highlightBorder: string;
|
|
18
|
+
highlightBackground: string;
|
|
19
|
+
highlightShadow: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function resolveTheme(input?: LocatorThemeInput): LocatorTheme;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const PRESET_COLORS = {
|
|
2
|
+
default: { background: '#0f172a', text: '#f8fafc', accent: '#38bdf8' },
|
|
3
|
+
light: { background: '#ffffff', text: '#0f172a', accent: '#2563eb' },
|
|
4
|
+
dark: { background: '#000000', text: '#ffffff', accent: '#a3a3a3' },
|
|
5
|
+
blue: { background: '#1e3a8a', text: '#eff6ff', accent: '#60a5fa' },
|
|
6
|
+
};
|
|
7
|
+
function withAlpha(hex, alpha) {
|
|
8
|
+
if (!hex.startsWith('#') || hex.length !== 7)
|
|
9
|
+
return hex;
|
|
10
|
+
const channel = Math.round(alpha * 255)
|
|
11
|
+
.toString(16)
|
|
12
|
+
.padStart(2, '0');
|
|
13
|
+
return `${hex}${channel}`;
|
|
14
|
+
}
|
|
15
|
+
function buildTheme(colors) {
|
|
16
|
+
const { background, text, accent } = colors;
|
|
17
|
+
return {
|
|
18
|
+
badgeBackground: background,
|
|
19
|
+
badgeText: accent,
|
|
20
|
+
badgeActiveBackground: accent,
|
|
21
|
+
badgeActiveText: background,
|
|
22
|
+
badgeBorder: accent,
|
|
23
|
+
tooltipBackground: background,
|
|
24
|
+
tooltipText: text,
|
|
25
|
+
tooltipBorder: accent,
|
|
26
|
+
highlightBorder: accent,
|
|
27
|
+
highlightBackground: withAlpha(accent, 0.12),
|
|
28
|
+
highlightShadow: withAlpha(background, 0.8),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function isThemePreset(value) {
|
|
32
|
+
return typeof value === 'string';
|
|
33
|
+
}
|
|
34
|
+
export function resolveTheme(input) {
|
|
35
|
+
if (!input)
|
|
36
|
+
return buildTheme(PRESET_COLORS.default);
|
|
37
|
+
if (isThemePreset(input)) {
|
|
38
|
+
const preset = PRESET_COLORS[input] ?? PRESET_COLORS.default;
|
|
39
|
+
return buildTheme(preset);
|
|
40
|
+
}
|
|
41
|
+
return buildTheme({ ...PRESET_COLORS.default, ...input });
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NodePath } from '@babel/traverse';
|
|
2
|
+
import * as t from '@babel/types';
|
|
3
|
+
type BabelOptions = {
|
|
4
|
+
attribute: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function babelPluginAddSourceAttr(_: unknown, opts: BabelOptions): {
|
|
7
|
+
name: string;
|
|
8
|
+
visitor: {
|
|
9
|
+
JSXOpeningElement(path: NodePath<t.JSXOpeningElement>, state: {
|
|
10
|
+
file: {
|
|
11
|
+
opts: {
|
|
12
|
+
filename?: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
}): void;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
function hasSourceAttr(attributes, attribute) {
|
|
3
|
+
return attributes.some((attr) => t.isJSXAttribute(attr) && attr.name.name === attribute);
|
|
4
|
+
}
|
|
5
|
+
function createSourceAttr(attribute, value) {
|
|
6
|
+
return t.jsxAttribute(t.jsxIdentifier(attribute), t.stringLiteral(value));
|
|
7
|
+
}
|
|
8
|
+
export function babelPluginAddSourceAttr(_, opts) {
|
|
9
|
+
const attribute = opts.attribute;
|
|
10
|
+
return {
|
|
11
|
+
name: 'add-source-attr',
|
|
12
|
+
visitor: {
|
|
13
|
+
JSXOpeningElement(path, state) {
|
|
14
|
+
const loc = path.node.loc;
|
|
15
|
+
const filename = state.file.opts.filename;
|
|
16
|
+
if (!loc || !filename)
|
|
17
|
+
return;
|
|
18
|
+
if (hasSourceAttr(path.node.attributes, attribute))
|
|
19
|
+
return;
|
|
20
|
+
const value = `${filename}:${loc.start.line}:${loc.start.column + 1}`;
|
|
21
|
+
path.node.attributes.push(createSourceAttr(attribute, value));
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
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
|
+
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);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
editor.openWithUrl(loc);
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import type { LocatorIde, LocatorThemeInput } from '../shared/index.js';
|
|
3
|
+
import { babelPluginAddSourceAttr } from './babel-plugin.js';
|
|
4
|
+
export type SourceLocatorOptions = {
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
attribute?: string;
|
|
8
|
+
ides?: LocatorIde[];
|
|
9
|
+
theme?: LocatorThemeInput;
|
|
10
|
+
};
|
|
11
|
+
declare function sourceLocator(options?: SourceLocatorOptions): Plugin;
|
|
12
|
+
declare namespace sourceLocator {
|
|
13
|
+
var babel: (options?: SourceLocatorOptions) => {
|
|
14
|
+
babel: {
|
|
15
|
+
plugins: (typeof babelPluginAddSourceAttr | {
|
|
16
|
+
attribute: string;
|
|
17
|
+
})[][];
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export { sourceLocator };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { DEFAULT_IDE, IDE_ORDER, OPEN_ENDPOINT, SOURCE_ATTR } from '../shared/index.js';
|
|
5
|
+
import { babelPluginAddSourceAttr } from './babel-plugin.js';
|
|
6
|
+
import { openInEditor } from './editors.js';
|
|
7
|
+
const VIRTUAL_CLIENT_ID = 'virtual:source-locator-client';
|
|
8
|
+
const RESOLVED_VIRTUAL_CLIENT_ID = '\0virtual:source-locator-client';
|
|
9
|
+
const CLIENT_ENTRY = fileURLToPath(new URL('../client/index.js', import.meta.url));
|
|
10
|
+
function resolveOptions(options = {}) {
|
|
11
|
+
return {
|
|
12
|
+
enabled: options.enabled ?? true,
|
|
13
|
+
endpoint: options.endpoint ?? OPEN_ENDPOINT,
|
|
14
|
+
attribute: options.attribute ?? SOURCE_ATTR,
|
|
15
|
+
ides: options.ides ?? IDE_ORDER,
|
|
16
|
+
theme: options.theme,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function readQuery(url) {
|
|
20
|
+
const parsed = new URL(url, 'http://localhost');
|
|
21
|
+
return {
|
|
22
|
+
file: parsed.searchParams.get('file'),
|
|
23
|
+
line: parsed.searchParams.get('line') ?? '1',
|
|
24
|
+
col: parsed.searchParams.get('col') ?? '1',
|
|
25
|
+
ide: parsed.searchParams.get('ide') ?? DEFAULT_IDE,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function resolveFilePath(file, root) {
|
|
29
|
+
if (file.startsWith('/src/'))
|
|
30
|
+
return resolve(root, file.slice(1));
|
|
31
|
+
if (isAbsolute(file))
|
|
32
|
+
return file;
|
|
33
|
+
return resolve(root, file);
|
|
34
|
+
}
|
|
35
|
+
function sourceLocator(options = {}) {
|
|
36
|
+
const config = resolveOptions(options);
|
|
37
|
+
const clientConfig = {
|
|
38
|
+
endpoint: config.endpoint,
|
|
39
|
+
attribute: config.attribute,
|
|
40
|
+
ides: config.ides,
|
|
41
|
+
theme: config.theme,
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
name: 'source-locator',
|
|
45
|
+
apply: 'serve',
|
|
46
|
+
resolveId(id) {
|
|
47
|
+
if (id === VIRTUAL_CLIENT_ID)
|
|
48
|
+
return RESOLVED_VIRTUAL_CLIENT_ID;
|
|
49
|
+
return undefined;
|
|
50
|
+
},
|
|
51
|
+
load(id) {
|
|
52
|
+
if (id !== RESOLVED_VIRTUAL_CLIENT_ID)
|
|
53
|
+
return undefined;
|
|
54
|
+
return `import ${JSON.stringify(CLIENT_ENTRY)}`;
|
|
55
|
+
},
|
|
56
|
+
configureServer(server) {
|
|
57
|
+
server.middlewares.use(config.endpoint, (req, res) => {
|
|
58
|
+
const { file, line, col, ide } = readQuery(req.url ?? '');
|
|
59
|
+
if (!file) {
|
|
60
|
+
res.writeHead(400);
|
|
61
|
+
res.end('missing file');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const resolvedFile = resolveFilePath(file, server.config.root);
|
|
66
|
+
if (!existsSync(resolvedFile)) {
|
|
67
|
+
res.writeHead(404);
|
|
68
|
+
res.end('file not found');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
openInEditor({ file: resolvedFile, line, col }, ide);
|
|
72
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
73
|
+
res.end('ok');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
res.writeHead(500);
|
|
77
|
+
res.end('failed to open editor');
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
transformIndexHtml() {
|
|
82
|
+
if (!config.enabled)
|
|
83
|
+
return;
|
|
84
|
+
return {
|
|
85
|
+
html: '',
|
|
86
|
+
tags: [
|
|
87
|
+
{
|
|
88
|
+
tag: 'script',
|
|
89
|
+
children: `window.__SOURCE_LOCATOR_CONFIG__=${JSON.stringify(clientConfig)}`,
|
|
90
|
+
injectTo: 'head',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
tag: 'script',
|
|
94
|
+
attrs: { type: 'module', src: `/@id/${VIRTUAL_CLIENT_ID}` },
|
|
95
|
+
injectTo: 'body',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
sourceLocator.babel = (options = {}) => {
|
|
103
|
+
const attribute = options.attribute ?? SOURCE_ATTR;
|
|
104
|
+
return { babel: { plugins: [[babelPluginAddSourceAttr, { attribute }]] } };
|
|
105
|
+
};
|
|
106
|
+
export { sourceLocator };
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-source-locator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Dev-only Vite plugin: click UI elements to jump to source in your IDE",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
"./vite": {
|
|
14
|
+
"types": "./dist/vite/index.d.ts",
|
|
15
|
+
"default": "./dist/vite/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./client": {
|
|
18
|
+
"types": "./dist/client/index.d.ts",
|
|
19
|
+
"default": "./dist/client/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./shared": {
|
|
22
|
+
"types": "./dist/shared/index.d.ts",
|
|
23
|
+
"default": "./dist/shared/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest",
|
|
31
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"vite": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@babel/core": "^7.29.7",
|
|
38
|
+
"@babel/traverse": "^7.29.7",
|
|
39
|
+
"@babel/types": "^7.29.7"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/babel__core": "^7.20.5",
|
|
43
|
+
"@types/babel__traverse": "^7.20.7",
|
|
44
|
+
"@types/node": "^20.12.12",
|
|
45
|
+
"happy-dom": "^15.11.7",
|
|
46
|
+
"typescript": "^5.4.5",
|
|
47
|
+
"vite": "^5.2.11",
|
|
48
|
+
"vitest": "^2.1.8"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/amir1824/UI-Locator.git"
|
|
53
|
+
},
|
|
54
|
+
"keywords": [
|
|
55
|
+
"vite",
|
|
56
|
+
"vite-plugin",
|
|
57
|
+
"source-locator",
|
|
58
|
+
"devtools",
|
|
59
|
+
"cursor",
|
|
60
|
+
"vscode"
|
|
61
|
+
],
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=18"
|
|
64
|
+
}
|
|
65
|
+
}
|