vite-plugin-devtools-vue2 0.1.0 → 0.1.2

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
@@ -1,4 +1,4 @@
1
- # vite-plugin-vue2-devtools
1
+ # vite-plugin-devtools-vue2
2
2
 
3
3
  一个**仅用于开发环境**的 Vite 插件,为 Vue 2.6 应用注入一个悬浮的组件审查面板(devtools)。
4
4
 
@@ -24,7 +24,7 @@
24
24
  ## 安装
25
25
 
26
26
  ```bash
27
- npm i -D vite-plugin-vue2-devtools
27
+ npm i -D vite-plugin-devtools-vue2
28
28
  ```
29
29
 
30
30
  ## 使用
@@ -33,7 +33,7 @@ npm i -D vite-plugin-vue2-devtools
33
33
 
34
34
  ```js
35
35
  import { createVuePlugin } from 'vite-plugin-vue2';
36
- import vueDevtools from 'vite-plugin-vue2-devtools';
36
+ import vueDevtools from 'vite-plugin-devtools-vue2';
37
37
 
38
38
  export default {
39
39
  plugins: [createVuePlugin(), vueDevtools()]
package/lib/config.js ADDED
@@ -0,0 +1,22 @@
1
+ // config.js — shared tunables for the devtools panel.
2
+
3
+ // localStorage key for persisted UI state (open/closed, tab, docked position).
4
+ export const STORE_KEY = 'vue-devtools:ui';
5
+
6
+ // Panel size (keep in sync with .panel CSS) — used to keep it on-screen.
7
+ export const PANEL_W = 620;
8
+ export const PANEL_H = 420;
9
+
10
+ // Margin between the floating entry and the viewport edge.
11
+ export const EDGE_MARGIN = 12;
12
+
13
+ // Distance from the viewport edge to the panel when open (leaves room for the
14
+ // floating entry to sit in the gutter, matching the official devtools).
15
+ export const PANEL_EDGE = EDGE_MARGIN + 30 / 2;
16
+
17
+ // Pointer travel (px) before a press on the entry counts as a drag vs a click.
18
+ export const DRAG_THRESHOLD = 4;
19
+
20
+ // Show a per-section (props/data/computed/attrs…) filter input once a section
21
+ // has more than this many keys.
22
+ export const SECTION_FILTER_THRESHOLD = 10;
package/lib/events.js ADDED
@@ -0,0 +1,71 @@
1
+ // events.js — records component events by patching Vue 2's `$emit`.
2
+ //
3
+ // We capture the Vue constructor from the global hook's `init` event (fired
4
+ // before the app mounts, since this module is imported early), then wrap
5
+ // `Vue.prototype.$emit` to log every custom event with its payload + source
6
+ // component. Lifecycle `hook:*` events are filtered out as noise.
7
+
8
+ import hook from './hook.js';
9
+
10
+ const events = [];
11
+ const listeners = new Set();
12
+ let patched = false;
13
+ let seq = 0;
14
+
15
+ const MAX_EVENTS = 200;
16
+
17
+ const emit = () => {
18
+ listeners.forEach(l => l());
19
+ };
20
+
21
+ const componentName = vm => {
22
+ const o = (vm && vm.$options) || {};
23
+ let n = o.name || o._componentTag;
24
+ if (!n && o.__file)
25
+ n = String(o.__file)
26
+ .split(/[\\/]/)
27
+ .pop()
28
+ .replace(/\.vue$/, '');
29
+ if (!n && vm && vm.$root === vm) n = 'Root';
30
+ return n || 'Anonymous';
31
+ };
32
+
33
+ const record = (vm, name, args) => {
34
+ if (typeof name === 'string' && name.indexOf('hook:') === 0) return; // lifecycle noise
35
+ events.push({
36
+ id: ++seq,
37
+ name: String(name),
38
+ args,
39
+ component: componentName(vm),
40
+ time: Date.now()
41
+ });
42
+ if (events.length > MAX_EVENTS) events.shift();
43
+ emit();
44
+ };
45
+
46
+ hook.on('init', Vue => {
47
+ if (patched || !Vue || !Vue.prototype) return;
48
+ patched = true;
49
+ const original = Vue.prototype.$emit;
50
+ // Must stay a real function: `this` is the emitting component instance.
51
+ Vue.prototype.$emit = function (name, ...args) {
52
+ try {
53
+ record(this, name, args);
54
+ } catch (e) {
55
+ /* never break the app because of devtools */
56
+ }
57
+ return original.apply(this, arguments);
58
+ };
59
+ });
60
+
61
+ export const getEvents = () => events;
62
+
63
+ export const subscribe = cb => {
64
+ listeners.add(cb);
65
+ return () => listeners.delete(cb);
66
+ };
67
+
68
+ export const clearEvents = () => {
69
+ events.length = 0;
70
+ emit();
71
+ };
package/lib/index.js CHANGED
@@ -13,7 +13,7 @@ const CLIENT_ENTRY = resolve(__dirname, 'main.js');
13
13
  const PLUGIN_ROOT = __dirname;
14
14
 
15
15
  /**
16
- * vite-plugin-vue2-devtools
16
+ * vite-plugin-devtools-vue2
17
17
  *
18
18
  * Dev-only plugin. Injects a small client bundle that hooks into the Vue 2.6
19
19
  * global devtools hook, walks the component tree and renders a floating
@@ -24,7 +24,7 @@ const PLUGIN_ROOT = __dirname;
24
24
  */
25
25
  const vueDevTools = () => {
26
26
  return {
27
- name: 'vite-plugin-vue2-devtools',
27
+ name: 'vite-plugin-devtools-vue2',
28
28
  // Inspector is a development aid only; never touch the production build.
29
29
  apply: 'serve',
30
30
 
package/lib/main.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import './hook.js';
6
6
  import './panel.js';
7
7
  import './vuex.js';
8
+ import './events.js';
8
9
 
9
10
  const mount = () => {
10
11
  if (document.querySelector('#__vue_devtools__')) return;
package/lib/panel.js CHANGED
@@ -3,25 +3,14 @@
3
3
  // on every Vue scheduler flush.
4
4
 
5
5
  import { LitElement, css, html } from 'lit';
6
+ import { DRAG_THRESHOLD, EDGE_MARGIN, PANEL_EDGE, PANEL_H, PANEL_W, SECTION_FILTER_THRESHOLD, STORE_KEY } from './config.js';
6
7
  import hook from './hook.js';
7
8
  import { hide, highlight } from './inspector.js';
8
9
  import { isPicking, startPicking, stopPicking } from './picker.js';
9
10
  import { commitAll, getSnapshots, getStore, hasStore, travelTo, subscribe as vuexSubscribe } from './vuex.js';
11
+ import { clearEvents, getEvents, subscribe as eventsSubscribe } from './events.js';
10
12
  import { buildTree, formatValue, getInstance } from './walker.js';
11
13
 
12
- // Persisted UI state (survives reloads). Only stable, cheap bits — not the
13
- // component tree / expanded set (ids are regenerated each load).
14
- const STORE_KEY = 'vue-devtools:ui';
15
-
16
- // Panel size (keep in sync with .panel CSS) — used to keep it on-screen.
17
- const PANEL_W = 620;
18
- const PANEL_H = 420;
19
- const EDGE_MARGIN = 12;
20
- // Distance from the viewport edge to the panel when open (leaves room for the
21
- // floating entry to sit in the gutter, matching the official devtools).
22
- const PANEL_EDGE = EDGE_MARGIN + 30 / 2;
23
- const DRAG_THRESHOLD = 4;
24
-
25
14
  export class VueDevToolsPanel extends LitElement {
26
15
  static properties = {
27
16
  tree: { state: true },
@@ -32,6 +21,7 @@ export class VueDevToolsPanel extends LitElement {
32
21
  query: { state: true },
33
22
  tab: { state: true },
34
23
  vuexSelected: { state: true },
24
+ timelineSelected: { state: true },
35
25
  renderCodeText: { state: true }
36
26
  };
37
27
 
@@ -47,9 +37,12 @@ export class VueDevToolsPanel extends LitElement {
47
37
  this.query = '';
48
38
  this.tab = ui.tab || 'components';
49
39
  this.vuexSelected = 0;
40
+ this.timelineSelected = null;
50
41
  this.renderCodeText = null;
51
42
  this.valueExpanded = new Set();
52
43
  this.sectionCollapsed = new Set();
44
+ // Per-section (props/data/…) key filter text, keyed by section title.
45
+ this._sectionFilter = {};
53
46
  // Docked position of the entry/panel. Defaults to the bottom edge near
54
47
  // the right; { edge: 'left'|'right'|'top'|'bottom', along: number }.
55
48
  this._pos = ui.pos || { edge: 'bottom', along: Number.POSITIVE_INFINITY };
@@ -219,6 +212,7 @@ export class VueDevToolsPanel extends LitElement {
219
212
  window.addEventListener('keydown', this._onKeydown, true);
220
213
  window.addEventListener('resize', this._onResize);
221
214
  this._vuexUnsub = vuexSubscribe(() => this.requestUpdate());
215
+ this._eventsUnsub = eventsSubscribe(() => this.requestUpdate());
222
216
  // DOM-based fallback refresh (see constructor). Only childList/subtree —
223
217
  // our own panel renders inside a shadow root (not observed), and the
224
218
  // inspector highlight box only mutates via style, so this won't loop.
@@ -243,6 +237,7 @@ export class VueDevToolsPanel extends LitElement {
243
237
  this._domObserver = null;
244
238
  }
245
239
  if (this._vuexUnsub) this._vuexUnsub();
240
+ if (this._eventsUnsub) this._eventsUnsub();
246
241
  stopPicking();
247
242
  }
248
243
 
@@ -520,13 +515,48 @@ export class VueDevToolsPanel extends LitElement {
520
515
 
521
516
  // Render an object as a titled, collapsible section of expandable value rows.
522
517
  // `editable` enables inline editing of primitive leaves (writes back into obj).
518
+ // Sections with many keys get a live filter input in the header.
523
519
  _renderKvSection(title, obj, editable) {
524
- const keys = obj ? Object.keys(obj) : [];
525
- if (!keys.length) return null;
520
+ const allKeys = obj ? Object.keys(obj) : [];
521
+ if (!allKeys.length) return null;
526
522
  const collapsed = this.sectionCollapsed.has(title);
523
+ const showFilter = allKeys.length > SECTION_FILTER_THRESHOLD;
524
+ const q = (this._sectionFilter[title] || '').trim().toLowerCase();
525
+ const keys = q ? allKeys.filter(k => k.toLowerCase().includes(q)) : allKeys;
527
526
  return html`
