@akccakcctw/vue-grab 1.0.0 → 1.3.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/dist/core/api.d.ts +28 -0
- package/dist/core/api.js +105 -0
- package/dist/core/identifier.d.ts +2 -0
- package/dist/core/identifier.js +101 -0
- package/dist/core/overlay.d.ts +24 -0
- package/dist/core/overlay.js +509 -0
- package/dist/core/widget.d.ts +10 -0
- package/dist/core/widget.js +251 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/nuxt/module.d.ts +8 -0
- package/dist/nuxt/module.js +40 -0
- package/dist/nuxt/runtime/plugin.d.ts +2 -0
- package/dist/nuxt/runtime/plugin.js +14 -0
- package/dist/plugin.d.ts +11 -0
- package/dist/plugin.js +47 -0
- package/dist/vite.d.ts +8 -0
- package/dist/vite.js +198 -0
- package/package.json +33 -8
- package/.github/release-please-config.json +0 -9
- package/.github/release-please-manifest.json +0 -3
- package/.github/workflows/release.yml +0 -37
- package/AGENTS.md +0 -75
- package/README.md +0 -116
- package/akccakcctw-vue-grab-1.0.0.tgz +0 -0
- package/docs/SDD.md +0 -188
- package/src/__tests__/plugin.spec.ts +0 -60
- package/src/core/__tests__/api.spec.ts +0 -178
- package/src/core/__tests__/identifier.spec.ts +0 -126
- package/src/core/__tests__/overlay.spec.ts +0 -431
- package/src/core/__tests__/widget.spec.ts +0 -57
- package/src/core/api.ts +0 -144
- package/src/core/identifier.ts +0 -89
- package/src/core/overlay.ts +0 -348
- package/src/core/widget.ts +0 -289
- package/src/index.ts +0 -8
- package/src/nuxt/module.ts +0 -102
- package/src/nuxt/runtime/plugin.ts +0 -13
- package/src/plugin.ts +0 -48
- package/tsconfig.json +0 -44
- package/vitest.config.ts +0 -9
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
const CURSOR_ICON = `<svg data-v-6fdbd1c9="" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style="height: 1.2em; width: 1.2em; pointer-events: none;"><g data-v-6fdbd1c9="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle data-v-6fdbd1c9="" cx="12" cy="12" r=".5" fill="currentColor"></circle><path data-v-6fdbd1c9="" d="M5 12a7 7 0 1 0 14 0a7 7 0 1 0-14 0m7-9v2m-9 7h2m7 7v2m7-9h2"></path></g></svg>`;
|
|
2
|
+
const CHEVRON_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" width="12" height="12" style="transform: rotate(180deg); pointer-events: none;"><path d="m18 15-6-6-6 6"></path></svg>`;
|
|
3
|
+
function createToolbar(targetWindow) {
|
|
4
|
+
const doc = targetWindow.document;
|
|
5
|
+
const container = doc.createElement('div');
|
|
6
|
+
// Outer Container Styles
|
|
7
|
+
Object.assign(container.style, {
|
|
8
|
+
position: 'fixed',
|
|
9
|
+
left: '16px',
|
|
10
|
+
top: '16px',
|
|
11
|
+
zIndex: '2147483647',
|
|
12
|
+
fontFamily: 'sans-serif',
|
|
13
|
+
fontSize: '13px',
|
|
14
|
+
userSelect: 'none',
|
|
15
|
+
cursor: 'grab',
|
|
16
|
+
filter: 'drop-shadow(0px 0px 4px rgba(81, 81, 81, 0.5))',
|
|
17
|
+
transition: 'opacity 300ms ease-out, padding 0.2s ease',
|
|
18
|
+
opacity: '1',
|
|
19
|
+
});
|
|
20
|
+
container.setAttribute('data-vue-grab-toolbar', '');
|
|
21
|
+
container.setAttribute('data-vue-grab-ignore-events', '');
|
|
22
|
+
const inner = doc.createElement('div');
|
|
23
|
+
Object.assign(inner.style, {
|
|
24
|
+
display: 'flex',
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
borderRadius: '4px',
|
|
28
|
+
backgroundColor: 'white',
|
|
29
|
+
gap: '6px',
|
|
30
|
+
padding: '6px 8px',
|
|
31
|
+
transition: 'gap 0.2s ease, padding 0.2s ease',
|
|
32
|
+
});
|
|
33
|
+
// Toggle Button Wrapper
|
|
34
|
+
const toggleWrapper = doc.createElement('div');
|
|
35
|
+
Object.assign(toggleWrapper.style, {
|
|
36
|
+
display: 'flex',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
gap: '6px',
|
|
39
|
+
overflow: 'hidden',
|
|
40
|
+
maxWidth: '200px',
|
|
41
|
+
transition: 'max-width 0.2s ease, opacity 0.2s ease'
|
|
42
|
+
});
|
|
43
|
+
// Toggle Button
|
|
44
|
+
const toggleBtn = doc.createElement('button');
|
|
45
|
+
toggleBtn.setAttribute('data-vue-grab-toggle', '');
|
|
46
|
+
Object.assign(toggleBtn.style, {
|
|
47
|
+
display: 'flex',
|
|
48
|
+
alignItems: 'center',
|
|
49
|
+
justifyContent: 'center',
|
|
50
|
+
cursor: 'pointer',
|
|
51
|
+
border: 'none',
|
|
52
|
+
background: 'transparent',
|
|
53
|
+
padding: '0',
|
|
54
|
+
transition: 'transform 0.1s',
|
|
55
|
+
color: 'rgba(0, 0, 0, 0.7)'
|
|
56
|
+
});
|
|
57
|
+
toggleBtn.innerHTML = CURSOR_ICON;
|
|
58
|
+
toggleBtn.onmouseenter = () => toggleBtn.style.transform = 'scale(1.05)';
|
|
59
|
+
toggleBtn.onmouseleave = () => toggleBtn.style.transform = 'scale(1)';
|
|
60
|
+
toggleWrapper.appendChild(toggleBtn);
|
|
61
|
+
// Collapse Button
|
|
62
|
+
const collapseBtn = doc.createElement('button');
|
|
63
|
+
collapseBtn.setAttribute('data-vue-grab-collapse', '');
|
|
64
|
+
Object.assign(collapseBtn.style, {
|
|
65
|
+
display: 'flex',
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
justifyContent: 'center',
|
|
68
|
+
cursor: 'pointer',
|
|
69
|
+
border: 'none',
|
|
70
|
+
background: 'transparent',
|
|
71
|
+
padding: '0',
|
|
72
|
+
transition: 'transform 0.1s',
|
|
73
|
+
color: '#B3B3B3'
|
|
74
|
+
});
|
|
75
|
+
collapseBtn.innerHTML = CHEVRON_ICON;
|
|
76
|
+
collapseBtn.onmouseenter = () => collapseBtn.style.transform = 'scale(1.05)';
|
|
77
|
+
collapseBtn.onmouseleave = () => collapseBtn.style.transform = 'scale(1)';
|
|
78
|
+
inner.appendChild(toggleWrapper);
|
|
79
|
+
inner.appendChild(collapseBtn);
|
|
80
|
+
container.appendChild(inner);
|
|
81
|
+
return { container, toggleBtn, collapseBtn, toggleWrapper };
|
|
82
|
+
}
|
|
83
|
+
function setButtonState(toggleBtn, active) {
|
|
84
|
+
// Use color to indicate state: Blue for active, Gray for inactive
|
|
85
|
+
toggleBtn.style.color = active ? '#3b82f6' : 'rgba(0, 0, 0, 0.7)';
|
|
86
|
+
}
|
|
87
|
+
export function createToggleWidget(targetWindow, options) {
|
|
88
|
+
let elements = null;
|
|
89
|
+
let mounted = false;
|
|
90
|
+
let isActive = false;
|
|
91
|
+
let isCollapsed = false;
|
|
92
|
+
let lastPosition = {
|
|
93
|
+
left: '',
|
|
94
|
+
top: '',
|
|
95
|
+
right: '',
|
|
96
|
+
bottom: ''
|
|
97
|
+
};
|
|
98
|
+
const defaultInnerGap = '6px';
|
|
99
|
+
const defaultInnerPadding = '6px 8px';
|
|
100
|
+
const dragState = {
|
|
101
|
+
dragging: false,
|
|
102
|
+
offsetX: 0,
|
|
103
|
+
offsetY: 0,
|
|
104
|
+
moved: false
|
|
105
|
+
};
|
|
106
|
+
const startDrag = (event) => {
|
|
107
|
+
if (!elements)
|
|
108
|
+
return;
|
|
109
|
+
// Don't drag if clicking buttons directly might be better handled by stopPropagation,
|
|
110
|
+
// but here we allow dragging from anywhere on the container.
|
|
111
|
+
// However, if we click a button, we might want to prevent drag start or ensure click works?
|
|
112
|
+
// Usually standard behavior: mousedown + mouseup without move = click. mousedown + move = drag.
|
|
113
|
+
dragState.dragging = true;
|
|
114
|
+
dragState.moved = false;
|
|
115
|
+
const rect = elements.container.getBoundingClientRect();
|
|
116
|
+
dragState.offsetX = event.clientX - rect.left;
|
|
117
|
+
dragState.offsetY = event.clientY - rect.top;
|
|
118
|
+
// Switch to explicit coords for dragging
|
|
119
|
+
elements.container.style.right = 'auto';
|
|
120
|
+
elements.container.style.bottom = 'auto';
|
|
121
|
+
elements.container.style.left = `${rect.left}px`;
|
|
122
|
+
elements.container.style.top = `${rect.top}px`;
|
|
123
|
+
elements.container.style.cursor = 'grabbing';
|
|
124
|
+
};
|
|
125
|
+
const onDrag = (event) => {
|
|
126
|
+
if (!elements || !dragState.dragging)
|
|
127
|
+
return;
|
|
128
|
+
dragState.moved = true;
|
|
129
|
+
const nextLeft = event.clientX - dragState.offsetX;
|
|
130
|
+
const nextTop = event.clientY - dragState.offsetY;
|
|
131
|
+
const maxLeft = targetWindow.innerWidth - elements.container.offsetWidth;
|
|
132
|
+
const maxTop = targetWindow.innerHeight - elements.container.offsetHeight;
|
|
133
|
+
const clampedLeft = Math.max(0, Math.min(maxLeft, nextLeft));
|
|
134
|
+
const clampedTop = Math.max(0, Math.min(maxTop, nextTop));
|
|
135
|
+
elements.container.style.left = `${clampedLeft}px`;
|
|
136
|
+
elements.container.style.top = `${clampedTop}px`;
|
|
137
|
+
};
|
|
138
|
+
const endDrag = () => {
|
|
139
|
+
if (!elements)
|
|
140
|
+
return;
|
|
141
|
+
dragState.dragging = false;
|
|
142
|
+
elements.container.style.cursor = 'grab';
|
|
143
|
+
};
|
|
144
|
+
const toggleCollapse = () => {
|
|
145
|
+
if (!elements)
|
|
146
|
+
return;
|
|
147
|
+
const { container, toggleWrapper, collapseBtn } = elements;
|
|
148
|
+
const inner = container.firstElementChild;
|
|
149
|
+
const svg = collapseBtn.querySelector('svg');
|
|
150
|
+
if (!isCollapsed) {
|
|
151
|
+
const rect = container.getBoundingClientRect();
|
|
152
|
+
lastPosition = {
|
|
153
|
+
left: container.style.left,
|
|
154
|
+
top: container.style.top,
|
|
155
|
+
right: container.style.right,
|
|
156
|
+
bottom: container.style.bottom
|
|
157
|
+
};
|
|
158
|
+
const stickLeft = rect.left + rect.width / 2 < targetWindow.innerWidth / 2;
|
|
159
|
+
container.style.top = `${rect.top}px`;
|
|
160
|
+
if (stickLeft) {
|
|
161
|
+
container.style.left = '0px';
|
|
162
|
+
container.style.right = 'auto';
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
container.style.left = 'auto';
|
|
166
|
+
container.style.right = '0px';
|
|
167
|
+
}
|
|
168
|
+
container.style.bottom = 'auto';
|
|
169
|
+
container.style.transform = 'scale(0.8)';
|
|
170
|
+
container.style.padding = '0';
|
|
171
|
+
toggleWrapper.style.maxWidth = '0px';
|
|
172
|
+
toggleWrapper.style.opacity = '0';
|
|
173
|
+
toggleWrapper.style.pointerEvents = 'none';
|
|
174
|
+
if (inner) {
|
|
175
|
+
inner.style.gap = '0';
|
|
176
|
+
inner.style.padding = '6px';
|
|
177
|
+
}
|
|
178
|
+
if (svg)
|
|
179
|
+
svg.style.transform = 'rotate(0deg)';
|
|
180
|
+
isCollapsed = true;
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
container.style.left = lastPosition.left;
|
|
184
|
+
container.style.top = lastPosition.top;
|
|
185
|
+
container.style.right = lastPosition.right;
|
|
186
|
+
container.style.bottom = lastPosition.bottom;
|
|
187
|
+
container.style.transform = 'scale(1)';
|
|
188
|
+
container.style.padding = '';
|
|
189
|
+
toggleWrapper.style.maxWidth = '200px';
|
|
190
|
+
toggleWrapper.style.opacity = '1';
|
|
191
|
+
toggleWrapper.style.pointerEvents = 'auto';
|
|
192
|
+
if (inner) {
|
|
193
|
+
inner.style.gap = defaultInnerGap;
|
|
194
|
+
inner.style.padding = defaultInnerPadding;
|
|
195
|
+
}
|
|
196
|
+
if (svg)
|
|
197
|
+
svg.style.transform = 'rotate(180deg)';
|
|
198
|
+
isCollapsed = false;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
return {
|
|
202
|
+
mount() {
|
|
203
|
+
if (mounted)
|
|
204
|
+
return;
|
|
205
|
+
mounted = true;
|
|
206
|
+
elements = createToolbar(targetWindow);
|
|
207
|
+
if (!elements)
|
|
208
|
+
return;
|
|
209
|
+
setButtonState(elements.toggleBtn, isActive);
|
|
210
|
+
// Drag listeners on the container
|
|
211
|
+
elements.container.addEventListener('mousedown', startDrag);
|
|
212
|
+
// Toggle logic
|
|
213
|
+
elements.toggleBtn.addEventListener('click', (event) => {
|
|
214
|
+
event.preventDefault();
|
|
215
|
+
event.stopPropagation(); // Prevent affecting container?
|
|
216
|
+
if (dragState.moved)
|
|
217
|
+
return; // Prevent toggle if it was a drag
|
|
218
|
+
isActive = !isActive;
|
|
219
|
+
setButtonState(elements.toggleBtn, isActive);
|
|
220
|
+
options.onToggle(isActive);
|
|
221
|
+
});
|
|
222
|
+
// Collapse logic (Placeholder)
|
|
223
|
+
elements.collapseBtn.addEventListener('click', (event) => {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
event.stopPropagation();
|
|
226
|
+
if (dragState.moved)
|
|
227
|
+
return;
|
|
228
|
+
toggleCollapse();
|
|
229
|
+
});
|
|
230
|
+
targetWindow.addEventListener('mousemove', onDrag);
|
|
231
|
+
targetWindow.addEventListener('mouseup', endDrag);
|
|
232
|
+
targetWindow.document.body.appendChild(elements.container);
|
|
233
|
+
},
|
|
234
|
+
unmount() {
|
|
235
|
+
if (!mounted || !elements)
|
|
236
|
+
return;
|
|
237
|
+
mounted = false;
|
|
238
|
+
elements.container.removeEventListener('mousedown', startDrag);
|
|
239
|
+
elements.container.remove();
|
|
240
|
+
elements = null;
|
|
241
|
+
targetWindow.removeEventListener('mousemove', onDrag);
|
|
242
|
+
targetWindow.removeEventListener('mouseup', endDrag);
|
|
243
|
+
},
|
|
244
|
+
setActive(active) {
|
|
245
|
+
isActive = active;
|
|
246
|
+
if (!elements)
|
|
247
|
+
return;
|
|
248
|
+
setButtonState(elements.toggleBtn, active);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createVueGrabAPI, installVueGrab } from './core/api.js';
|
|
2
|
+
export type { VueGrabOptions } from './core/api.js';
|
|
3
|
+
export type { OverlayOptions, OverlayStyle } from './core/overlay.js';
|
|
4
|
+
export { createToggleWidget } from './core/widget.js';
|
|
5
|
+
export { identifyComponent, extractMetadata } from './core/identifier.js';
|
|
6
|
+
export { createOverlayController } from './core/overlay.js';
|
|
7
|
+
export { createVueGrabPlugin } from './plugin.js';
|
|
8
|
+
export { default } from './plugin.js';
|
|
9
|
+
export { createVueGrabVitePlugin } from './vite.js';
|
|
10
|
+
export type { VueGrabVitePluginOptions } from './vite.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createVueGrabAPI, installVueGrab } from './core/api.js';
|
|
2
|
+
export { createToggleWidget } from './core/widget.js';
|
|
3
|
+
export { identifyComponent, extractMetadata } from './core/identifier.js';
|
|
4
|
+
export { createOverlayController } from './core/overlay.js';
|
|
5
|
+
export { createVueGrabPlugin } from './plugin.js';
|
|
6
|
+
export { default } from './plugin.js';
|
|
7
|
+
export { createVueGrabVitePlugin } from './vite.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { addPlugin, createResolver, defineNuxtModule, addVitePlugin } from '@nuxt/kit';
|
|
2
|
+
import { createVueGrabVitePlugin } from '../vite.js';
|
|
3
|
+
export default defineNuxtModule({
|
|
4
|
+
meta: {
|
|
5
|
+
name: 'vue-grab',
|
|
6
|
+
configKey: 'vueGrab'
|
|
7
|
+
},
|
|
8
|
+
defaults: {
|
|
9
|
+
enabled: true
|
|
10
|
+
},
|
|
11
|
+
setup(options, nuxt) {
|
|
12
|
+
if (options.enabled === false)
|
|
13
|
+
return;
|
|
14
|
+
const shouldEnable = nuxt.options.dev || options.enabled === true;
|
|
15
|
+
if (!shouldEnable)
|
|
16
|
+
return;
|
|
17
|
+
const publicConfig = (nuxt.options.runtimeConfig.public ||= {});
|
|
18
|
+
const { enabled, overlayStyle, copyOnClick, rootDir } = options;
|
|
19
|
+
publicConfig.vueGrab = {
|
|
20
|
+
...publicConfig.vueGrab,
|
|
21
|
+
enabled,
|
|
22
|
+
overlayStyle,
|
|
23
|
+
copyOnClick,
|
|
24
|
+
rootDir: rootDir || nuxt.options.rootDir
|
|
25
|
+
};
|
|
26
|
+
nuxt.options.build.transpile = nuxt.options.build.transpile || [];
|
|
27
|
+
if (!nuxt.options.build.transpile.includes('vue-grab')) {
|
|
28
|
+
nuxt.options.build.transpile.push('vue-grab');
|
|
29
|
+
}
|
|
30
|
+
const resolver = createResolver(import.meta.url);
|
|
31
|
+
addPlugin({
|
|
32
|
+
src: resolver.resolve('./runtime/plugin'),
|
|
33
|
+
mode: 'client'
|
|
34
|
+
});
|
|
35
|
+
addVitePlugin(createVueGrabVitePlugin({
|
|
36
|
+
enabled: true,
|
|
37
|
+
rootDir: rootDir || nuxt.options.rootDir
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineNuxtPlugin, useRuntimeConfig } from '#app';
|
|
2
|
+
import { installVueGrab } from '../../core/api.js';
|
|
3
|
+
export default defineNuxtPlugin(() => {
|
|
4
|
+
const config = useRuntimeConfig().public?.vueGrab ?? {};
|
|
5
|
+
if (config.enabled === false)
|
|
6
|
+
return;
|
|
7
|
+
if (typeof window === 'undefined')
|
|
8
|
+
return;
|
|
9
|
+
installVueGrab(window, {
|
|
10
|
+
overlayStyle: config.overlayStyle,
|
|
11
|
+
copyOnClick: config.copyOnClick,
|
|
12
|
+
rootDir: config.rootDir
|
|
13
|
+
});
|
|
14
|
+
});
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { VueGrabOptions } from './core/api.js';
|
|
2
|
+
export type VueGrabPluginOptions = VueGrabOptions & {
|
|
3
|
+
enabled?: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function createVueGrabPlugin(options?: VueGrabPluginOptions): {
|
|
6
|
+
install(): void;
|
|
7
|
+
};
|
|
8
|
+
declare const _default: {
|
|
9
|
+
install(): void;
|
|
10
|
+
};
|
|
11
|
+
export default _default;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { installVueGrab } from './core/api.js';
|
|
2
|
+
function isDevEnvironment() {
|
|
3
|
+
if (typeof import.meta !== 'undefined' && import.meta.env) {
|
|
4
|
+
return Boolean(import.meta.env.DEV);
|
|
5
|
+
}
|
|
6
|
+
try {
|
|
7
|
+
const globalScope = typeof globalThis !== 'undefined' ? globalThis :
|
|
8
|
+
typeof window !== 'undefined' ? window :
|
|
9
|
+
typeof self !== 'undefined' ? self :
|
|
10
|
+
typeof global !== 'undefined' ? global : {};
|
|
11
|
+
const proc = globalScope.process;
|
|
12
|
+
if (proc && proc.env) {
|
|
13
|
+
return proc.env.NODE_ENV !== 'production';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// ignore
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
export function createVueGrabPlugin(options = {}) {
|
|
22
|
+
return {
|
|
23
|
+
install() {
|
|
24
|
+
const enabled = typeof options.enabled === 'boolean' ? options.enabled : isDevEnvironment();
|
|
25
|
+
if (enabled && typeof window !== 'undefined') {
|
|
26
|
+
const overlayOptions = {};
|
|
27
|
+
if (options.overlayStyle !== undefined) {
|
|
28
|
+
overlayOptions.overlayStyle = options.overlayStyle;
|
|
29
|
+
}
|
|
30
|
+
if (options.onCopy !== undefined) {
|
|
31
|
+
overlayOptions.onCopy = options.onCopy;
|
|
32
|
+
}
|
|
33
|
+
if (options.copyOnClick !== undefined) {
|
|
34
|
+
overlayOptions.copyOnClick = options.copyOnClick;
|
|
35
|
+
}
|
|
36
|
+
if (options.rootDir !== undefined) {
|
|
37
|
+
overlayOptions.rootDir = options.rootDir;
|
|
38
|
+
}
|
|
39
|
+
if (options.domFileResolver !== undefined) {
|
|
40
|
+
overlayOptions.domFileResolver = options.domFileResolver;
|
|
41
|
+
}
|
|
42
|
+
installVueGrab(window, overlayOptions);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export default createVueGrabPlugin();
|
package/dist/vite.d.ts
ADDED
package/dist/vite.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const LOC_ATTR = 'data-vue-grab-loc';
|
|
4
|
+
function createLocAttributeTransform() {
|
|
5
|
+
const transform = (node) => {
|
|
6
|
+
if (node?.type !== 1 || node?.tagType !== 0)
|
|
7
|
+
return;
|
|
8
|
+
const loc = node.loc?.start;
|
|
9
|
+
if (!loc)
|
|
10
|
+
return;
|
|
11
|
+
if (!Array.isArray(node.props))
|
|
12
|
+
node.props = [];
|
|
13
|
+
const hasLoc = node.props.some((prop) => prop?.type === 6 && prop?.name === LOC_ATTR);
|
|
14
|
+
if (hasLoc)
|
|
15
|
+
return;
|
|
16
|
+
node.props.push({
|
|
17
|
+
type: 6,
|
|
18
|
+
name: LOC_ATTR,
|
|
19
|
+
value: {
|
|
20
|
+
type: 2,
|
|
21
|
+
content: `${loc.line}:${loc.column}`,
|
|
22
|
+
loc: node.loc
|
|
23
|
+
},
|
|
24
|
+
loc: node.loc
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
transform.__vueGrabLocTransform = true;
|
|
28
|
+
return transform;
|
|
29
|
+
}
|
|
30
|
+
function normalizePath(value) {
|
|
31
|
+
return value.split(path.sep).join('/');
|
|
32
|
+
}
|
|
33
|
+
function resolveFilePath(filename, rootDir) {
|
|
34
|
+
if (!rootDir)
|
|
35
|
+
return filename;
|
|
36
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
37
|
+
const resolvedFile = path.resolve(filename);
|
|
38
|
+
const relative = path.relative(resolvedRoot, resolvedFile);
|
|
39
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
40
|
+
return filename;
|
|
41
|
+
}
|
|
42
|
+
return `/${normalizePath(relative)}`;
|
|
43
|
+
}
|
|
44
|
+
function escapeRegExp(value) {
|
|
45
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
46
|
+
}
|
|
47
|
+
function getLineCol(code, index) {
|
|
48
|
+
const pre = code.slice(0, index);
|
|
49
|
+
const lines = pre.split('\n');
|
|
50
|
+
const line = lines.length;
|
|
51
|
+
const lastLine = lines[lines.length - 1] ?? '';
|
|
52
|
+
const column = lastLine.length + 1;
|
|
53
|
+
return { line, column };
|
|
54
|
+
}
|
|
55
|
+
function normalizeScriptStart(source, index) {
|
|
56
|
+
if (source[index] === '\r' && source[index + 1] === '\n')
|
|
57
|
+
return index + 2;
|
|
58
|
+
if (source[index] === '\n')
|
|
59
|
+
return index + 1;
|
|
60
|
+
return index;
|
|
61
|
+
}
|
|
62
|
+
function getComponentLocationFromSource(source) {
|
|
63
|
+
const scriptSetupMatch = source.match(/<script\b[^>]*\bsetup\b[^>]*>/i);
|
|
64
|
+
if (scriptSetupMatch && scriptSetupMatch.index !== undefined) {
|
|
65
|
+
const scriptStart = normalizeScriptStart(source, scriptSetupMatch.index + scriptSetupMatch[0].length);
|
|
66
|
+
return getLineCol(source, scriptStart);
|
|
67
|
+
}
|
|
68
|
+
const scriptMatch = source.match(/<script\b[^>]*>/i);
|
|
69
|
+
if (scriptMatch && scriptMatch.index !== undefined) {
|
|
70
|
+
const scriptStart = normalizeScriptStart(source, scriptMatch.index + scriptMatch[0].length);
|
|
71
|
+
const scriptEnd = source.indexOf('</script>', scriptStart);
|
|
72
|
+
const scriptContent = scriptEnd === -1 ? source.slice(scriptStart) : source.slice(scriptStart, scriptEnd);
|
|
73
|
+
const exportMatch = scriptContent.match(/export default/);
|
|
74
|
+
if (exportMatch && exportMatch.index !== undefined) {
|
|
75
|
+
return getLineCol(source, scriptStart + exportMatch.index);
|
|
76
|
+
}
|
|
77
|
+
const defineMatch = scriptContent.match(/defineComponent\s*\(/);
|
|
78
|
+
if (defineMatch && defineMatch.index !== undefined) {
|
|
79
|
+
return getLineCol(source, scriptStart + defineMatch.index);
|
|
80
|
+
}
|
|
81
|
+
return getLineCol(source, scriptStart);
|
|
82
|
+
}
|
|
83
|
+
return { line: 1, column: 1 };
|
|
84
|
+
}
|
|
85
|
+
function createInjectedCode(name, file, line, column) {
|
|
86
|
+
return `\n${name}.__file = ${JSON.stringify(file)};\n${name}.__line = ${line};\n${name}.__column = ${column};\n`;
|
|
87
|
+
}
|
|
88
|
+
function injectComponentMetadata(code, file, source) {
|
|
89
|
+
const sourceLocation = source ? getComponentLocationFromSource(source) : null;
|
|
90
|
+
const matchExportSfc = code.match(/export default\s+(?:\/\*.*?\*\/\s*)*_export_sfc\s*\(\s*([a-zA-Z0-9_$]+)\s*,/);
|
|
91
|
+
if (matchExportSfc && matchExportSfc.index !== undefined) {
|
|
92
|
+
const name = matchExportSfc[1];
|
|
93
|
+
if (!name)
|
|
94
|
+
return null;
|
|
95
|
+
const location = sourceLocation ?? getLineCol(code, matchExportSfc.index);
|
|
96
|
+
const inject = createInjectedCode(name, file, location.line, location.column);
|
|
97
|
+
return {
|
|
98
|
+
code: code.replace(matchExportSfc[0], inject + matchExportSfc[0]),
|
|
99
|
+
map: null
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const matchVar = code.match(/export default\s+([a-zA-Z0-9_$]+)/);
|
|
103
|
+
if (matchVar && matchVar.index !== undefined) {
|
|
104
|
+
const name = matchVar[1];
|
|
105
|
+
if (!name)
|
|
106
|
+
return null;
|
|
107
|
+
const varMatch = code.match(new RegExp(`(?:const|let|var)\\s+${escapeRegExp(name)}\\s*=`));
|
|
108
|
+
const location = sourceLocation ??
|
|
109
|
+
(varMatch?.index !== undefined ? getLineCol(code, varMatch.index) : { line: 1, column: 1 });
|
|
110
|
+
const inject = createInjectedCode(name, file, location.line, location.column);
|
|
111
|
+
return {
|
|
112
|
+
code: code.replace(matchVar[0], inject + matchVar[0]),
|
|
113
|
+
map: null
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const matchObj = code.match(/export default\s*\{/);
|
|
117
|
+
if (matchObj && matchObj.index !== undefined) {
|
|
118
|
+
const { line, column } = sourceLocation ?? getLineCol(code, matchObj.index);
|
|
119
|
+
return {
|
|
120
|
+
code: code.replace(/export default\s*\{/, `export default { __file: ${JSON.stringify(file)}, __line: ${line}, __column: ${column},`),
|
|
121
|
+
map: null
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const matchDef = code.match(/export default\s+defineComponent\s*\(\s*\{/);
|
|
125
|
+
if (matchDef && matchDef.index !== undefined) {
|
|
126
|
+
const { line, column } = sourceLocation ?? getLineCol(code, matchDef.index);
|
|
127
|
+
return {
|
|
128
|
+
code: code.replace(/export default\s+defineComponent\s*\(\s*\{/, `export default defineComponent({ __file: ${JSON.stringify(file)}, __line: ${line}, __column: ${column},`),
|
|
129
|
+
map: null
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
function shouldTransform(id, include, exclude) {
|
|
135
|
+
if (!id.match(/\.vue($|\?)/))
|
|
136
|
+
return false;
|
|
137
|
+
if (id.includes('node_modules'))
|
|
138
|
+
return false;
|
|
139
|
+
if (exclude?.test(id))
|
|
140
|
+
return false;
|
|
141
|
+
if (include && !include.test(id))
|
|
142
|
+
return false;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
export function createVueGrabVitePlugin(options = {}) {
|
|
146
|
+
let enabled = false;
|
|
147
|
+
let resolvedRoot;
|
|
148
|
+
let resolvedInclude = options.include;
|
|
149
|
+
let resolvedExclude = options.exclude;
|
|
150
|
+
return {
|
|
151
|
+
name: 'vite-plugin-vue-grab-injector',
|
|
152
|
+
enforce: 'post',
|
|
153
|
+
configResolved(config) {
|
|
154
|
+
enabled = options.enabled ?? config.command === 'serve';
|
|
155
|
+
resolvedRoot = options.rootDir ?? config.root;
|
|
156
|
+
if (!enabled)
|
|
157
|
+
return;
|
|
158
|
+
const vuePlugin = config.plugins.find((plugin) => plugin.name === 'vite:vue' && plugin.api?.options);
|
|
159
|
+
if (!vuePlugin)
|
|
160
|
+
return;
|
|
161
|
+
const vueOptions = vuePlugin.api.options;
|
|
162
|
+
const templateOptions = vueOptions.template ?? {};
|
|
163
|
+
const compilerOptions = templateOptions.compilerOptions ?? {};
|
|
164
|
+
const nodeTransforms = compilerOptions.nodeTransforms ?? [];
|
|
165
|
+
const hasTransform = nodeTransforms.some((transform) => transform?.__vueGrabLocTransform);
|
|
166
|
+
if (hasTransform)
|
|
167
|
+
return;
|
|
168
|
+
vuePlugin.api.options = {
|
|
169
|
+
...vueOptions,
|
|
170
|
+
template: {
|
|
171
|
+
...templateOptions,
|
|
172
|
+
compilerOptions: {
|
|
173
|
+
...compilerOptions,
|
|
174
|
+
nodeTransforms: [...nodeTransforms, createLocAttributeTransform()]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
transform(code, id) {
|
|
180
|
+
if (!enabled)
|
|
181
|
+
return;
|
|
182
|
+
if (!shouldTransform(id, resolvedInclude, resolvedExclude))
|
|
183
|
+
return;
|
|
184
|
+
const [filename] = id.split('?');
|
|
185
|
+
if (!filename)
|
|
186
|
+
return;
|
|
187
|
+
const file = resolveFilePath(filename, resolvedRoot);
|
|
188
|
+
let source;
|
|
189
|
+
try {
|
|
190
|
+
source = fs.readFileSync(filename, 'utf8');
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
source = undefined;
|
|
194
|
+
}
|
|
195
|
+
return injectComponentMetadata(code, file, source);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akccakcctw/vue-grab",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "",
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Developer-only bridge for inspecting Vue/Nuxt component context from the DOM.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/akccakcctw/vue-grab.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
7
13
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
-
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./module": {
|
|
19
|
+
"types": "./dist/nuxt/module.d.ts",
|
|
20
|
+
"default": "./dist/nuxt/module.js"
|
|
21
|
+
},
|
|
22
|
+
"./vite": {
|
|
23
|
+
"types": "./dist/vite.d.ts",
|
|
24
|
+
"default": "./dist/vite.js"
|
|
25
|
+
}
|
|
10
26
|
},
|
|
11
|
-
"
|
|
12
|
-
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"nuxt": "./dist/nuxt/module.js",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"vue",
|
|
33
|
+
"nuxt",
|
|
34
|
+
"devtools"
|
|
35
|
+
],
|
|
13
36
|
"author": "",
|
|
14
37
|
"license": "MIT",
|
|
15
38
|
"publishConfig": {
|
|
@@ -17,6 +40,7 @@
|
|
|
17
40
|
},
|
|
18
41
|
"devDependencies": {
|
|
19
42
|
"@nuxt/kit": "^4.2.2",
|
|
43
|
+
"@types/node": "^20.19.11",
|
|
20
44
|
"@vitejs/plugin-vue": "^6.0.3",
|
|
21
45
|
"@vue/test-utils": "^2.4.6",
|
|
22
46
|
"jsdom": "^27.4.0",
|
|
@@ -26,6 +50,7 @@
|
|
|
26
50
|
"vue": "^3.5.26"
|
|
27
51
|
},
|
|
28
52
|
"scripts": {
|
|
53
|
+
"build": "tsc -p tsconfig.build.json",
|
|
29
54
|
"test": "vitest run"
|
|
30
55
|
}
|
|
31
56
|
}
|