vite-plugin-devtools-vue2 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +55 -0
- package/lib/hook.js +41 -0
- package/lib/index.js +63 -0
- package/lib/inspector.js +38 -0
- package/lib/main.js +21 -0
- package/lib/panel.js +1550 -0
- package/lib/picker.js +128 -0
- package/lib/vuex.js +88 -0
- package/lib/walker.js +133 -0
- package/package.json +24 -0
package/lib/picker.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// picker.js — "click an element on the page to select its component" mode.
|
|
2
|
+
// Hover draws a transient highlight + a name label; click resolves the nearest
|
|
3
|
+
// Vue instance and hands it back via the callback. Esc cancels.
|
|
4
|
+
|
|
5
|
+
let active = false;
|
|
6
|
+
let box = null;
|
|
7
|
+
let label = null;
|
|
8
|
+
let onPick = null;
|
|
9
|
+
let hovered = null;
|
|
10
|
+
|
|
11
|
+
const nearestVm = el => {
|
|
12
|
+
while (el) {
|
|
13
|
+
if (el.__vue__) return el.__vue__;
|
|
14
|
+
el = el.parentElement;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const vmName = vm => {
|
|
20
|
+
const o = vm.$options || {};
|
|
21
|
+
let n = o.name || o._componentTag;
|
|
22
|
+
if (!n && o.__file)
|
|
23
|
+
n = String(o.__file)
|
|
24
|
+
.split(/[\\/]/)
|
|
25
|
+
.pop()
|
|
26
|
+
.replace(/\.vue$/, '');
|
|
27
|
+
if (!n && vm.$root === vm) n = 'Root';
|
|
28
|
+
return n || 'Anonymous';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ensureEls = () => {
|
|
32
|
+
if (box) return;
|
|
33
|
+
box = document.createElement('div');
|
|
34
|
+
Object.assign(box.style, {
|
|
35
|
+
position: 'fixed',
|
|
36
|
+
zIndex: '2147483646',
|
|
37
|
+
background: 'rgba(65, 184, 131, 0.35)',
|
|
38
|
+
border: '1px solid rgba(65, 184, 131, 0.9)',
|
|
39
|
+
borderRadius: '3px',
|
|
40
|
+
pointerEvents: 'none',
|
|
41
|
+
display: 'none'
|
|
42
|
+
});
|
|
43
|
+
label = document.createElement('div');
|
|
44
|
+
Object.assign(label.style, {
|
|
45
|
+
position: 'fixed',
|
|
46
|
+
zIndex: '2147483647',
|
|
47
|
+
background: '#41b883',
|
|
48
|
+
color: '#17222b',
|
|
49
|
+
font: '600 11px -apple-system, sans-serif',
|
|
50
|
+
padding: '2px 6px',
|
|
51
|
+
borderRadius: '3px',
|
|
52
|
+
pointerEvents: 'none',
|
|
53
|
+
display: 'none',
|
|
54
|
+
whiteSpace: 'nowrap'
|
|
55
|
+
});
|
|
56
|
+
document.body.appendChild(box);
|
|
57
|
+
document.body.appendChild(label);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const paint = vm => {
|
|
61
|
+
const el = vm && vm.$el;
|
|
62
|
+
if (!el || !el.getBoundingClientRect) return clear();
|
|
63
|
+
const r = el.getBoundingClientRect();
|
|
64
|
+
Object.assign(box.style, {
|
|
65
|
+
display: 'block',
|
|
66
|
+
top: `${r.top}px`,
|
|
67
|
+
left: `${r.left}px`,
|
|
68
|
+
width: `${r.width}px`,
|
|
69
|
+
height: `${r.height}px`
|
|
70
|
+
});
|
|
71
|
+
label.textContent = `<${vmName(vm)}>`;
|
|
72
|
+
label.style.display = 'block';
|
|
73
|
+
label.style.top = `${Math.max(0, r.top - 20)}px`;
|
|
74
|
+
label.style.left = `${r.left}px`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const clear = () => {
|
|
78
|
+
if (box) box.style.display = 'none';
|
|
79
|
+
if (label) label.style.display = 'none';
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const onMove = e => {
|
|
83
|
+
const vm = nearestVm(e.target);
|
|
84
|
+
hovered = vm;
|
|
85
|
+
if (vm) paint(vm);
|
|
86
|
+
else clear();
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onClick = e => {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
e.stopPropagation();
|
|
92
|
+
const vm = hovered || nearestVm(e.target);
|
|
93
|
+
const cb = onPick;
|
|
94
|
+
stopPicking();
|
|
95
|
+
if (vm && cb) cb(vm);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onKey = e => {
|
|
99
|
+
if (e.key === 'Escape') stopPicking();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const startPicking = cb => {
|
|
103
|
+
if (active) return;
|
|
104
|
+
ensureEls();
|
|
105
|
+
active = true;
|
|
106
|
+
onPick = cb;
|
|
107
|
+
hovered = null;
|
|
108
|
+
document.addEventListener('mousemove', onMove, true);
|
|
109
|
+
document.addEventListener('click', onClick, true);
|
|
110
|
+
document.addEventListener('keydown', onKey, true);
|
|
111
|
+
document.body.style.cursor = 'crosshair';
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const stopPicking = () => {
|
|
115
|
+
if (!active) return;
|
|
116
|
+
active = false;
|
|
117
|
+
onPick = null;
|
|
118
|
+
hovered = null;
|
|
119
|
+
document.removeEventListener('mousemove', onMove, true);
|
|
120
|
+
document.removeEventListener('click', onClick, true);
|
|
121
|
+
document.removeEventListener('keydown', onKey, true);
|
|
122
|
+
document.body.style.cursor = '';
|
|
123
|
+
clear();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const isPicking = () => {
|
|
127
|
+
return active;
|
|
128
|
+
};
|
package/lib/vuex.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// vuex.js — Vuex 3.x integration.
|
|
2
|
+
//
|
|
3
|
+
// Vuex's devtool plugin talks to the global hook: on store creation it emits
|
|
4
|
+
// `vuex:init` with the store, subscribes to mutations emitting `vuex:mutation`
|
|
5
|
+
// (mutation, state), and listens for `vuex:travel-to-state` to replaceState.
|
|
6
|
+
// We record a snapshot per mutation so the panel can inspect state over time
|
|
7
|
+
// and time-travel. This module must be imported before the store is created,
|
|
8
|
+
// which main.js guarantees (it runs before the app code).
|
|
9
|
+
|
|
10
|
+
import hook from './hook.js';
|
|
11
|
+
|
|
12
|
+
const snapshots = []; // { type, payload, state, base? }
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
let store = null;
|
|
15
|
+
|
|
16
|
+
const emit = () => {
|
|
17
|
+
listeners.forEach(l => l());
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Deep clone so a snapshot isn't mutated by later state changes. Vuex state is
|
|
21
|
+
// normally serialisable; fall back to the live reference if it isn't.
|
|
22
|
+
const clone = state => {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(JSON.stringify(state));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return state;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
hook.on('vuex:init', s => {
|
|
31
|
+
store = s;
|
|
32
|
+
snapshots.length = 0;
|
|
33
|
+
snapshots.push({
|
|
34
|
+
type: 'Base State',
|
|
35
|
+
payload: undefined,
|
|
36
|
+
state: clone(s.state),
|
|
37
|
+
base: true
|
|
38
|
+
});
|
|
39
|
+
emit();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
hook.on('vuex:mutation', (mutation, state) => {
|
|
43
|
+
if (!store) return;
|
|
44
|
+
snapshots.push({
|
|
45
|
+
type: mutation.type,
|
|
46
|
+
payload: mutation.payload,
|
|
47
|
+
state: clone(state)
|
|
48
|
+
});
|
|
49
|
+
emit();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const hasStore = () => {
|
|
53
|
+
return !!store;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const getSnapshots = () => {
|
|
57
|
+
return snapshots;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const getStore = () => {
|
|
61
|
+
return store;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const subscribe = cb => {
|
|
65
|
+
listeners.add(cb);
|
|
66
|
+
return () => listeners.delete(cb);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Apply a recorded snapshot to the live store (Vuex's plugin does the
|
|
70
|
+
// replaceState in response to this event).
|
|
71
|
+
export const travelTo = index => {
|
|
72
|
+
const snap = snapshots[index];
|
|
73
|
+
if (!snap || !store) return;
|
|
74
|
+
hook.emit('vuex:travel-to-state', snap.state);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Drop history and treat the current live state as the new base.
|
|
78
|
+
export const commitAll = () => {
|
|
79
|
+
if (!store) return;
|
|
80
|
+
snapshots.length = 0;
|
|
81
|
+
snapshots.push({
|
|
82
|
+
type: 'Base State',
|
|
83
|
+
payload: undefined,
|
|
84
|
+
state: clone(store.state),
|
|
85
|
+
base: true
|
|
86
|
+
});
|
|
87
|
+
emit();
|
|
88
|
+
};
|
package/lib/walker.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// walker.js — turn live Vue 2 component instances into plain data the panel
|
|
2
|
+
// can render, and read a selected instance's props/data/computed.
|
|
3
|
+
|
|
4
|
+
// Stable ids across re-walks: same instance -> same id.
|
|
5
|
+
const idMap = new WeakMap();
|
|
6
|
+
let uid = 0;
|
|
7
|
+
const idOf = vm => {
|
|
8
|
+
let id = idMap.get(vm);
|
|
9
|
+
if (id === undefined) {
|
|
10
|
+
id = ++uid;
|
|
11
|
+
idMap.set(vm, id);
|
|
12
|
+
}
|
|
13
|
+
return id;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Registry so the panel can resolve an id back to the live instance.
|
|
17
|
+
const registry = new Map();
|
|
18
|
+
|
|
19
|
+
export const getInstance = id => {
|
|
20
|
+
return registry.get(id);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const displayName = vm => {
|
|
24
|
+
const opts = vm.$options || {};
|
|
25
|
+
let name = opts.name || opts._componentTag;
|
|
26
|
+
if (!name && opts.__file) {
|
|
27
|
+
// vite-plugin-vue2 sets __file in dev — gives nice names like "UserCard".
|
|
28
|
+
name = String(opts.__file)
|
|
29
|
+
.split(/[\\/]/)
|
|
30
|
+
.pop()
|
|
31
|
+
.replace(/\.vue$/, '');
|
|
32
|
+
}
|
|
33
|
+
if (!name && vm.$root === vm) name = 'Root';
|
|
34
|
+
return name || 'Anonymous';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Find every root instance currently mounted in the DOM. Vue 2 sets
|
|
38
|
+
// `el.__vue__` on component elements; `$root` dedupes them into app roots.
|
|
39
|
+
export const findRoots = () => {
|
|
40
|
+
const roots = new Set();
|
|
41
|
+
const els = document.querySelectorAll('*');
|
|
42
|
+
for (const el of els) {
|
|
43
|
+
const vm = el.__vue__;
|
|
44
|
+
if (vm && vm.$root) roots.add(vm.$root);
|
|
45
|
+
}
|
|
46
|
+
return [...roots];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Build a serialisable tree; keeps the registry in sync with what's shown.
|
|
50
|
+
// The synthetic root instance (`new Vue({ render: h => h(App) })`) carries no
|
|
51
|
+
// meaningful state, so — like vue-devtools — we skip it and surface its
|
|
52
|
+
// children (e.g. <App>) as the top-level nodes.
|
|
53
|
+
export const buildTree = () => {
|
|
54
|
+
registry.clear();
|
|
55
|
+
return findRoots().flatMap(vm => walk(vm).children);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const walk = vm => {
|
|
59
|
+
const id = idOf(vm);
|
|
60
|
+
registry.set(id, vm);
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
name: displayName(vm),
|
|
64
|
+
children: (vm.$children || []).map(walk)
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ---- value formatting -------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
// Render an arbitrary value into a short, safe display string. Same-realm, so
|
|
71
|
+
// we can inspect types directly; we just avoid dumping huge/circular objects.
|
|
72
|
+
export const formatValue = (value, depth = 0) => {
|
|
73
|
+
const t = typeof value;
|
|
74
|
+
if (value === null) return 'null';
|
|
75
|
+
if (value === undefined) return 'undefined';
|
|
76
|
+
if (t === 'string') return JSON.stringify(value);
|
|
77
|
+
if (t === 'number' || t === 'boolean') return String(value);
|
|
78
|
+
if (t === 'function') return `ƒ ${value.name || 'anonymous'}()`;
|
|
79
|
+
if (t === 'symbol') return value.toString();
|
|
80
|
+
if (value instanceof Node) return `<${value.nodeName.toLowerCase()}>`;
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
if (depth > 1) return `Array(${value.length})`;
|
|
83
|
+
const items = value.slice(0, 5).map(v => formatValue(v, depth + 1));
|
|
84
|
+
if (value.length > 5) items.push(`… +${value.length - 5}`);
|
|
85
|
+
return `[${items.join(', ')}]`;
|
|
86
|
+
}
|
|
87
|
+
if (t === 'object') {
|
|
88
|
+
if (depth > 1) return 'Object';
|
|
89
|
+
const keys = Object.keys(value);
|
|
90
|
+
const preview = keys.slice(0, 5).map(k => `${k}: ${formatValue(value[k], depth + 1)}`);
|
|
91
|
+
if (keys.length > 5) preview.push('…');
|
|
92
|
+
return `{ ${preview.join(', ')} }`;
|
|
93
|
+
}
|
|
94
|
+
return String(value);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Extract props / data / computed for the detail pane. Values are returned raw
|
|
98
|
+
// (not stringified) so the panel can render an expandable value tree.
|
|
99
|
+
export const inspect = vm => {
|
|
100
|
+
if (!vm) return { props: [], data: [], computed: [] };
|
|
101
|
+
|
|
102
|
+
const props = [];
|
|
103
|
+
const propDefs = (vm.$options && vm.$options.props) || null;
|
|
104
|
+
if (propDefs) {
|
|
105
|
+
for (const key of Object.keys(propDefs)) {
|
|
106
|
+
props.push({ key, value: vm[key] });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const data = [];
|
|
111
|
+
const raw = vm._data || vm.$data;
|
|
112
|
+
if (raw) {
|
|
113
|
+
for (const key of Object.keys(raw)) {
|
|
114
|
+
data.push({ key, value: raw[key] });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const computed = [];
|
|
119
|
+
const computedDefs = (vm.$options && vm.$options.computed) || null;
|
|
120
|
+
if (computedDefs) {
|
|
121
|
+
for (const key of Object.keys(computedDefs)) {
|
|
122
|
+
let value;
|
|
123
|
+
try {
|
|
124
|
+
value = vm[key]; // reading the getter is intentional
|
|
125
|
+
} catch (err) {
|
|
126
|
+
value = `⚠ ${err && err.message}`;
|
|
127
|
+
}
|
|
128
|
+
computed.push({ key, value });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { props, data, computed };
|
|
133
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vite-plugin-devtools-vue2",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "vite-plugin-vue-devtools for vue@2.x",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"lib"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"vite-plugin",
|
|
15
|
+
"vue2",
|
|
16
|
+
"vue-devtools",
|
|
17
|
+
"devtools",
|
|
18
|
+
"inspector"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"lit": "^3.1.4"
|
|
23
|
+
}
|
|
24
|
+
}
|