528
- <div class="section-title" @click=${() => this._toggleSection(title)}>${this._caret(!collapsed, false)} ${title}</div>
529
- ${collapsed ? null : keys.map(k => this._renderValueRow(k, obj[k], `${title}.${k}`, 0, obj, editable))}
527
+ <div class="section-title" @click=${() => this._toggleSection(title)}>
528
+ ${this._caret(!collapsed, false)}
529
+ <span class="section-name">${title}</span>
530
+ ${showFilter
531
+ ? html`
532
+ <input
533
+ class="section-filter"
534
+ type="search"
535
+ placeholder="filter…"
536
+ .value=${this._sectionFilter[title] || ''}
537
+ @click=${e => e.stopPropagation()}
538
+ @keydown=${e => {
539
+ if (e.key === 'Escape') {
540
+ this._sectionFilter[title] = '';
541
+ this.requestUpdate();
542
+ }
543
+ e.stopPropagation();
544
+ }}
545
+ @input=${e => {
546
+ this._sectionFilter[title] = e.target.value;
547
+ this.requestUpdate();
548
+ }}
549
+ />
550
+ `
551
+ : null}
552
+ </div>
553
+ ${collapsed
554
+ ? null
555
+ : keys.length
556
+ ? keys.map(k => this._renderValueRow(k, obj[k], `${title}.${k}`, 0, obj, editable))
557
+ : html`
558
+ <div class="empty">No match</div>
559
+ `}
530
560
  `;
531
561
  }
532
562
 
@@ -833,6 +863,10 @@ export class VueDevToolsPanel extends LitElement {
833
863
  ${this._icon('vuex')}
834
864
  <span class="tip">Vuex</span>
835
865
  </button>
866
+ <button class="side-tab ${this.tab === 'timeline' ? 'active' : ''}" @click=${() => (this.tab = 'timeline')}>
867
+ ${this._icon('timeline')}
868
+ <span class="tip">Timeline</span>
869
+ </button>
836
870
  <span class="side-spacer"></span>
837
871
  ${this.tab === 'components'
838
872
  ? html`
