vite-plugin-source-locator 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -20
- package/dist/client/controller.js +24 -49
- package/dist/client/overlay.d.ts +2 -1
- package/dist/client/overlay.js +15 -10
- package/dist/client/tooltip-text.d.ts +1 -1
- package/dist/client/tooltip-text.js +8 -17
- package/dist/shared/index.d.ts +0 -2
- package/dist/shared/index.js +0 -9
- package/dist/shared/theme.js +11 -17
- package/dist/vite/editor-cli.js +3 -2
- package/dist/vite/editors.js +2 -2
- package/dist/vite/index.js +25 -2
- package/package.json +1 -1
- package/dist/client/css-source.d.ts +0 -1
- package/dist/client/css-source.js +0 -60
- package/dist/client/preference.d.ts +0 -1
- package/dist/client/preference.js +0 -5
- package/dist/client/source-panel.d.ts +0 -11
- package/dist/client/source-panel.js +0 -113
package/README.md
CHANGED
|
@@ -44,13 +44,12 @@ No `main.tsx` wiring required. The plugin auto-injects the client overlay in dev
|
|
|
44
44
|
|
|
45
45
|
1. Click the badge (bottom-right): **Locator**
|
|
46
46
|
2. Hover elements — blue highlight + file paths in tooltip
|
|
47
|
-
3. Click to open
|
|
47
|
+
3. Click to open the TSX source file in your IDE
|
|
48
48
|
4. **Esc** — cancel pick mode
|
|
49
49
|
|
|
50
50
|
| Shortcut | Action |
|
|
51
51
|
|----------|--------|
|
|
52
|
-
| Click | Open TSX
|
|
53
|
-
| **Shift+C** | Open CSS/style directly |
|
|
52
|
+
| Click | Open TSX source |
|
|
54
53
|
| Esc | Cancel pick |
|
|
55
54
|
|
|
56
55
|
## Exports
|
|
@@ -80,7 +79,7 @@ Control overlay colors (badge, tooltip, highlight). Presets or custom colors:
|
|
|
80
79
|
| Preset | Look |
|
|
81
80
|
|--------|------|
|
|
82
81
|
| `'default'` | Dark slate + cyan accent |
|
|
83
|
-
| `'light'` | White background + blue accent |
|
|
82
|
+
| `'light'` | White background + blue accent (also used when `theme` is omitted) |
|
|
84
83
|
| `'dark'` | Black background + white/gray text |
|
|
85
84
|
| `'blue'` | Navy background + light blue accent |
|
|
86
85
|
|
|
@@ -108,20 +107,6 @@ initSourceLocator({
|
|
|
108
107
|
| `text` | Tooltip text |
|
|
109
108
|
| `accent` | Borders, highlight, badge label |
|
|
110
109
|
|
|
111
|
-
## CSS Detection
|
|
112
|
-
|
|
113
|
-
When an element has styles from a project `.css` file (e.g. `index.css` custom classes like `.glass`), the tooltip shows:
|
|
114
|
-
|
|
115
|
-
```
|
|
116
|
-
components/DashboardStats.tsx
|
|
117
|
-
CSS: index.css
|
|
118
|
-
Click → TSX | next: CSS
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Click cycles between opening the TSX and CSS file.
|
|
122
|
-
|
|
123
|
-
**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.
|
|
124
|
-
|
|
125
110
|
## MFE Safety
|
|
126
111
|
|
|
127
112
|
- **Idempotent** — `window.__sourceLocator` guard: multiple bundles loading the locator mount only one overlay
|
|
@@ -162,7 +147,7 @@ sourceLocator()
|
|
|
162
147
|
sourceLocator({ ides: ['vscode'] })
|
|
163
148
|
```
|
|
164
149
|
|
|
165
|
-
The `ides` option controls which editors the server may open. The client always
|
|
150
|
+
The `ides` option controls which editors the server may open. The browser client always sends `ide=auto`; the server resolves that via `resolveIde` against your `ides` list.
|
|
166
151
|
|
|
167
152
|
### Explicit editor
|
|
168
153
|
|
|
@@ -190,7 +175,6 @@ REACT_EDITOR=/Applications/Visual Studio Code.app/Contents/Resources/app/bin/cod
|
|
|
190
175
|
|
|
191
176
|
- Dev only — no production impact
|
|
192
177
|
- JSX/TSX only for `data-source` injection
|
|
193
|
-
- CSS line 1 only (no source-map line mapping yet)
|
|
194
178
|
|
|
195
179
|
## License
|
|
196
180
|
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { DEFAULT_IDE,
|
|
2
|
-
import { findCssSource } from './css-source.js';
|
|
1
|
+
import { DEFAULT_IDE, parseSourceLocation, resolveTheme } from '../shared/index.js';
|
|
3
2
|
import { createLocatorOverlayUi } from './overlay.js';
|
|
4
|
-
const EMPTY_SOURCES = Object.freeze({
|
|
5
|
-
tsx: undefined,
|
|
6
|
-
css: undefined,
|
|
7
|
-
clickTarget: 'tsx',
|
|
8
|
-
});
|
|
9
3
|
function getSourceEl(target, attribute, host) {
|
|
10
4
|
if (!target || host.contains(target) || target === host)
|
|
11
5
|
return null;
|
|
@@ -14,6 +8,7 @@ function getSourceEl(target, attribute, host) {
|
|
|
14
8
|
}
|
|
15
9
|
async function openSourceInEditor(source, config) {
|
|
16
10
|
const loc = parseSourceLocation(source);
|
|
11
|
+
// Client always sends ide=auto; server resolveIde + plugin ides config pick the editor.
|
|
17
12
|
const params = new URLSearchParams({
|
|
18
13
|
file: loc.file,
|
|
19
14
|
line: loc.line,
|
|
@@ -22,27 +17,26 @@ async function openSourceInEditor(source, config) {
|
|
|
22
17
|
});
|
|
23
18
|
await fetch(`${config.endpoint}?${params.toString()}`);
|
|
24
19
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
const css = findCssSource(el);
|
|
28
|
-
return { tsx, css, clickTarget: 'tsx' };
|
|
29
|
-
}
|
|
30
|
-
function resolveOpenSource(sources) {
|
|
31
|
-
return sources[sources.clickTarget];
|
|
20
|
+
function readComponentSource(el, attribute) {
|
|
21
|
+
return el.getAttribute(attribute) ?? undefined;
|
|
32
22
|
}
|
|
33
23
|
export function startPickController(root, host, config) {
|
|
34
24
|
let pickMode = false;
|
|
35
|
-
let
|
|
25
|
+
let componentSource;
|
|
36
26
|
const ui = createLocatorOverlayUi(root, () => setPickMode(!pickMode), resolveTheme(config.theme));
|
|
37
27
|
function setPickMode(active) {
|
|
28
|
+
if (active)
|
|
29
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
30
|
+
else
|
|
31
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
38
32
|
pickMode = active;
|
|
39
33
|
ui.setPickActive(active);
|
|
40
34
|
if (!active)
|
|
41
|
-
|
|
35
|
+
componentSource = undefined;
|
|
42
36
|
}
|
|
43
|
-
const
|
|
37
|
+
const syncSource = (el) => {
|
|
44
38
|
if (el !== ui.getActiveEl())
|
|
45
|
-
|
|
39
|
+
componentSource = readComponentSource(el, config.attribute);
|
|
46
40
|
};
|
|
47
41
|
const updateHover = (target, x, y) => {
|
|
48
42
|
const el = getSourceEl(target, config.attribute, host);
|
|
@@ -50,33 +44,15 @@ export function startPickController(root, host, config) {
|
|
|
50
44
|
ui.removeTooltip();
|
|
51
45
|
return;
|
|
52
46
|
}
|
|
53
|
-
|
|
54
|
-
ui.showSourceTooltip(el,
|
|
47
|
+
syncSource(el);
|
|
48
|
+
ui.showSourceTooltip(el, componentSource, x, y);
|
|
55
49
|
};
|
|
56
50
|
const onMouseMove = (e) => {
|
|
57
|
-
if (!pickMode) {
|
|
58
|
-
ui.removeTooltip();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
51
|
updateHover(e.target, e.clientX, e.clientY);
|
|
62
52
|
};
|
|
63
|
-
const onKeyDown =
|
|
64
|
-
if (e.key === 'Escape' && pickMode)
|
|
53
|
+
const onKeyDown = (e) => {
|
|
54
|
+
if (e.key === 'Escape' && pickMode)
|
|
65
55
|
setPickMode(false);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (e.shiftKey && e.key === 'C' && pickMode) {
|
|
69
|
-
const el = ui.getActiveEl();
|
|
70
|
-
if (!(el instanceof HTMLElement))
|
|
71
|
-
return;
|
|
72
|
-
syncSources(el);
|
|
73
|
-
if (!sources.css) {
|
|
74
|
-
ui.flashMessage('No CSS for this element');
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
await openSourceInEditor(sources.css, config);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
56
|
};
|
|
81
57
|
const onClick = async (e) => {
|
|
82
58
|
if (!pickMode || host.contains(e.target))
|
|
@@ -84,24 +60,23 @@ export function startPickController(root, host, config) {
|
|
|
84
60
|
e.preventDefault();
|
|
85
61
|
e.stopPropagation();
|
|
86
62
|
const el = getSourceEl(e.target, config.attribute, host);
|
|
87
|
-
if (el)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (!
|
|
63
|
+
if (!el)
|
|
64
|
+
return;
|
|
65
|
+
syncSource(el);
|
|
66
|
+
if (!componentSource) {
|
|
91
67
|
ui.flashMessage('No source for this element');
|
|
92
68
|
return;
|
|
93
69
|
}
|
|
94
|
-
await openSourceInEditor(
|
|
95
|
-
|
|
96
|
-
ui.showSourceTooltip(el, sources.tsx, sources.css, e.clientX, e.clientY);
|
|
70
|
+
await openSourceInEditor(componentSource, config);
|
|
71
|
+
ui.showSourceTooltip(el, componentSource, e.clientX, e.clientY);
|
|
97
72
|
};
|
|
98
73
|
document.addEventListener('keydown', onKeyDown);
|
|
99
|
-
document.addEventListener('mousemove', onMouseMove);
|
|
100
74
|
document.addEventListener('click', onClick, true);
|
|
101
75
|
ui.mountBadge();
|
|
102
76
|
return () => {
|
|
77
|
+
setPickMode(false);
|
|
103
78
|
document.removeEventListener('keydown', onKeyDown);
|
|
104
|
-
document.removeEventListener('mousemove', onMouseMove);
|
|
105
79
|
document.removeEventListener('click', onClick, true);
|
|
80
|
+
ui.dispose();
|
|
106
81
|
};
|
|
107
82
|
}
|
package/dist/client/overlay.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { LocatorTheme } from '../shared/index.js';
|
|
|
2
2
|
export declare function createLocatorOverlayUi(root: ShadowRoot, onTogglePick: () => void, theme: LocatorTheme): {
|
|
3
3
|
mountBadge: () => void;
|
|
4
4
|
setPickActive: (active: boolean) => void;
|
|
5
|
-
showSourceTooltip: (el: Element,
|
|
5
|
+
showSourceTooltip: (el: Element, source: string | undefined, x: number, y: number) => void;
|
|
6
6
|
flashMessage: (text: string) => void;
|
|
7
7
|
removeTooltip: () => void;
|
|
8
|
+
dispose: () => void;
|
|
8
9
|
getActiveEl: () => Element | null;
|
|
9
10
|
};
|
|
10
11
|
export type LocatorOverlayUi = ReturnType<typeof createLocatorOverlayUi>;
|
package/dist/client/overlay.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LAYOUT, UI_IDS } from './overlay-styles.js';
|
|
2
|
-
import { badgeLabel } from './preference.js';
|
|
3
2
|
import { buildTooltipText } from './tooltip-text.js';
|
|
3
|
+
const BADGE_LABEL_IDLE = 'Locator';
|
|
4
|
+
const BADGE_LABEL_PICKING = 'Picking — Esc';
|
|
4
5
|
const HIGHLIGHT_PADDING = 2;
|
|
5
6
|
const TOOLTIP_CURSOR_OFFSET = 16;
|
|
6
7
|
const FLASH_DURATION_MS = 1500;
|
|
@@ -49,8 +50,8 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
|
49
50
|
if (el)
|
|
50
51
|
showHighlight(el);
|
|
51
52
|
};
|
|
52
|
-
const showSourceTooltip = (el,
|
|
53
|
-
showTooltip(buildTooltipText(
|
|
53
|
+
const showSourceTooltip = (el, source, x, y) => {
|
|
54
|
+
showTooltip(buildTooltipText(source), el, x, y);
|
|
54
55
|
};
|
|
55
56
|
const flashMessage = (text) => {
|
|
56
57
|
if (flashTimeout)
|
|
@@ -71,26 +72,29 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
|
71
72
|
document.body.style.cursor = active ? 'crosshair' : '';
|
|
72
73
|
if (!badgeEl)
|
|
73
74
|
return;
|
|
74
|
-
badgeEl.textContent =
|
|
75
|
+
badgeEl.textContent = active ? BADGE_LABEL_PICKING : BADGE_LABEL_IDLE;
|
|
75
76
|
applyBadgeColors(active);
|
|
76
77
|
if (!active)
|
|
77
78
|
removeTooltip();
|
|
78
79
|
};
|
|
80
|
+
const dispose = () => {
|
|
81
|
+
if (flashTimeout)
|
|
82
|
+
clearTimeout(flashTimeout);
|
|
83
|
+
flashTimeout = null;
|
|
84
|
+
removeTooltip();
|
|
85
|
+
};
|
|
79
86
|
const mountBadge = () => {
|
|
80
87
|
badgeEl = document.createElement('button');
|
|
81
88
|
badgeEl.id = UI_IDS.badge;
|
|
82
89
|
badgeEl.type = 'button';
|
|
83
|
-
badgeEl.textContent =
|
|
84
|
-
Object.assign(badgeEl.style, LAYOUT.badge
|
|
85
|
-
background: theme.badgeBackground,
|
|
86
|
-
color: theme.badgeText,
|
|
87
|
-
border: `1px solid ${theme.badgeBorder}`,
|
|
88
|
-
});
|
|
90
|
+
badgeEl.textContent = BADGE_LABEL_IDLE;
|
|
91
|
+
Object.assign(badgeEl.style, LAYOUT.badge);
|
|
89
92
|
badgeEl.addEventListener('click', (event) => {
|
|
90
93
|
event.stopPropagation();
|
|
91
94
|
onTogglePick();
|
|
92
95
|
});
|
|
93
96
|
root.appendChild(badgeEl);
|
|
97
|
+
applyBadgeColors(false);
|
|
94
98
|
};
|
|
95
99
|
return {
|
|
96
100
|
mountBadge,
|
|
@@ -98,6 +102,7 @@ export function createLocatorOverlayUi(root, onTogglePick, theme) {
|
|
|
98
102
|
showSourceTooltip,
|
|
99
103
|
flashMessage,
|
|
100
104
|
removeTooltip,
|
|
105
|
+
dispose,
|
|
101
106
|
getActiveEl: () => activeEl,
|
|
102
107
|
};
|
|
103
108
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function buildTooltipText(
|
|
1
|
+
export declare function buildTooltipText(source: string | undefined): string;
|
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import { parseSourceLocation } from '../shared/index.js';
|
|
2
2
|
const DEFAULT_LINE = '1';
|
|
3
|
-
function formatSourceLabel(source
|
|
3
|
+
function formatSourceLabel(source) {
|
|
4
4
|
const { file, line } = parseSourceLocation(source);
|
|
5
5
|
const name = file.split('/').pop() ?? file;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return label;
|
|
6
|
+
if (line !== DEFAULT_LINE)
|
|
7
|
+
return `${name}:${line}`;
|
|
8
|
+
return name;
|
|
10
9
|
}
|
|
11
|
-
export function buildTooltipText(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (cssSource)
|
|
16
|
-
lines.push(formatSourceLabel(cssSource, 'CSS'));
|
|
17
|
-
if (!cssSource) {
|
|
18
|
-
lines.push('Click → open TSX');
|
|
19
|
-
return lines.join('\n');
|
|
20
|
-
}
|
|
21
|
-
lines.push('Click → open TSX', 'Shift+C → CSS');
|
|
22
|
-
return lines.join('\n');
|
|
10
|
+
export function buildTooltipText(source) {
|
|
11
|
+
if (!source)
|
|
12
|
+
return 'No source for this element';
|
|
13
|
+
return `${formatSourceLabel(source)}\nClick → open`;
|
|
23
14
|
}
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ export type SourceLocation = {
|
|
|
6
6
|
line: string;
|
|
7
7
|
col: string;
|
|
8
8
|
};
|
|
9
|
-
export type ClickTarget = 'tsx' | 'css';
|
|
10
9
|
export declare const SOURCE_ATTR = "data-source";
|
|
11
10
|
export declare const OPEN_ENDPOINT = "/__open-in-editor";
|
|
12
11
|
export declare const DEFAULT_IDE: LocatorIde;
|
|
@@ -15,4 +14,3 @@ export declare function isLocatorIde(value: string): value is LocatorIde;
|
|
|
15
14
|
export declare function resolveIde(value: string, allowed: LocatorIde[]): LocatorIde;
|
|
16
15
|
export declare function parseSourceLocation(raw: string): SourceLocation;
|
|
17
16
|
export declare function formatSourceLocation(loc: SourceLocation): string;
|
|
18
|
-
export declare function nextClickTarget(current: ClickTarget, hasCss: boolean): ClickTarget;
|
package/dist/shared/index.js
CHANGED
|
@@ -25,12 +25,3 @@ export function parseSourceLocation(raw) {
|
|
|
25
25
|
export function formatSourceLocation(loc) {
|
|
26
26
|
return `${loc.file}:${loc.line}:${loc.col}`;
|
|
27
27
|
}
|
|
28
|
-
const CLICK_TARGET_TRANSITION = {
|
|
29
|
-
tsx: 'css',
|
|
30
|
-
css: 'tsx',
|
|
31
|
-
};
|
|
32
|
-
export function nextClickTarget(current, hasCss) {
|
|
33
|
-
if (!hasCss)
|
|
34
|
-
return 'tsx';
|
|
35
|
-
return CLICK_TARGET_TRANSITION[current];
|
|
36
|
-
}
|
package/dist/shared/theme.js
CHANGED
|
@@ -19,23 +19,7 @@ function withAlpha(hex, alpha) {
|
|
|
19
19
|
}
|
|
20
20
|
function buildTheme(colors, preset) {
|
|
21
21
|
const { background, text, accent } = colors;
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
badgeBackground: background,
|
|
25
|
-
badgeText: text,
|
|
26
|
-
badgeActiveBackground: background,
|
|
27
|
-
badgeActiveText: accent,
|
|
28
|
-
badgeBorder: LIGHT_BADGE_BORDER,
|
|
29
|
-
badgeActiveBorder: accent,
|
|
30
|
-
tooltipBackground: background,
|
|
31
|
-
tooltipText: text,
|
|
32
|
-
tooltipBorder: LIGHT_BADGE_BORDER,
|
|
33
|
-
highlightBorder: accent,
|
|
34
|
-
highlightBackground: withAlpha(accent, HIGHLIGHT_ALPHA),
|
|
35
|
-
highlightShadow: withAlpha(background, SHADOW_ALPHA),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
22
|
+
const base = {
|
|
39
23
|
badgeBackground: background,
|
|
40
24
|
badgeText: accent,
|
|
41
25
|
badgeActiveBackground: accent,
|
|
@@ -49,6 +33,16 @@ function buildTheme(colors, preset) {
|
|
|
49
33
|
highlightBackground: withAlpha(accent, HIGHLIGHT_ALPHA),
|
|
50
34
|
highlightShadow: withAlpha(background, SHADOW_ALPHA),
|
|
51
35
|
};
|
|
36
|
+
if (preset !== 'light')
|
|
37
|
+
return base;
|
|
38
|
+
return {
|
|
39
|
+
...base,
|
|
40
|
+
badgeText: text,
|
|
41
|
+
badgeActiveBackground: background,
|
|
42
|
+
badgeActiveText: accent,
|
|
43
|
+
badgeBorder: LIGHT_BADGE_BORDER,
|
|
44
|
+
tooltipBorder: LIGHT_BADGE_BORDER,
|
|
45
|
+
};
|
|
52
46
|
}
|
|
53
47
|
export function resolveTheme(input) {
|
|
54
48
|
const lightBase = PRESET_COLORS.light;
|
package/dist/vite/editor-cli.js
CHANGED
|
@@ -26,15 +26,16 @@ function buildCliCandidates() {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
const CLI_CANDIDATES = buildCliCandidates();
|
|
29
|
+
const isPathLike = (value) => value.includes('/') || value.includes('\\');
|
|
29
30
|
export function toLaunchEditorName(ide) {
|
|
30
31
|
return IDE_LAUNCH_NAMES[ide];
|
|
31
32
|
}
|
|
32
33
|
export function resolveCliPath(command) {
|
|
33
|
-
if ((command
|
|
34
|
+
if (isPathLike(command) && existsSync(command)) {
|
|
34
35
|
return command;
|
|
35
36
|
}
|
|
36
37
|
const candidates = CLI_CANDIDATES[command] ?? [command];
|
|
37
|
-
const bundlePath = candidates.find((path) => (path
|
|
38
|
+
const bundlePath = candidates.find((path) => isPathLike(path) && existsSync(path));
|
|
38
39
|
if (bundlePath)
|
|
39
40
|
return bundlePath;
|
|
40
41
|
return command;
|
package/dist/vite/editors.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import launch from 'launch-editor';
|
|
2
|
-
import { resolveIde } from '../shared/index.js';
|
|
2
|
+
import { formatSourceLocation, resolveIde } from '../shared/index.js';
|
|
3
3
|
import { formatLaunchEditorCommand, resolveAutoEditor, resolveCliPath, toLaunchEditorName } from './editor-cli.js';
|
|
4
4
|
export function openInEditor(loc, ideParam, allowed) {
|
|
5
5
|
const ide = resolveIde(ideParam, allowed);
|
|
6
|
-
const spec =
|
|
6
|
+
const spec = formatSourceLocation(loc);
|
|
7
7
|
if (ide === 'auto') {
|
|
8
8
|
const resolved = resolveAutoEditor();
|
|
9
9
|
if (resolved) {
|
package/dist/vite/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { IDE_ORDER, OPEN_ENDPOINT, SOURCE_ATTR } from '../shared/index.js';
|
|
5
5
|
import { babelPluginAddSourceAttr } from './babel-plugin.js';
|
|
@@ -25,12 +25,25 @@ function readQuery(url, defaultIde) {
|
|
|
25
25
|
ide: parsed.searchParams.get('ide') ?? defaultIde,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
function isInsideRoot(resolved, root) {
|
|
29
|
+
const rel = relative(root, resolved);
|
|
30
|
+
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
|
|
31
|
+
}
|
|
32
|
+
function isAllowedRequest(req) {
|
|
33
|
+
const origin = req.headers.origin;
|
|
34
|
+
const host = req.headers.host;
|
|
35
|
+
if (origin && host && new URL(origin).host !== host)
|
|
36
|
+
return false;
|
|
37
|
+
if (req.headers['sec-fetch-site'] === 'cross-site')
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
28
41
|
function resolveFilePath(file, root) {
|
|
29
42
|
const viteDevMatch = file.match(/^\/src\/(.+)$/);
|
|
30
43
|
if (viteDevMatch)
|
|
31
44
|
return resolve(root, 'src', viteDevMatch[1]);
|
|
32
45
|
if (isAbsolute(file))
|
|
33
|
-
return file;
|
|
46
|
+
return resolve(file);
|
|
34
47
|
return resolve(root, file);
|
|
35
48
|
}
|
|
36
49
|
function sourceLocator(options = {}) {
|
|
@@ -55,6 +68,11 @@ function sourceLocator(options = {}) {
|
|
|
55
68
|
},
|
|
56
69
|
configureServer(server) {
|
|
57
70
|
server.middlewares.use(config.endpoint, (req, res) => {
|
|
71
|
+
if (!isAllowedRequest(req)) {
|
|
72
|
+
res.writeHead(403);
|
|
73
|
+
res.end('forbidden');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
58
76
|
const { file, line, col, ide } = readQuery(req.url ?? '', config.ides[0] ?? 'auto');
|
|
59
77
|
if (!file) {
|
|
60
78
|
res.writeHead(400);
|
|
@@ -63,6 +81,11 @@ function sourceLocator(options = {}) {
|
|
|
63
81
|
}
|
|
64
82
|
try {
|
|
65
83
|
const resolvedFile = resolveFilePath(file, server.config.root);
|
|
84
|
+
if (!isInsideRoot(resolvedFile, server.config.root)) {
|
|
85
|
+
res.writeHead(403);
|
|
86
|
+
res.end('outside project');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
66
89
|
if (!existsSync(resolvedFile)) {
|
|
67
90
|
res.writeHead(404);
|
|
68
91
|
res.end('file not found');
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function findCssSource(element: Element): string | undefined;
|
|
@@ -1,60 +0,0 @@
|
|
|
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;
|
|
5
|
-
function scoreSelector(selector, element) {
|
|
6
|
-
let score = selector.length;
|
|
7
|
-
if (selector.includes('#'))
|
|
8
|
-
score += SELECTOR_ID_BONUS;
|
|
9
|
-
if (selector.includes('.'))
|
|
10
|
-
score += SELECTOR_CLASS_BONUS;
|
|
11
|
-
if (element instanceof HTMLElement) {
|
|
12
|
-
element.classList.forEach((cls) => {
|
|
13
|
-
if (selector.includes(`.${cls}`))
|
|
14
|
-
score += SELECTOR_CLASS_MATCH_BONUS;
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
return score;
|
|
18
|
-
}
|
|
19
|
-
function isProjectStylesheet(href) {
|
|
20
|
-
return href.includes('.css') && (href.includes('localhost') || href.includes('/src/'));
|
|
21
|
-
}
|
|
22
|
-
function hrefToFile(href) {
|
|
23
|
-
const url = new URL(href, window.location.origin);
|
|
24
|
-
return decodeURIComponent(url.pathname);
|
|
25
|
-
}
|
|
26
|
-
export function findCssSource(element) {
|
|
27
|
-
let bestScore = -1;
|
|
28
|
-
let bestFile;
|
|
29
|
-
Array.from(document.styleSheets).forEach((sheet) => {
|
|
30
|
-
const href = sheet.href ?? '';
|
|
31
|
-
if (!href || !isProjectStylesheet(href))
|
|
32
|
-
return;
|
|
33
|
-
let rules;
|
|
34
|
-
try {
|
|
35
|
-
rules = sheet.cssRules;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
Array.from(rules).forEach((rule) => {
|
|
41
|
-
if (!(rule instanceof CSSStyleRule))
|
|
42
|
-
return;
|
|
43
|
-
try {
|
|
44
|
-
if (!element.matches(rule.selectorText))
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const score = scoreSelector(rule.selectorText, element);
|
|
51
|
-
if (score <= bestScore)
|
|
52
|
-
return;
|
|
53
|
-
bestScore = score;
|
|
54
|
-
bestFile = hrefToFile(href);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
if (!bestFile)
|
|
58
|
-
return undefined;
|
|
59
|
-
return formatSourceLocation({ file: bestFile, line: '1', col: '1' });
|
|
60
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function badgeLabel(picking: boolean): string;
|
|
@@ -1,11 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,113 +0,0 @@
|
|
|
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
|
-
}
|