@dimina-kit/inspect 0.3.0-dev.20260711141929
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 +56 -0
- package/dist/connected-panel.js +28 -0
- package/dist/connected-storage-panel.js +33 -0
- package/dist/index.js +4 -0
- package/dist/inspector.js +122 -0
- package/dist/panel-source.js +1 -0
- package/dist/panel-view.js +136 -0
- package/dist/panel.js +7 -0
- package/dist/sid-registry.js +45 -0
- package/dist/storage-panel-view.js +127 -0
- package/dist/storage-reducer.js +25 -0
- package/dist/storage-source.js +1 -0
- package/dist/storage-types.js +4 -0
- package/dist/types.js +5 -0
- package/dist/use-source-wiring.js +52 -0
- package/dist/wxml-extract.js +381 -0
- package/package.json +72 -0
- package/src/connected-panel.test.tsx +224 -0
- package/src/connected-panel.tsx +53 -0
- package/src/connected-storage-panel.test.tsx +289 -0
- package/src/connected-storage-panel.tsx +62 -0
- package/src/index.ts +16 -0
- package/src/inspector.test.ts +282 -0
- package/src/inspector.ts +144 -0
- package/src/panel-source.ts +20 -0
- package/src/panel-view.tsx +355 -0
- package/src/panel.tsx +7 -0
- package/src/sid-registry.ts +45 -0
- package/src/storage-panel-view.tsx +252 -0
- package/src/storage-reducer.test.ts +80 -0
- package/src/storage-reducer.ts +26 -0
- package/src/storage-source.ts +28 -0
- package/src/storage-types.ts +26 -0
- package/src/types.ts +35 -0
- package/src/use-source-wiring.ts +69 -0
- package/src/wxml-extract.ts +366 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { SYNTHETIC_SID_PREFIX } from './sid-registry.js'
|
|
3
|
+
import { createWxmlInspector, type WxmlInspector } from './inspector.js'
|
|
4
|
+
import type { WxmlNode } from './types.js'
|
|
5
|
+
|
|
6
|
+
/** Minimal fake Vue instance leaf: a mounted element with no children of its own. */
|
|
7
|
+
function makeLeafInstance(tagName: string, props: Record<string, unknown>, el: HTMLElement) {
|
|
8
|
+
return { type: { __tagName: tagName }, props, subTree: { el, children: [] } }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function wait(ms: number): Promise<void> {
|
|
12
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('createWxmlInspector', () => {
|
|
16
|
+
let inspector: WxmlInspector
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
inspector = createWxmlInspector({ document })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
inspector.dispose()
|
|
24
|
+
delete (document.body as unknown as Record<string, unknown>).__vue_app__
|
|
25
|
+
document.body.innerHTML = ''
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('getWxml', () => {
|
|
29
|
+
it('returns null when body has no mounted Vue app', () => {
|
|
30
|
+
expect(inspector.getWxml()).toBeNull()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('walks _instance into a WxmlNode tree with synthetic sids on nodes and their children', () => {
|
|
34
|
+
// Mounted-DOM fixture: the sid registry deliberately refuses to resolve
|
|
35
|
+
// disconnected elements, and a walked Vue subtree is always mounted.
|
|
36
|
+
const rootEl = document.createElement('div')
|
|
37
|
+
const childEl = document.createElement('span')
|
|
38
|
+
rootEl.appendChild(childEl)
|
|
39
|
+
document.body.appendChild(rootEl)
|
|
40
|
+
const child = makeLeafInstance('text', {}, childEl)
|
|
41
|
+
const root = {
|
|
42
|
+
type: { __tagName: 'view' },
|
|
43
|
+
props: { class: 'user-cls' },
|
|
44
|
+
subTree: { el: rootEl, children: [{ component: child }] },
|
|
45
|
+
}
|
|
46
|
+
;(document.body as unknown as Record<string, unknown>).__vue_app__ = { _instance: root }
|
|
47
|
+
|
|
48
|
+
const tree = inspector.getWxml()
|
|
49
|
+
|
|
50
|
+
expect(tree?.tagName).toBe('view')
|
|
51
|
+
expect(tree?.attrs).toEqual({ class: 'user-cls' })
|
|
52
|
+
expect(tree?.sid).toMatch(new RegExp(`^${SYNTHETIC_SID_PREFIX}`))
|
|
53
|
+
expect(tree?.children).toHaveLength(1)
|
|
54
|
+
expect(tree?.children[0]).toMatchObject({ tagName: 'text', attrs: {}, children: [] })
|
|
55
|
+
expect(tree?.children[0]?.sid).toMatch(new RegExp(`^${SYNTHETIC_SID_PREFIX}`))
|
|
56
|
+
// The minted sids must round-trip back to the exact elements they came from.
|
|
57
|
+
expect(inspector.elementFor(tree!.sid!)).toBe(rootEl)
|
|
58
|
+
expect(inspector.elementFor(tree!.children[0]!.sid!)).toBe(childEl)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('uses an element pre-existing data-sid attribute instead of minting a synthetic one', () => {
|
|
62
|
+
const el = document.createElement('div')
|
|
63
|
+
el.setAttribute('data-sid', 'real-sid-7')
|
|
64
|
+
const root = makeLeafInstance('view', {}, el)
|
|
65
|
+
;(document.body as unknown as Record<string, unknown>).__vue_app__ = { _instance: root }
|
|
66
|
+
|
|
67
|
+
expect(inspector.getWxml()?.sid).toBe('real-sid-7')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('falls back to _container._vnode.component when _instance is absent', () => {
|
|
71
|
+
const el = document.createElement('div')
|
|
72
|
+
const inst = makeLeafInstance('view', {}, el)
|
|
73
|
+
;(document.body as unknown as Record<string, unknown>).__vue_app__ = {
|
|
74
|
+
_container: { _vnode: { component: inst } },
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
expect(inspector.getWxml()?.tagName).toBe('view')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('wraps a multi-root walk result in a synthetic #fragment node', () => {
|
|
81
|
+
const elA = document.createElement('div')
|
|
82
|
+
const elB = document.createElement('div')
|
|
83
|
+
const childA = makeLeafInstance('view', {}, elA)
|
|
84
|
+
const childB = makeLeafInstance('text', {}, elB)
|
|
85
|
+
// A nameless top-level instance (no `type`) is transparent, so walking it
|
|
86
|
+
// surfaces its two component children as separate roots.
|
|
87
|
+
const root = { subTree: { children: [{ component: childA }, { component: childB }] } }
|
|
88
|
+
;(document.body as unknown as Record<string, unknown>).__vue_app__ = { _instance: root }
|
|
89
|
+
|
|
90
|
+
const tree = inspector.getWxml()
|
|
91
|
+
|
|
92
|
+
expect(tree?.tagName).toBe('#fragment')
|
|
93
|
+
expect(tree?.children).toHaveLength(2)
|
|
94
|
+
expect(tree?.children.map((c: WxmlNode) => c.tagName)).toEqual(['view', 'text'])
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('returns null when the walked tree has no content', () => {
|
|
98
|
+
const root = { subTree: { children: [] } }
|
|
99
|
+
;(document.body as unknown as Record<string, unknown>).__vue_app__ = { _instance: root }
|
|
100
|
+
|
|
101
|
+
expect(inspector.getWxml()).toBeNull()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('defaults to globalThis.document when no document option is given', () => {
|
|
105
|
+
const defaultInspector = createWxmlInspector()
|
|
106
|
+
expect(() => defaultInspector.getWxml()).not.toThrow()
|
|
107
|
+
defaultInspector.dispose()
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('highlightElement', () => {
|
|
112
|
+
it('measures rect and the requested style subset without mutating the element', () => {
|
|
113
|
+
const el = document.createElement('div')
|
|
114
|
+
el.setAttribute('data-sid', 'measured-1')
|
|
115
|
+
el.style.display = 'flex'
|
|
116
|
+
el.style.position = 'absolute'
|
|
117
|
+
el.style.boxSizing = 'border-box'
|
|
118
|
+
el.style.margin = '4px'
|
|
119
|
+
el.style.padding = '2px'
|
|
120
|
+
el.style.color = 'rgb(255, 0, 0)'
|
|
121
|
+
el.style.backgroundColor = 'rgb(0, 0, 255)'
|
|
122
|
+
el.style.fontSize = '16px'
|
|
123
|
+
document.body.appendChild(el)
|
|
124
|
+
|
|
125
|
+
const fakeRect = { x: 10, y: 20, width: 100, height: 50, top: 20, left: 10, right: 110, bottom: 70, toJSON() {} }
|
|
126
|
+
el.getBoundingClientRect = vi.fn(() => fakeRect as unknown as DOMRect)
|
|
127
|
+
|
|
128
|
+
const beforeHtml = document.body.innerHTML
|
|
129
|
+
|
|
130
|
+
const inspection = inspector.highlightElement('measured-1')
|
|
131
|
+
|
|
132
|
+
expect(inspection?.sid).toBe('measured-1')
|
|
133
|
+
expect(inspection?.rect).toEqual({ x: 10, y: 20, width: 100, height: 50 })
|
|
134
|
+
expect(inspection?.style).toEqual({
|
|
135
|
+
display: 'flex',
|
|
136
|
+
position: 'absolute',
|
|
137
|
+
boxSizing: 'border-box',
|
|
138
|
+
margin: '4px',
|
|
139
|
+
padding: '2px',
|
|
140
|
+
color: 'rgb(255, 0, 0)',
|
|
141
|
+
backgroundColor: 'rgb(0, 0, 255)',
|
|
142
|
+
fontSize: '16px',
|
|
143
|
+
})
|
|
144
|
+
// A measurement must be read-only: no attribute/overlay side effects.
|
|
145
|
+
expect(document.body.innerHTML).toBe(beforeHtml)
|
|
146
|
+
expect(document.body.children).toHaveLength(1)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('returns null for an sid with no matching element', () => {
|
|
150
|
+
expect(inspector.highlightElement('does-not-exist')).toBeNull()
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('elementFor', () => {
|
|
155
|
+
it('resolves a data-sid attribute to its live element', () => {
|
|
156
|
+
const el = document.createElement('div')
|
|
157
|
+
el.setAttribute('data-sid', 'find-me')
|
|
158
|
+
document.body.appendChild(el)
|
|
159
|
+
|
|
160
|
+
expect(inspector.elementFor('find-me')).toBe(el)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('returns null for an sid with no registered or matching element', () => {
|
|
164
|
+
expect(inspector.elementFor('nope')).toBeNull()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('setObserving', () => {
|
|
169
|
+
it('merges a burst of DOM mutations into a single debounced callback', async () => {
|
|
170
|
+
const container = document.createElement('div')
|
|
171
|
+
document.body.appendChild(container)
|
|
172
|
+
const onMutated = vi.fn()
|
|
173
|
+
const observing = createWxmlInspector({ document, onMutated, debounceMs: 20 })
|
|
174
|
+
|
|
175
|
+
observing.setObserving(true)
|
|
176
|
+
container.setAttribute('data-x', '1')
|
|
177
|
+
container.setAttribute('data-x', '2')
|
|
178
|
+
container.appendChild(document.createElement('span'))
|
|
179
|
+
|
|
180
|
+
await wait(80)
|
|
181
|
+
|
|
182
|
+
expect(onMutated).toHaveBeenCalledTimes(1)
|
|
183
|
+
observing.dispose()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('does not call back before observing has been turned on', async () => {
|
|
187
|
+
const container = document.createElement('div')
|
|
188
|
+
document.body.appendChild(container)
|
|
189
|
+
const onMutated = vi.fn()
|
|
190
|
+
const observing = createWxmlInspector({ document, onMutated, debounceMs: 20 })
|
|
191
|
+
|
|
192
|
+
container.setAttribute('data-x', '1')
|
|
193
|
+
await wait(80)
|
|
194
|
+
|
|
195
|
+
expect(onMutated).not.toHaveBeenCalled()
|
|
196
|
+
observing.dispose()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('stops delivering callbacks once observing is turned off', async () => {
|
|
200
|
+
const container = document.createElement('div')
|
|
201
|
+
document.body.appendChild(container)
|
|
202
|
+
const onMutated = vi.fn()
|
|
203
|
+
const observing = createWxmlInspector({ document, onMutated, debounceMs: 20 })
|
|
204
|
+
|
|
205
|
+
observing.setObserving(true)
|
|
206
|
+
observing.setObserving(false)
|
|
207
|
+
container.setAttribute('data-x', '1')
|
|
208
|
+
await wait(80)
|
|
209
|
+
|
|
210
|
+
expect(onMutated).not.toHaveBeenCalled()
|
|
211
|
+
observing.dispose()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('cancels a debounced callback already pending when observing turns off before it fires', async () => {
|
|
215
|
+
const container = document.createElement('div')
|
|
216
|
+
document.body.appendChild(container)
|
|
217
|
+
const onMutated = vi.fn()
|
|
218
|
+
const observing = createWxmlInspector({ document, onMutated, debounceMs: 30 })
|
|
219
|
+
|
|
220
|
+
observing.setObserving(true)
|
|
221
|
+
container.setAttribute('data-x', '1')
|
|
222
|
+
await wait(5) // mutation observed and debounce timer armed, well short of debounceMs
|
|
223
|
+
observing.setObserving(false)
|
|
224
|
+
await wait(80)
|
|
225
|
+
|
|
226
|
+
expect(onMutated).not.toHaveBeenCalled()
|
|
227
|
+
observing.dispose()
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('does not double the callback when turned on twice in a row', async () => {
|
|
231
|
+
const container = document.createElement('div')
|
|
232
|
+
document.body.appendChild(container)
|
|
233
|
+
const onMutated = vi.fn()
|
|
234
|
+
const observing = createWxmlInspector({ document, onMutated, debounceMs: 20 })
|
|
235
|
+
|
|
236
|
+
observing.setObserving(true)
|
|
237
|
+
observing.setObserving(true)
|
|
238
|
+
container.setAttribute('data-x', '1')
|
|
239
|
+
await wait(80)
|
|
240
|
+
|
|
241
|
+
expect(onMutated).toHaveBeenCalledTimes(1)
|
|
242
|
+
observing.dispose()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('resumes observing after being turned off and back on', async () => {
|
|
246
|
+
const container = document.createElement('div')
|
|
247
|
+
document.body.appendChild(container)
|
|
248
|
+
const onMutated = vi.fn()
|
|
249
|
+
const observing = createWxmlInspector({ document, onMutated, debounceMs: 20 })
|
|
250
|
+
|
|
251
|
+
observing.setObserving(true)
|
|
252
|
+
observing.setObserving(false)
|
|
253
|
+
observing.setObserving(true)
|
|
254
|
+
container.setAttribute('data-x', '1')
|
|
255
|
+
await wait(80)
|
|
256
|
+
|
|
257
|
+
expect(onMutated).toHaveBeenCalledTimes(1)
|
|
258
|
+
observing.dispose()
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
describe('dispose', () => {
|
|
263
|
+
it('produces no further callbacks after dispose, even for mutations already pending', async () => {
|
|
264
|
+
const container = document.createElement('div')
|
|
265
|
+
document.body.appendChild(container)
|
|
266
|
+
const onMutated = vi.fn()
|
|
267
|
+
const observing = createWxmlInspector({ document, onMutated, debounceMs: 20 })
|
|
268
|
+
|
|
269
|
+
observing.setObserving(true)
|
|
270
|
+
container.setAttribute('data-x', '1')
|
|
271
|
+
observing.dispose()
|
|
272
|
+
await wait(80)
|
|
273
|
+
|
|
274
|
+
expect(onMutated).not.toHaveBeenCalled()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('is safe to call before observing was ever turned on', () => {
|
|
278
|
+
const observing = createWxmlInspector({ document })
|
|
279
|
+
expect(() => observing.dispose()).not.toThrow()
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
})
|
package/src/inspector.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Host-agnostic WXML inspector over a render-layer document. It owns the
|
|
2
|
+
// three read paths every host needs — Vue-tree walk (getWxml), sid → element
|
|
3
|
+
// resolution (elementFor), element measurement (highlightElement) — plus the
|
|
4
|
+
// visibility-gated DOM observer that debounces a setData burst into a single
|
|
5
|
+
// onMutated callback. Hosts wire the callback to their own transport
|
|
6
|
+
// (Electron bridge message, iframe postMessage, …) and draw any visual
|
|
7
|
+
// highlight themselves: every method here is strictly read-only on the page.
|
|
8
|
+
import { findElementBySid } from './sid-registry.js'
|
|
9
|
+
import { walkInstance, type ComponentInstance } from './wxml-extract.js'
|
|
10
|
+
import type { ElementInspection, WxmlNode } from './types.js'
|
|
11
|
+
|
|
12
|
+
export interface WxmlInspectorOptions {
|
|
13
|
+
/** Document hosting the mounted Vue app. Defaults to `globalThis.document`. */
|
|
14
|
+
document?: Document
|
|
15
|
+
/** Called (debounced) on every DOM change while observing is on. */
|
|
16
|
+
onMutated?: () => void
|
|
17
|
+
/** Debounce window for mutation bursts. Defaults to 200ms. */
|
|
18
|
+
debounceMs?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WxmlInspector {
|
|
22
|
+
/** Walk `document.body.__vue_app__` into a WxmlNode tree. Multiple roots are
|
|
23
|
+
* wrapped in a synthetic `#fragment`; no app / empty tree → null. */
|
|
24
|
+
getWxml(): WxmlNode | null
|
|
25
|
+
/** Measure the element with `sid`: bounding rect + the computed-style subset
|
|
26
|
+
* the panel footer shows. Read-only — drawing a highlight is the host's job. */
|
|
27
|
+
highlightElement(sid: string): ElementInspection | null
|
|
28
|
+
/** Resolve the sid to its live DOM element (synthetic registry or data-sid). */
|
|
29
|
+
elementFor(sid: string): HTMLElement | null
|
|
30
|
+
/** Start/stop watching the page DOM. Idempotent in both directions; turning
|
|
31
|
+
* off cancels a pending debounced callback. */
|
|
32
|
+
setObserving(on: boolean): void
|
|
33
|
+
/** Stop observing; the instance produces no further callbacks. */
|
|
34
|
+
dispose(): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Coalesce a burst of setData-driven mutations into one notify per frame-ish. */
|
|
38
|
+
const DEFAULT_DEBOUNCE_MS = 200
|
|
39
|
+
|
|
40
|
+
export function createWxmlInspector(options: WxmlInspectorOptions = {}): WxmlInspector {
|
|
41
|
+
const doc = options.document ?? globalThis.document
|
|
42
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS
|
|
43
|
+
const onMutated = options.onMutated
|
|
44
|
+
|
|
45
|
+
const getVueApp = (): ComponentInstance | null => {
|
|
46
|
+
try {
|
|
47
|
+
const body = doc?.body as unknown as Record<string, unknown> | null
|
|
48
|
+
const app = body?.__vue_app__ as Record<string, unknown> | undefined
|
|
49
|
+
if (!app) return null
|
|
50
|
+
if (app._instance) return app._instance as ComponentInstance
|
|
51
|
+
const container = app._container as Record<string, unknown> | undefined
|
|
52
|
+
const vnode = container?._vnode as Record<string, unknown> | undefined
|
|
53
|
+
return (vnode?.component as ComponentInstance | null) ?? null
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const getWxml = (): WxmlNode | null => {
|
|
60
|
+
const instance = getVueApp()
|
|
61
|
+
if (!instance) return null
|
|
62
|
+
const tree = walkInstance(instance, 0)
|
|
63
|
+
if (!tree) return null
|
|
64
|
+
return Array.isArray(tree)
|
|
65
|
+
? { tagName: '#fragment', attrs: {}, children: tree }
|
|
66
|
+
: tree
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const elementFor = (sid: string): HTMLElement | null => {
|
|
70
|
+
if (!sid) return null
|
|
71
|
+
return findElementBySid(doc, sid)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const highlightElement = (sid: string): ElementInspection | null => {
|
|
75
|
+
const el = elementFor(sid)
|
|
76
|
+
if (!el) return null
|
|
77
|
+
const rect = el.getBoundingClientRect()
|
|
78
|
+
const style = el.ownerDocument.defaultView?.getComputedStyle(el)
|
|
79
|
+
if (!style) return null
|
|
80
|
+
return {
|
|
81
|
+
sid,
|
|
82
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
83
|
+
style: {
|
|
84
|
+
display: style.display,
|
|
85
|
+
position: style.position,
|
|
86
|
+
boxSizing: style.boxSizing,
|
|
87
|
+
margin: style.margin,
|
|
88
|
+
padding: style.padding,
|
|
89
|
+
color: style.color,
|
|
90
|
+
backgroundColor: style.backgroundColor,
|
|
91
|
+
fontSize: style.fontSize,
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// getWxml/highlightElement are read-only, so the observer never re-triggers
|
|
97
|
+
// itself. Debounced so a setData burst coalesces into a single callback.
|
|
98
|
+
let observer: MutationObserver | null = null
|
|
99
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
100
|
+
|
|
101
|
+
const notifyMutated = (): void => {
|
|
102
|
+
debounceTimer = null
|
|
103
|
+
onMutated?.()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const setObserving = (on: boolean): void => {
|
|
107
|
+
if (on) {
|
|
108
|
+
if (observer) return
|
|
109
|
+
observer = new MutationObserver(() => {
|
|
110
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer)
|
|
111
|
+
debounceTimer = setTimeout(notifyMutated, debounceMs)
|
|
112
|
+
})
|
|
113
|
+
// The host may enable observing before the page DOM is up. Observing a
|
|
114
|
+
// null body throws, so defer to DOMContentLoaded when needed — but bail
|
|
115
|
+
// if observing was turned off again before the body arrived.
|
|
116
|
+
const begin = (): void => {
|
|
117
|
+
if (!observer || !doc.body) return
|
|
118
|
+
observer.observe(doc.body, {
|
|
119
|
+
childList: true,
|
|
120
|
+
subtree: true,
|
|
121
|
+
attributes: true,
|
|
122
|
+
characterData: true,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
if (doc.body) begin()
|
|
126
|
+
else doc.addEventListener('DOMContentLoaded', begin, { once: true })
|
|
127
|
+
} else {
|
|
128
|
+
if (debounceTimer !== null) {
|
|
129
|
+
clearTimeout(debounceTimer)
|
|
130
|
+
debounceTimer = null
|
|
131
|
+
}
|
|
132
|
+
observer?.disconnect()
|
|
133
|
+
observer = null
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
getWxml,
|
|
139
|
+
highlightElement,
|
|
140
|
+
elementFor,
|
|
141
|
+
setObserving,
|
|
142
|
+
dispose: () => setObserving(false),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The host-transport contract behind the WXML panel: the panel's data wiring
|
|
2
|
+
// (seed, live push, visibility gating, element inspection) is written ONCE
|
|
3
|
+
// against this interface (see ConnectedWxmlPanel); each host only implements
|
|
4
|
+
// how the five operations travel — Electron IPC channels, iframe postMessage,
|
|
5
|
+
// or anything else.
|
|
6
|
+
import type { ElementInspection, WxmlNode } from './types.js'
|
|
7
|
+
|
|
8
|
+
export interface WxmlPanelSource {
|
|
9
|
+
/** Fetch the current tree snapshot (seed / manual refresh). */
|
|
10
|
+
getSnapshot(): Promise<WxmlNode | null>
|
|
11
|
+
/** Live tree pushes; returns an unsubscribe function. */
|
|
12
|
+
subscribe(onTree: (tree: WxmlNode | null) => void): () => void
|
|
13
|
+
/** Visibility gate: the producer only observes the page DOM (and pays the
|
|
14
|
+
* Vue-tree walk) while some panel is visible. */
|
|
15
|
+
setActive(on: boolean): void
|
|
16
|
+
/** Measure the element with `sid`; the producer draws its own highlight. */
|
|
17
|
+
inspect(sid: string): Promise<ElementInspection | null>
|
|
18
|
+
/** Drop the current measurement highlight. */
|
|
19
|
+
clearInspection(): void | Promise<void>
|
|
20
|
+
}
|