@@ -847,7 +881,9 @@ export class VueDevToolsPanel extends LitElement {
847
881
  <span class="tip">Minimize</span>
848
882
  </button>
849
883
  </nav>
850
- <div class="main">${this.tab === 'components' ? this._renderComponents() : this._renderVuex()}</div>
884
+ <div class="main">
885
+ ${this.tab === 'components' ? this._renderComponents() : this.tab === 'vuex' ? this._renderVuex() : this._renderTimeline()}
886
+ </div>
851
887
  ${this.renderCodeText != null
852
888
  ? html`
853
889
  <div class="code-overlay">
@@ -955,7 +991,96 @@ export class VueDevToolsPanel extends LitElement {
955
991
  `;
956
992
  }
957
993
 
958
- // Plugin logo (Baidu mark).
994
+ // Merge component events + Vuex mutations into one chronological timeline
995
+ // (like the vue-devtools v7 Timeline).
996
+ _timelineEntries() {
997
+ const entries = [];
998
+ for (const e of getEvents()) {
999
+ entries.push({ id: `e${e.id}`, kind: 'event', time: e.time, title: e.name, sub: `<${e.component}>`, event: e });
1000
+ }
1001
+ getSnapshots().forEach((s, i) => {
1002
+ if (s.base) return;
1003
+ entries.push({ id: `m${i}`, kind: 'mutation', time: s.time || 0, title: s.type, sub: 'vuex', snap: s, index: i });
1004
+ });
1005
+ entries.sort((a, b) => a.time - b.time);
1006
+ return entries;
1007
+ }
1008
+
1009
+ _renderTimeline() {
1010
+ const entries = this._timelineEntries();
1011
+ if (!entries.length) {
1012
+ return html`
1013
+ <div class="body"><div class="empty">No timeline activity yet</div></div>
1014
+ `;
1015
+ }
1016
+ const sel = entries.find(e => e.id === this.timelineSelected) || entries[entries.length - 1];
1017
+ return html`
1018
+ <div class="body">
1019
+ <div class="tree">
1020
+ <div class="vuex-bar">
1021
+ <button
1022
+ class="btn"
1023
+ title="Clear recorded events"
1024
+ @click=${() => {
1025
+ clearEvents();
1026
+ this.timelineSelected = null;
1027
+ }}
1028
+ >
1029
+ ✕ Clear events
1030
+ </button>
1031
+ </div>
1032
+ ${entries.map(
1033
+ e => html`
1034
+ <div class="node ${e.id === sel.id ? 'selected' : ''}" @click=${() => (this.timelineSelected = e.id)}>
1035
+ <span class="tl-badge tl-${e.kind}">${e.kind === 'event' ? 'evt' : 'mut'}</span>
1036
+ <span class="ev-time">${this._formatTime(e.time)}</span>
1037
+ <span class="tag">${e.title}</span>
1038
+ <span class="ev-comp">${e.sub}</span>
1039
+ </div>
1040
+ `
1041
+ )}
1042
+ </div>
1043
+ <div class="detail">${this._renderTimelineDetail(sel)}</div>
1044
+ </div>
1045
+ `;
1046
+ }
1047
+
1048
+ _renderTimelineDetail(entry) {
1049
+ if (!entry) return null;
1050
+ if (entry.kind === 'event') {
1051
+ const e = entry.event;
1052
+ const argsObj = {};
1053
+ (e.args || []).forEach((a, i) => {
1054
+ argsObj[i] = a;
1055
+ });
1056
+ return html`
1057
+ ${this._renderKvSection('event', { name: e.name, from: e.component, time: new Date(e.time).toLocaleTimeString() }, false)}
1058
+ ${e.args && e.args.length
1059
+ ? this._renderKvSection('payload', argsObj, false)
1060
+ : html`
1061
+ <div class="empty">No payload</div>
1062
+ `}
1063
+ `;
1064
+ }
1065
+ const s = entry.snap;
1066
+ const store = getStore();
1067
+ const payloadObj = s.payload === undefined ? null : { payload: s.payload };
1068
+ return html`
1069
+ <button class="btn on time-travel" @click=${() => travelTo(entry.index)}>⏱ Time Travel</button>
1070
+ ${this._renderKvSection('mutation', { type: s.type, time: new Date(s.time).toLocaleTimeString() }, false)}
1071
+ ${payloadObj ? this._renderKvSection('payload', payloadObj, false) : null}
1072
+ ${this._renderKvSection('state', s.state || {}, false)}
1073
+ ${store ? this._renderKvSection('getters (live)', store.getters || {}, false) : null}
1074
+ `;
1075
+ }
1076
+
1077
+ _formatTime(t) {
1078
+ const d = new Date(t);
1079
+ const p = (n, l = 2) => String(n).padStart(l, '0');
1080
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
1081
+ }
1082
+
1083
+ // Plugin logo
959
1084
  _vueLogo() {
960
1085
  return html`
961
1086
  <svg fill-rule="evenodd" viewBox="64 64 896 896" fill="#2932E1" aria-hidden="true">
@@ -986,6 +1111,12 @@ export class VueDevToolsPanel extends LitElement {
986
1111
  <path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6" />
987
1112
  </svg>
988
1113
  `;
1114
+ case 'timeline':
1115
+ return html`
1116
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
1117
+ <path d="M3 12h4l3 8 4-16 3 8h4" />
1118
+ </svg>
1119
+ `;
989
1120
  case 'pick':
990
1121
  return html`
991
1122
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
@@ -1442,6 +1573,22 @@ export class VueDevToolsPanel extends LitElement {
1442
1573
  &:hover {
1443
1574
  color: #475467;
1444
1575
  }
1576
+ & .section-filter {
1577
+ margin-inline-start: 6px;
1578
+ inline-size: 96px;
1579
+ padding: 1px 6px;
1580
+ border: 1px solid var(--field-border);
1581
+ border-radius: 5px;
1582
+ background: var(--bg);
1583
+ color: var(--text-strong);
1584
+ font-size: 11px;
1585
+ text-transform: none;
1586
+ outline: none;
1587
+ cursor: text;
1588
+ }
1589
+ & .section-filter:focus {
1590
+ border-color: var(--accent);
1591
+ }
1445
1592
  }
1446
1593
  .vrow {
1447
1594
  display: flex;
@@ -1544,6 +1691,21 @@ export class VueDevToolsPanel extends LitElement {
1544
1691
  display: inline-block;
1545
1692
  margin-block: 4px 8px;
1546
1693
  }
1694
+ .ev-time {
1695
+ flex: none;
1696
+ font-size: 10px;
1697
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1698
+ color: var(--muted);
1699
+ }
1700
+ .ev-comp {
1701
+ margin-inline-start: auto;
1702
+ padding-inline-start: 8px;
1703
+ color: var(--muted);
1704
+ }
1705
+ .node.selected .ev-time,
1706
+ .node.selected .ev-comp {
1707
+ color: rgb(255 255 255 / 0.85);
1708
+ }
1547
1709
  `;
1548
1710
  }
1549
1711
 
package/lib/vuex.js CHANGED
@@ -44,7 +44,8 @@ hook.on('vuex:mutation', (mutation, state) => {
44
44
  snapshots.push({
45
45
  type: mutation.type,
46
46
  payload: mutation.payload,
47
- state: clone(state)
47
+ state: clone(state),
48
+ time: Date.now()
48
49
  });
49
50
  emit();
50
51
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-devtools-vue2",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "vite-plugin-vue-devtools for vue@2.x",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",