vite-plugin-source-locator 1.1.3 → 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 CHANGED
@@ -42,11 +42,15 @@ 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 (auto) — click to pick**
45
+ 1. Click the badge (bottom-right): **Locator**
46
46
  2. Hover elements — blue highlight + file paths in tooltip
47
- 3. Click to open source — cycles **TSX CSS TSX** when both exist
47
+ 3. Click to open the TSX source file in your IDE
48
48
  4. **Esc** — cancel pick mode
49
- 5. **Shift+L** — cycle IDE (session only, resets on refresh)
49
+
50
+ | Shortcut | Action |
51
+ |----------|--------|
52
+ | Click | Open TSX source |
53
+ | Esc | Cancel pick |
50
54
 
51
55
  ## Exports
52
56
 
@@ -75,7 +79,7 @@ Control overlay colors (badge, tooltip, highlight). Presets or custom colors:
75
79
  | Preset | Look |
76
80
  |--------|------|
77
81
  | `'default'` | Dark slate + cyan accent |
78
- | `'light'` | White background + blue accent |
82
+ | `'light'` | White background + blue accent (also used when `theme` is omitted) |
79
83
  | `'dark'` | Black background + white/gray text |
80
84
  | `'blue'` | Navy background + light blue accent |
81
85
 
@@ -89,7 +93,6 @@ import { initSourceLocator } from 'vite-plugin-source-locator/client'
89
93
  initSourceLocator({
90
94
  endpoint: '/__open-in-editor',
91
95
  attribute: 'data-source',
92
- ides: ['auto', 'cursor', 'vscode', 'webstorm'],
93
96
  theme: {
94
97
  background: '#ffffff',
95
98
  text: '#000000',
@@ -104,20 +107,6 @@ initSourceLocator({
104
107
  | `text` | Tooltip text |
105
108
  | `accent` | Borders, highlight, badge label |
106
109
 
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
110
  ## MFE Safety
122
111
 
123
112
  - **Idempotent** — `window.__sourceLocator` guard: multiple bundles loading the locator mount only one overlay
@@ -132,7 +121,6 @@ import { initSourceLocator } from 'vite-plugin-source-locator/client'
132
121
  initSourceLocator({
133
122
  endpoint: '/__open-in-editor',
134
123
  attribute: 'data-source',
135
- ides: ['auto', 'cursor', 'vscode', 'webstorm'],
136
124
  theme: 'blue',
137
125
  })
138
126
  ```
@@ -157,12 +145,9 @@ sourceLocator()
157
145
 
158
146
  // VS Code only
159
147
  sourceLocator({ ides: ['vscode'] })
160
-
161
- // auto + manual override with Shift+L (session only)
162
- sourceLocator({ ides: ['auto', 'vscode'] })
163
148
  ```
164
149
 
165
- The first entry in `ides` is the default. Shift+L cycles through the list in memory (resets on page refresh).
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,8 +1,7 @@
1
- import type { LocatorIde, LocatorThemeInput } from '../shared/index.js';
1
+ import type { LocatorThemeInput } from '../shared/index.js';
2
2
  export type ClientConfig = {
3
3
  endpoint: string;
4
4
  attribute: string;
5
- ides: LocatorIde[];
6
5
  theme?: LocatorThemeInput;
7
6
  };
8
7
  export declare function startPickController(root: ShadowRoot, host: Element, config: ClientConfig): () => void;
@@ -1,49 +1,42 @@
1
- import { nextClickTarget, nextIde, parseSourceLocation, resolveTheme } from '../shared/index.js';
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;
12
6
  const el = target.closest(`[${attribute}]`);
13
7
  return el instanceof HTMLElement ? el : null;
14
8
  }
15
- async function openSourceInEditor(source, config, activeIde) {
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,
20
15
  col: loc.col,
21
- ide: activeIde,
16
+ ide: DEFAULT_IDE,
22
17
  });
23
18
  await fetch(`${config.endpoint}?${params.toString()}`);
24
19
  }
25
- function readElementSources(el, attribute) {
26
- const tsx = el.getAttribute(attribute) ?? undefined;
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 sources = EMPTY_SOURCES;
36
- let activeIde = config.ides[0] ?? 'auto';
37
- const ui = createLocatorOverlayUi(root, () => setPickMode(!pickMode), resolveTheme(config.theme), () => activeIde);
25
+ let componentSource;
26
+ const ui = createLocatorOverlayUi(root, () => setPickMode(!pickMode), resolveTheme(config.theme));
38
27
  function setPickMode(active) {
28
+ if (active)
29
+ document.addEventListener('mousemove', onMouseMove);
30
+ else
31
+ document.removeEventListener('mousemove', onMouseMove);
39
32
  pickMode = active;
40
33
  ui.setPickActive(active);
41
34
  if (!active)
42
- sources = EMPTY_SOURCES;
35
+ componentSource = undefined;
43
36
  }
44
- const syncSources = (el) => {
37
+ const syncSource = (el) => {
45
38
  if (el !== ui.getActiveEl())
46
- sources = readElementSources(el, config.attribute);
39
+ componentSource = readComponentSource(el, config.attribute);
47
40
  };
48
41
  const updateHover = (target, x, y) => {
49
42
  const el = getSourceEl(target, config.attribute, host);
@@ -51,27 +44,15 @@ export function startPickController(root, host, config) {
51
44
  ui.removeTooltip();
52
45
  return;
53
46
  }
54
- syncSources(el);
55
- ui.showSourceTooltip(el, sources.tsx, sources.css, sources.clickTarget, x, y);
47
+ syncSource(el);
48
+ ui.showSourceTooltip(el, componentSource, x, y);
56
49
  };
57
50
  const onMouseMove = (e) => {
58
- if (!pickMode) {
59
- ui.removeTooltip();
60
- return;
61
- }
62
51
  updateHover(e.target, e.clientX, e.clientY);
63
52
  };
64
53
  const onKeyDown = (e) => {
65
- if (e.key === 'Escape' && pickMode) {
54
+ if (e.key === 'Escape' && pickMode)
66
55
  setPickMode(false);
67
- return;
68
- }
69
- if (!e.shiftKey || e.key !== 'L')
70
- return;
71
- activeIde = nextIde(activeIde, config.ides);
72
- if (!pickMode)
73
- ui.refreshBadgeLabel();
74
- ui.flashMessage(`IDE: ${activeIde}`);
75
56
  };
76
57
  const onClick = async (e) => {
77
58
  if (!pickMode || host.contains(e.target))
@@ -79,24 +60,23 @@ export function startPickController(root, host, config) {
79
60
  e.preventDefault();
80
61
  e.stopPropagation();
81
62
  const el = getSourceEl(e.target, config.attribute, host);
82
- if (el)
83
- syncSources(el);
84
- const openSource = el && resolveOpenSource(sources);
85
- if (!el || !openSource) {
63
+ if (!el)
64
+ return;
65
+ syncSource(el);
66
+ if (!componentSource) {
86
67
  ui.flashMessage('No source for this element');
87
68
  return;
88
69
  }
89
- await openSourceInEditor(openSource, config, activeIde);
90
- sources = { ...sources, clickTarget: nextClickTarget(sources.clickTarget, !!sources.css) };
91
- ui.showSourceTooltip(el, sources.tsx, sources.css, sources.clickTarget, e.clientX, e.clientY);
70
+ await openSourceInEditor(componentSource, config);
71
+ ui.showSourceTooltip(el, componentSource, e.clientX, e.clientY);
92
72
  };
93
73
  document.addEventListener('keydown', onKeyDown);
94
- document.addEventListener('mousemove', onMouseMove);
95
74
  document.addEventListener('click', onClick, true);
96
75
  ui.mountBadge();
97
76
  return () => {
77
+ setPickMode(false);
98
78
  document.removeEventListener('keydown', onKeyDown);
99
- document.removeEventListener('mousemove', onMouseMove);
100
79
  document.removeEventListener('click', onClick, true);
80
+ ui.dispose();
101
81
  };
102
82
  }
@@ -1,10 +1,12 @@
1
1
  import { startPickController } from './controller.js';
2
+ import { HOST_LAYOUT } from './overlay-styles.js';
2
3
  const HOST_ID = 'source-locator-host';
3
4
  export function initSourceLocator(config) {
4
5
  if (window.__sourceLocator || document.getElementById(HOST_ID))
5
6
  return;
6
7
  const host = document.createElement('div');
7
8
  host.id = HOST_ID;
9
+ Object.assign(host.style, HOST_LAYOUT);
8
10
  const root = host.attachShadow({ mode: 'open' });
9
11
  document.body.appendChild(host);
10
12
  const disposeController = startPickController(root, host, config);
@@ -3,18 +3,25 @@ export declare const UI_IDS: {
3
3
  readonly tooltip: "source-locator-tooltip";
4
4
  readonly highlight: "source-locator-highlight";
5
5
  };
6
+ export declare const HOST_LAYOUT: {
7
+ readonly position: "fixed";
8
+ readonly inset: "0";
9
+ readonly zIndex: "99999";
10
+ readonly pointerEvents: "none";
11
+ };
6
12
  export declare const LAYOUT: {
7
13
  readonly badge: {
8
14
  readonly position: "fixed";
9
15
  readonly bottom: "12px";
10
16
  readonly right: "12px";
11
- readonly padding: "8px 12px";
12
- readonly borderRadius: "999px";
13
- readonly fontSize: "11px";
17
+ readonly padding: "10px 14px";
18
+ readonly borderRadius: "8px";
19
+ readonly fontSize: "12px";
14
20
  readonly fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace";
15
21
  readonly zIndex: "99999";
16
22
  readonly cursor: "pointer";
17
- readonly boxShadow: "0 4px 16px rgba(0, 0, 0, 0.35)";
23
+ readonly pointerEvents: "auto";
24
+ readonly boxShadow: "0 2px 12px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.08)";
18
25
  };
19
26
  readonly tooltip: {
20
27
  readonly position: "fixed";
@@ -3,18 +3,25 @@ export const UI_IDS = {
3
3
  tooltip: 'source-locator-tooltip',
4
4
  highlight: 'source-locator-highlight',
5
5
  };
6
+ export const HOST_LAYOUT = {
7
+ position: 'fixed',
8
+ inset: '0',
9
+ zIndex: '99999',
10
+ pointerEvents: 'none',
11
+ };
6
12
  export const LAYOUT = {
7
13
  badge: {
8
14
  position: 'fixed',
9
15
  bottom: '12px',
10
16
  right: '12px',
11
- padding: '8px 12px',
12
- borderRadius: '999px',
13
- fontSize: '11px',
17
+ padding: '10px 14px',
18
+ borderRadius: '8px',
19
+ fontSize: '12px',
14
20
  fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
15
21
  zIndex: '99999',
16
22
  cursor: 'pointer',
17
- boxShadow: '0 4px 16px rgba(0, 0, 0, 0.35)',
23
+ pointerEvents: 'auto',
24
+ boxShadow: '0 2px 12px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.08)',
18
25
  },
19
26
  tooltip: {
20
27
  position: 'fixed',
@@ -1,11 +1,11 @@
1
- import type { ClickTarget, LocatorIde, LocatorTheme } from '../shared/index.js';
2
- export declare function createLocatorOverlayUi(root: ShadowRoot, onTogglePick: () => void, theme: LocatorTheme, getActiveIde: () => LocatorIde): {
1
+ import type { LocatorTheme } from '../shared/index.js';
2
+ export declare function createLocatorOverlayUi(root: ShadowRoot, onTogglePick: () => void, theme: LocatorTheme): {
3
3
  mountBadge: () => void;
4
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;
5
+ showSourceTooltip: (el: Element, source: string | undefined, x: number, y: number) => void;
7
6
  flashMessage: (text: string) => void;
8
7
  removeTooltip: () => void;
8
+ dispose: () => void;
9
9
  getActiveEl: () => Element | null;
10
10
  };
11
11
  export type LocatorOverlayUi = ReturnType<typeof createLocatorOverlayUi>;
@@ -1,12 +1,13 @@
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;
7
8
  const FLASH_HORIZONTAL_OFFSET = 80;
8
9
  const FLASH_BOTTOM_OFFSET = 80;
9
- export function createLocatorOverlayUi(root, onTogglePick, theme, getActiveIde) {
10
+ export function createLocatorOverlayUi(root, onTogglePick, theme) {
10
11
  let activeEl = null;
11
12
  let flashTimeout = null;
12
13
  let badgeEl = null;
@@ -49,8 +50,8 @@ export function createLocatorOverlayUi(root, onTogglePick, theme, getActiveIde)
49
50
  if (el)
50
51
  showHighlight(el);
51
52
  };
52
- const showSourceTooltip = (el, tsxSource, cssSource, clickTarget, x, y) => {
53
- showTooltip(buildTooltipText(tsxSource, cssSource, clickTarget), el, x, y);
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)
@@ -65,43 +66,43 @@ export function createLocatorOverlayUi(root, onTogglePick, theme, getActiveIde)
65
66
  return;
66
67
  badgeEl.style.background = active ? theme.badgeActiveBackground : theme.badgeBackground;
67
68
  badgeEl.style.color = active ? theme.badgeActiveText : theme.badgeText;
69
+ badgeEl.style.border = `1px solid ${active ? theme.badgeActiveBorder : theme.badgeBorder}`;
68
70
  };
69
71
  const setPickActive = (active) => {
70
72
  document.body.style.cursor = active ? 'crosshair' : '';
71
73
  if (!badgeEl)
72
74
  return;
73
- badgeEl.textContent = badgeLabel(active, getActiveIde());
75
+ badgeEl.textContent = active ? BADGE_LABEL_PICKING : BADGE_LABEL_IDLE;
74
76
  applyBadgeColors(active);
75
77
  if (!active)
76
78
  removeTooltip();
77
79
  };
78
- const refreshBadgeLabel = () => {
79
- if (badgeEl)
80
- badgeEl.textContent = badgeLabel(false, getActiveIde());
80
+ const dispose = () => {
81
+ if (flashTimeout)
82
+ clearTimeout(flashTimeout);
83
+ flashTimeout = null;
84
+ removeTooltip();
81
85
  };
82
86
  const mountBadge = () => {
83
87
  badgeEl = document.createElement('button');
84
88
  badgeEl.id = UI_IDS.badge;
85
89
  badgeEl.type = 'button';
86
- badgeEl.textContent = badgeLabel(false, getActiveIde());
87
- Object.assign(badgeEl.style, LAYOUT.badge, {
88
- background: theme.badgeBackground,
89
- color: theme.badgeText,
90
- border: `1px solid ${theme.badgeBorder}`,
91
- });
90
+ badgeEl.textContent = BADGE_LABEL_IDLE;
91
+ Object.assign(badgeEl.style, LAYOUT.badge);
92
92
  badgeEl.addEventListener('click', (event) => {
93
93
  event.stopPropagation();
94
94
  onTogglePick();
95
95
  });
96
96
  root.appendChild(badgeEl);
97
+ applyBadgeColors(false);
97
98
  };
98
99
  return {
99
100
  mountBadge,
100
101
  setPickActive,
101
- refreshBadgeLabel,
102
102
  showSourceTooltip,
103
103
  flashMessage,
104
104
  removeTooltip,
105
+ dispose,
105
106
  getActiveEl: () => activeEl,
106
107
  };
107
108
  }
@@ -1,2 +1 @@
1
- import type { ClickTarget } from '../shared/index.js';
2
- export declare function buildTooltipText(tsxSource: string | undefined, cssSource: string | undefined, clickTarget: ClickTarget): string;
1
+ export declare function buildTooltipText(source: string | undefined): string;
@@ -1,28 +1,14 @@
1
1
  import { parseSourceLocation } from '../shared/index.js';
2
2
  const DEFAULT_LINE = '1';
3
- const CLICK_PROMPT_ORDER = {
4
- tsx: ['TSX', 'CSS'],
5
- css: ['CSS', 'TSX'],
6
- };
7
- function formatSourceLabel(source, prefix) {
3
+ function formatSourceLabel(source) {
8
4
  const { file, line } = parseSourceLocation(source);
9
5
  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;
6
+ if (line !== DEFAULT_LINE)
7
+ return `${name}:${line}`;
8
+ return name;
14
9
  }
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');
10
+ export function buildTooltipText(source) {
11
+ if (!source)
12
+ return 'No source for this element';
13
+ return `${formatSourceLabel(source)}\nClick → open`;
28
14
  }
@@ -6,14 +6,11 @@ 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;
13
12
  export declare const IDE_ORDER: LocatorIde[];
14
13
  export declare function isLocatorIde(value: string): value is LocatorIde;
15
14
  export declare function resolveIde(value: string, allowed: LocatorIde[]): LocatorIde;
16
- export declare function nextIde(current: LocatorIde, order?: LocatorIde[]): LocatorIde;
17
15
  export declare function parseSourceLocation(raw: string): SourceLocation;
18
16
  export declare function formatSourceLocation(loc: SourceLocation): string;
19
- export declare function nextClickTarget(current: ClickTarget, hasCss: boolean): ClickTarget;
@@ -14,11 +14,6 @@ export function resolveIde(value, allowed) {
14
14
  return fallback;
15
15
  return value;
16
16
  }
17
- export function nextIde(current, order = IDE_ORDER) {
18
- const index = order.indexOf(current);
19
- const nextIndex = (index + 1) % order.length;
20
- return order[nextIndex] ?? DEFAULT_IDE;
21
- }
22
17
  export function parseSourceLocation(raw) {
23
18
  const parts = raw.split(':');
24
19
  if (parts.length < 3)
@@ -30,12 +25,3 @@ export function parseSourceLocation(raw) {
30
25
  export function formatSourceLocation(loc) {
31
26
  return `${loc.file}:${loc.line}:${loc.col}`;
32
27
  }
33
- const CLICK_TARGET_TRANSITION = {
34
- tsx: 'css',
35
- css: 'tsx',
36
- };
37
- export function nextClickTarget(current, hasCss) {
38
- if (!hasCss)
39
- return 'tsx';
40
- return CLICK_TARGET_TRANSITION[current];
41
- }
@@ -11,6 +11,7 @@ export type LocatorTheme = {
11
11
  badgeActiveBackground: string;
12
12
  badgeActiveText: string;
13
13
  badgeBorder: string;
14
+ badgeActiveBorder: string;
14
15
  tooltipBackground: string;
15
16
  tooltipText: string;
16
17
  tooltipBorder: string;
@@ -1,9 +1,10 @@
1
1
  const PRESET_COLORS = {
2
2
  default: { background: '#0f172a', text: '#f8fafc', accent: '#38bdf8' },
3
- light: { background: '#ffffff', text: '#0f172a', accent: '#2563eb' },
3
+ light: { background: '#ffffff', text: '#111827', accent: '#2563eb' },
4
4
  dark: { background: '#000000', text: '#ffffff', accent: '#a3a3a3' },
5
5
  blue: { background: '#1e3a8a', text: '#eff6ff', accent: '#60a5fa' },
6
6
  };
7
+ const LIGHT_BADGE_BORDER = '#d1d5db';
7
8
  const HEX_COLOR_LENGTH = 7;
8
9
  const RGB_CHANNEL_MAX = 255;
9
10
  const HIGHLIGHT_ALPHA = 0.12;
@@ -16,14 +17,15 @@ function withAlpha(hex, alpha) {
16
17
  .padStart(2, '0');
17
18
  return `${hex}${channel}`;
18
19
  }
19
- function buildTheme(colors) {
20
+ function buildTheme(colors, preset) {
20
21
  const { background, text, accent } = colors;
21
- return {
22
+ const base = {
22
23
  badgeBackground: background,
23
24
  badgeText: accent,
24
25
  badgeActiveBackground: accent,
25
26
  badgeActiveText: background,
26
27
  badgeBorder: accent,
28
+ badgeActiveBorder: accent,
27
29
  tooltipBackground: background,
28
30
  tooltipText: text,
29
31
  tooltipBorder: accent,
@@ -31,12 +33,22 @@ function buildTheme(colors) {
31
33
  highlightBackground: withAlpha(accent, HIGHLIGHT_ALPHA),
32
34
  highlightShadow: withAlpha(background, SHADOW_ALPHA),
33
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
+ };
34
46
  }
35
47
  export function resolveTheme(input) {
36
- const base = PRESET_COLORS.default;
48
+ const lightBase = PRESET_COLORS.light;
37
49
  if (!input)
38
- return buildTheme(base);
50
+ return buildTheme(lightBase, 'light');
39
51
  if (typeof input === 'string')
40
- return buildTheme(PRESET_COLORS[input] ?? base);
41
- return buildTheme({ ...base, ...input });
52
+ return buildTheme(PRESET_COLORS[input] ?? lightBase, input);
53
+ return buildTheme({ ...lightBase, ...input }, 'light');
42
54
  }
@@ -1,4 +1,5 @@
1
1
  import type { LocatorIde } from '../shared/index.js';
2
2
  export declare function toLaunchEditorName(ide: Exclude<LocatorIde, 'auto'>): string;
3
3
  export declare function resolveCliPath(command: string): string;
4
+ export declare function formatLaunchEditorCommand(command: string): string;
4
5
  export declare function resolveAutoEditor(): string | null;
@@ -26,19 +26,26 @@ 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.includes('/') || command.includes('\\')) && existsSync(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.includes('/') || path.includes('\\')) && existsSync(path));
38
+ const bundlePath = candidates.find((path) => isPathLike(path) && existsSync(path));
38
39
  if (bundlePath)
39
40
  return bundlePath;
40
41
  return command;
41
42
  }
43
+ export function formatLaunchEditorCommand(command) {
44
+ if (command.includes(' ')) {
45
+ return `"${command}"`;
46
+ }
47
+ return command;
48
+ }
42
49
  export function resolveAutoEditor() {
43
50
  const [editor] = guessEditor();
44
51
  if (!editor)
@@ -1,18 +1,18 @@
1
1
  import launch from 'launch-editor';
2
- import { resolveIde } from '../shared/index.js';
3
- import { resolveAutoEditor, resolveCliPath, toLaunchEditorName } from './editor-cli.js';
2
+ import { formatSourceLocation, resolveIde } from '../shared/index.js';
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 = `${loc.file}:${loc.line}:${loc.col}`;
6
+ const spec = formatSourceLocation(loc);
7
7
  if (ide === 'auto') {
8
8
  const resolved = resolveAutoEditor();
9
9
  if (resolved) {
10
- launch(spec, resolved);
10
+ launch(spec, formatLaunchEditorCommand(resolved));
11
11
  return;
12
12
  }
13
13
  launch(spec);
14
14
  return;
15
15
  }
16
16
  const command = resolveCliPath(toLaunchEditorName(ide));
17
- launch(spec, command);
17
+ launch(spec, formatLaunchEditorCommand(command));
18
18
  }
@@ -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 = {}) {
@@ -38,7 +51,6 @@ function sourceLocator(options = {}) {
38
51
  const clientConfig = {
39
52
  endpoint: config.endpoint,
40
53
  attribute: config.attribute,
41
- ides: config.ides,
42
54
  theme: config.theme,
43
55
  };
44
56
  return {
@@ -56,6 +68,11 @@ function sourceLocator(options = {}) {
56
68
  },
57
69
  configureServer(server) {
58
70
  server.middlewares.use(config.endpoint, (req, res) => {
71
+ if (!isAllowedRequest(req)) {
72
+ res.writeHead(403);
73
+ res.end('forbidden');
74
+ return;
75
+ }
59
76
  const { file, line, col, ide } = readQuery(req.url ?? '', config.ides[0] ?? 'auto');
60
77
  if (!file) {
61
78
  res.writeHead(400);
@@ -64,6 +81,11 @@ function sourceLocator(options = {}) {
64
81
  }
65
82
  try {
66
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
+ }
67
89
  if (!existsSync(resolvedFile)) {
68
90
  res.writeHead(404);
69
91
  res.end('file not found');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-source-locator",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
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",
@@ -32,7 +32,7 @@
32
32
  "prepublishOnly": "npm run lint && npm run build && npm run test"
33
33
  },
34
34
  "peerDependencies": {
35
- "vite": "^5.0.0"
35
+ "vite": "^5.0.0 || ^6.0.0"
36
36
  },
37
37
  "dependencies": {
38
38
  "@babel/core": "^7.29.7",
@@ -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,2 +0,0 @@
1
- import type { LocatorIde } from '../shared/index.js';
2
- export declare function badgeLabel(picking: boolean, activeIde: LocatorIde): string;
@@ -1,7 +0,0 @@
1
- const BADGE_PICKING_HINT = 'Esc to cancel';
2
- const BADGE_IDLE_HINT = 'click to pick';
3
- export function badgeLabel(picking, activeIde) {
4
- if (picking)
5
- return `Pick element (${activeIde}) — ${BADGE_PICKING_HINT}`;
6
- return `Source Locator (${activeIde}) — ${BADGE_IDLE_HINT}`;
7
- }
@@ -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, '&amp;')
8
- .replace(/</g, '&lt;')
9
- .replace(/>/g, '&gt;');
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
- }