@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.
Files changed (41) hide show
  1. package/dist/core/api.d.ts +28 -0
  2. package/dist/core/api.js +105 -0
  3. package/dist/core/identifier.d.ts +2 -0
  4. package/dist/core/identifier.js +101 -0
  5. package/dist/core/overlay.d.ts +24 -0
  6. package/dist/core/overlay.js +509 -0
  7. package/dist/core/widget.d.ts +10 -0
  8. package/dist/core/widget.js +251 -0
  9. package/dist/index.d.ts +10 -0
  10. package/dist/index.js +7 -0
  11. package/dist/nuxt/module.d.ts +8 -0
  12. package/dist/nuxt/module.js +40 -0
  13. package/dist/nuxt/runtime/plugin.d.ts +2 -0
  14. package/dist/nuxt/runtime/plugin.js +14 -0
  15. package/dist/plugin.d.ts +11 -0
  16. package/dist/plugin.js +47 -0
  17. package/dist/vite.d.ts +8 -0
  18. package/dist/vite.js +198 -0
  19. package/package.json +33 -8
  20. package/.github/release-please-config.json +0 -9
  21. package/.github/release-please-manifest.json +0 -3
  22. package/.github/workflows/release.yml +0 -37
  23. package/AGENTS.md +0 -75
  24. package/README.md +0 -116
  25. package/akccakcctw-vue-grab-1.0.0.tgz +0 -0
  26. package/docs/SDD.md +0 -188
  27. package/src/__tests__/plugin.spec.ts +0 -60
  28. package/src/core/__tests__/api.spec.ts +0 -178
  29. package/src/core/__tests__/identifier.spec.ts +0 -126
  30. package/src/core/__tests__/overlay.spec.ts +0 -431
  31. package/src/core/__tests__/widget.spec.ts +0 -57
  32. package/src/core/api.ts +0 -144
  33. package/src/core/identifier.ts +0 -89
  34. package/src/core/overlay.ts +0 -348
  35. package/src/core/widget.ts +0 -289
  36. package/src/index.ts +0 -8
  37. package/src/nuxt/module.ts +0 -102
  38. package/src/nuxt/runtime/plugin.ts +0 -13
  39. package/src/plugin.ts +0 -48
  40. package/tsconfig.json +0 -44
  41. 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
+ }
@@ -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,8 @@
1
+ export type VueGrabNuxtModuleOptions = {
2
+ enabled?: boolean;
3
+ overlayStyle?: Record<string, string>;
4
+ copyOnClick?: boolean;
5
+ rootDir?: string;
6
+ };
7
+ declare const _default: NuxtModule<TOptions, TOptions, false>;
8
+ export default _default;
@@ -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,2 @@
1
+ declare const _default: any;
2
+ export default _default;
@@ -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
+ });
@@ -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
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from 'vite';
2
+ export type VueGrabVitePluginOptions = {
3
+ enabled?: boolean;
4
+ rootDir?: string;
5
+ include?: RegExp;
6
+ exclude?: RegExp;
7
+ };
8
+ export declare function createVueGrabVitePlugin(options?: VueGrabVitePluginOptions): Plugin;
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.0.0",
4
- "description": "",
5
- "main": "src/index.ts",
6
- "module": "src/index.ts",
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
- ".": "./src/index.ts",
9
- "./module": "./src/nuxt/module.ts"
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
- "nuxt": "src/nuxt/module.ts",
12
- "keywords": [],
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
  }