glass-easel-devtools-agent 0.9.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.
@@ -0,0 +1,899 @@
1
+ import * as glassEasel from 'glass-easel'
2
+ import { type protocol, type Connection } from '.'
3
+ import {
4
+ type GlassEaselVar,
5
+ glassEaselVarToString,
6
+ toGlassEaselVar,
7
+ type NodeId,
8
+ type dom,
9
+ } from './protocol'
10
+ import { GlassEaselNodeType, glassEaselNodeTypeToCDP } from './protocol/dom'
11
+ import * as backendUtils from './backend'
12
+ import { warn } from './utils'
13
+
14
+ export type NodeMeta = {
15
+ node: glassEasel.Node
16
+ nodeId: NodeId
17
+ childNodesSent: boolean
18
+ observer: glassEasel.MutationObserver
19
+ }
20
+
21
+ export const enum StaticNodeName {
22
+ Document = '#document',
23
+ TextNode = '#text',
24
+ ShadowRoot = '#shadow-root',
25
+ Slot = 'SLOT',
26
+ Unknown = 'UNKNOWN',
27
+ }
28
+
29
+ const getNodeType = (node: glassEasel.Node): GlassEaselNodeType => {
30
+ if (node.asTextNode()) return GlassEaselNodeType.TextNode
31
+ if (node.asNativeNode()) return GlassEaselNodeType.NativeNode
32
+ if (node.asVirtualNode()) return GlassEaselNodeType.VirtualNode
33
+ if (node.asGeneralComponent()) return GlassEaselNodeType.Component
34
+ return GlassEaselNodeType.Unknown
35
+ }
36
+
37
+ const getNodeName = (
38
+ node: glassEasel.Node,
39
+ nodeType: GlassEaselNodeType,
40
+ local: boolean,
41
+ ): string => {
42
+ if (nodeType === GlassEaselNodeType.TextNode) return StaticNodeName.TextNode
43
+ if (nodeType === GlassEaselNodeType.NativeNode) return node.asNativeNode()!.is
44
+ if (nodeType === GlassEaselNodeType.Component) {
45
+ const comp = node.asGeneralComponent()!
46
+ return local ? comp.is : comp.tagName
47
+ }
48
+ if (nodeType === GlassEaselNodeType.VirtualNode) return node.asVirtualNode()!.is
49
+ return StaticNodeName.Unknown
50
+ }
51
+
52
+ export class MountPointsManager {
53
+ private conn: Connection
54
+ private nodeIdMap = new WeakMap<glassEasel.Node, NodeId>()
55
+ private activeNodes = Object.create(null) as Record<NodeId, NodeMeta>
56
+ private activeBackendNodes = Object.create(null) as Record<NodeId, WeakRef<glassEasel.Node>>
57
+ readonly documentNodeId = 1
58
+ private nodeIdInc = 2
59
+ private mountPoints: { nodeMeta: NodeMeta; env: glassEasel.MountPointEnv }[] = []
60
+ private selectedNodeId = 0
61
+
62
+ constructor(conn: Connection) {
63
+ this.conn = conn
64
+ this.init()
65
+ }
66
+
67
+ init() {
68
+ this.conn.setRequestHandler('DOM.describeNode', async (args) => {
69
+ let node: dom.Node
70
+ if (args.nodeId !== undefined) {
71
+ const nodeMeta = this.queryActiveNode(args.nodeId)
72
+ node = this.collectNodeDetails(nodeMeta, args.depth ?? 0, false)
73
+ } else if (args.backendNodeId !== undefined) {
74
+ const nodeMeta = this.activateBackendNodeIfNeeded(args.backendNodeId)
75
+ if (!nodeMeta) throw new Error('no such node found')
76
+ node = this.collectNodeDetails(nodeMeta, args.depth ?? 0, false)
77
+ } else {
78
+ throw new Error('missing (backend) node id')
79
+ }
80
+ return { node }
81
+ })
82
+
83
+ this.conn.setRequestHandler('DOM.getDocument', async ({ depth }) => {
84
+ let children: dom.Node[] | undefined
85
+ if (depth && depth > 1) {
86
+ children = this.mountPoints.map(({ nodeMeta }) =>
87
+ this.collectNodeDetails(nodeMeta, depth - 1, true),
88
+ )
89
+ }
90
+ const ty = GlassEaselNodeType.Unknown
91
+ const root: dom.Node = {
92
+ backendNodeId: this.documentNodeId,
93
+ nodeType: glassEaselNodeTypeToCDP(ty),
94
+ glassEaselNodeType: ty,
95
+ nodeName: StaticNodeName.Document,
96
+ virtual: true,
97
+ inheritSlots: false,
98
+ nodeId: this.documentNodeId,
99
+ localName: StaticNodeName.Document,
100
+ nodeValue: '',
101
+ attributes: [],
102
+ glassEaselAttributeCount: 0,
103
+ children,
104
+ }
105
+ return { root }
106
+ })
107
+
108
+ this.conn.setRequestHandler('DOM.setInspectedNode', async ({ nodeId }) => {
109
+ const { node } = this.queryActiveNode(nodeId)
110
+ const elem = node.asElement()
111
+ if (!elem) return
112
+ this.selectedNodeId = nodeId
113
+ })
114
+
115
+ this.conn.setRequestHandler('DOM.requestChildNodes', async ({ nodeId }) => {
116
+ const nodeMeta = this.queryActiveNode(nodeId)
117
+ this.sendChildNodes(nodeMeta)
118
+ })
119
+
120
+ this.conn.setRequestHandler('DOM.getGlassEaselAttributes', async ({ nodeId }) => {
121
+ const { node } = this.queryActiveNode(nodeId)
122
+ const elem = node.asElement()
123
+ if (!elem) {
124
+ return {
125
+ glassEaselNodeType: GlassEaselNodeType.TextNode,
126
+ virtual: false,
127
+ is: '',
128
+ id: '',
129
+ class: '',
130
+ slot: '',
131
+ slotName: undefined,
132
+ slotValues: undefined,
133
+ eventBindings: [],
134
+ dataset: [],
135
+ marks: [],
136
+ }
137
+ }
138
+
139
+ // element types
140
+ const comp = elem.asGeneralComponent()
141
+ const nativeNode = elem.asNativeNode()
142
+ const virtualNode = elem.asVirtualNode()
143
+ let glassEaselNodeType = GlassEaselNodeType.Unknown
144
+ if (comp) glassEaselNodeType = GlassEaselNodeType.Component
145
+ if (nativeNode) glassEaselNodeType = GlassEaselNodeType.NativeNode
146
+ if (virtualNode) glassEaselNodeType = GlassEaselNodeType.VirtualNode
147
+
148
+ // collect basic attributes
149
+ const virtual = elem.isVirtual()
150
+ let is = ''
151
+ if (comp) is = comp.is
152
+ if (nativeNode) is = nativeNode.is
153
+ if (virtualNode) is = virtualNode.is
154
+ const id = elem.id
155
+ const nodeClass = elem.class
156
+ const slot = elem.slot
157
+ let slotName
158
+ const maybeSlotName = Reflect.get(elem, '_$slotName') as unknown
159
+ if (typeof maybeSlotName === 'string') slotName = maybeSlotName
160
+ let slotValues: { name: string; value: GlassEaselVar }[] | undefined
161
+ const maybeSlotValues = Reflect.get(elem, '_$slotValues') as unknown
162
+ if (typeof maybeSlotValues === 'object' && maybeSlotValues !== null) {
163
+ slotValues = []
164
+ Object.entries(maybeSlotValues).forEach(([name, value]) => {
165
+ slotValues!.push({ name, value: toGlassEaselVar(value) })
166
+ })
167
+ }
168
+
169
+ // collect event bindings
170
+ const eventBindings: {
171
+ name: string
172
+ capture: boolean
173
+ count: number
174
+ hasCatch: boolean
175
+ hasMutBind: boolean
176
+ }[] = []
177
+ type EventPoint = {
178
+ mutCount?: number
179
+ finalCount?: number
180
+ funcArr?: { _$arr?: { f: unknown }[] | null }
181
+ }
182
+ const maybeEventTarget = Reflect.get(elem, '_$eventTarget') as
183
+ | {
184
+ listeners?: { [name: string]: EventPoint }
185
+ captureListeners?: { [name: string]: EventPoint }
186
+ }
187
+ | null
188
+ | undefined
189
+ if (typeof maybeEventTarget === 'object' && maybeEventTarget !== null) {
190
+ const processListeners = (capture: boolean, listeners?: { [name: string]: EventPoint }) => {
191
+ if (typeof listeners === 'object' && listeners !== null) {
192
+ Object.entries(listeners).forEach(([name, value]) => {
193
+ const count = value?.funcArr?._$arr?.length ?? 0
194
+ if (count > 0) {
195
+ const hasCatch = (value?.finalCount ?? 0) > 0
196
+ const hasMutBind = (value?.finalCount ?? 0) > 0
197
+ eventBindings.push({ name, capture, count, hasCatch, hasMutBind })
198
+ }
199
+ })
200
+ }
201
+ }
202
+ processListeners(true, maybeEventTarget.captureListeners)
203
+ processListeners(false, maybeEventTarget.listeners)
204
+ }
205
+
206
+ // collect attributes, properties, and external classes
207
+ let normalAttributes: { name: string; value: GlassEaselVar }[] | undefined
208
+ let properties: { name: string; value: GlassEaselVar }[] | undefined
209
+ let externalClasses: { name: string; value: string }[] | undefined
210
+ if (nativeNode) {
211
+ normalAttributes = []
212
+ elem.attributes.forEach(({ name, value }) => {
213
+ normalAttributes!.push({ name, value: toGlassEaselVar(value) })
214
+ })
215
+ }
216
+ if (comp) {
217
+ properties = []
218
+ const beh = comp.getComponentDefinition().behavior
219
+ const names = beh.listProperties()
220
+ names.forEach((name) => {
221
+ properties!.push({ name, value: toGlassEaselVar(comp.data[name]) })
222
+ })
223
+ const ec = comp.getExternalClasses()
224
+ if (ec) {
225
+ externalClasses = Object.entries(ec).map(([name, value]) => ({
226
+ name,
227
+ value: value?.join(' ') ?? '',
228
+ }))
229
+ }
230
+ }
231
+
232
+ // collect dataset
233
+ const dataset: { name: string; value: GlassEaselVar }[] = []
234
+ Object.entries(elem.dataset ?? {}).forEach(([name, value]) => {
235
+ dataset.push({ name, value: toGlassEaselVar(value) })
236
+ })
237
+ const marks: { name: string; value: GlassEaselVar }[] = []
238
+ const maybeMarks = Reflect.get(elem, '_$marks') as { [key: string]: unknown } | undefined
239
+ Object.entries(maybeMarks ?? {}).forEach(([name, value]) => {
240
+ marks.push({ name, value: toGlassEaselVar(value) })
241
+ })
242
+
243
+ return {
244
+ glassEaselNodeType,
245
+ virtual,
246
+ is,
247
+ id,
248
+ class: nodeClass,
249
+ slot,
250
+ slotName,
251
+ slotValues,
252
+ eventBindings,
253
+ normalAttributes,
254
+ properties,
255
+ externalClasses,
256
+ dataset,
257
+ marks,
258
+ }
259
+ })
260
+
261
+ this.conn.setRequestHandler('DOM.getGlassEaselComposedChildren', async ({ nodeId }) => {
262
+ const { node } = this.queryActiveNode(nodeId)
263
+ const elem = node.asElement()
264
+ if (!elem) return { nodes: [] }
265
+ const nodes: dom.Node[] = []
266
+ elem.forEachComposedChild((child) => {
267
+ const nodeMeta = this.activateNode(child)
268
+ nodes.push(this.collectNodeDetails(nodeMeta, 0, false))
269
+ })
270
+ return { nodes }
271
+ })
272
+
273
+ this.conn.setRequestHandler(
274
+ 'DOM.pushNodesByBackendIdsToFrontend',
275
+ async ({ backendNodeIds }) => {
276
+ backendNodeIds.forEach((backendNodeId) => {
277
+ this.activateBackendNodeIfNeeded(backendNodeId)
278
+ })
279
+ return { nodeIds: backendNodeIds }
280
+ },
281
+ )
282
+
283
+ this.conn.setRequestHandler('DOM.getBoxModel', async (args) => {
284
+ let node: glassEasel.Node | null
285
+ if ('nodeId' in args) {
286
+ node = this.queryActiveNode(args.nodeId).node
287
+ } else {
288
+ node = this.getMaybeBackendNode(args.backendNodeId)
289
+ }
290
+ const ctx = node?.getBackendContext()
291
+ const elem = node?.getBackendElement()
292
+ if (!ctx || !elem) {
293
+ throw new Error('no such backend node found')
294
+ }
295
+ const { margin, border, padding, content } = await backendUtils.getBoxModel(ctx, elem)
296
+ const toQuad = (x: backendUtils.BoundingClientRect) => {
297
+ const lt = [x.left, x.top]
298
+ const rt = [x.left + x.width, x.top]
299
+ const lb = [x.left, x.top + x.height]
300
+ const rb = [x.left + x.width, x.top + x.height]
301
+ return [...lt, ...rt, ...rb, ...lb] as protocol.dom.Quad
302
+ }
303
+ return {
304
+ margin: toQuad(margin),
305
+ border: toQuad(border),
306
+ padding: toQuad(padding),
307
+ content: toQuad(content),
308
+ width: border.width,
309
+ height: border.height,
310
+ }
311
+ })
312
+
313
+ this.conn.setRequestHandler('DOM.useGlassEaselElementInConsole', async ({ nodeId }) => {
314
+ const { node } = this.queryActiveNode(nodeId)
315
+ const varName = this.useInConsole(node)
316
+ return { varName }
317
+ })
318
+
319
+ this.conn.setRequestHandler(
320
+ 'DOM.useGlassEaselAttributeInConsole',
321
+ async ({ nodeId, attribute }) => {
322
+ const { node } = this.queryActiveNode(nodeId)
323
+ let attr: unknown
324
+ const elem = node.asElement()
325
+ if (!elem) throw new Error('not an element')
326
+ if (attribute.startsWith('data:')) {
327
+ attr = elem.dataset[attribute.slice(5)]
328
+ } else if (attribute.startsWith('mark:')) {
329
+ const maybeMarks = Reflect.get(elem, '_$marks') as { [key: string]: unknown } | undefined
330
+ attr = maybeMarks?.[attribute.slice(5)]
331
+ } else {
332
+ const comp = elem.asGeneralComponent()
333
+ if (comp) {
334
+ attr = comp.data[attribute]
335
+ } else {
336
+ attr = elem.attributes.find(({ name }) => name === attribute)?.value
337
+ }
338
+ }
339
+ const varName = this.useInConsole(attr)
340
+ return { varName }
341
+ },
342
+ )
343
+
344
+ this.conn.setRequestHandler('CSS.getComputedStyleForNode', async ({ nodeId }) => {
345
+ const { node } = this.queryActiveNode(nodeId)
346
+ const ctx = node?.getBackendContext()
347
+ const elem = node?.getBackendElement()
348
+ if (!ctx || !elem) {
349
+ throw new Error('no such backend node found')
350
+ }
351
+ const computedStyle = (await backendUtils.getAllComputedStyles(ctx, elem)).properties
352
+ return { computedStyle }
353
+ })
354
+
355
+ this.conn.setRequestHandler('CSS.getMatchedStylesForNode', async ({ nodeId }) => {
356
+ const { node } = this.queryActiveNode(nodeId)
357
+ const ctx = node?.getBackendContext()
358
+ const elem = node?.getBackendElement()
359
+ if (!ctx || !elem) {
360
+ throw new Error('no such backend node found')
361
+ }
362
+ const { inline, inlineText, rules, crossOriginFailing } = await backendUtils.getMatchedRules(
363
+ ctx,
364
+ elem,
365
+ )
366
+ const inlineStyle = { cssProperties: inline, cssText: inlineText }
367
+ const matchedCSSRules = rules.map((rule) => ({
368
+ rule: {
369
+ selectorList: { selectors: [{ text: rule.selector }], text: rule.selector },
370
+ style: { cssProperties: rule.properties, cssText: rule.propertyText },
371
+ media: rule.mediaQueries.map((x) => ({ text: x })),
372
+ inactive: rule.inactive || false,
373
+ },
374
+ }))
375
+ return { inlineStyle, matchedCSSRules, inherited: [], crossOriginFailing }
376
+ })
377
+
378
+ this.conn.setRequestHandler('Overlay.setInspectMode', async ({ mode }) => {
379
+ if (mode === 'searchForNode') {
380
+ let prevHighlight = 0
381
+ this.listOverlayComponents().forEach((x) =>
382
+ x.startNodeSelect((node, isFinal) => {
383
+ if (isFinal) {
384
+ this.listOverlayComponents().forEach((x) => x.endNodeSelect())
385
+ if (node) {
386
+ const backendNodeId = this.addBackendNode(node)
387
+ this.conn.sendEvent('Overlay.inspectNodeRequested', {
388
+ backendNodeId,
389
+ })
390
+ }
391
+ this.conn.sendEvent('Overlay.inspectModeCanceled', {})
392
+ } else {
393
+ let nodeId = node ? this.getNodeId(node) : 0
394
+ if (!this.activeNodes[nodeId]) nodeId = 0
395
+ if (prevHighlight !== nodeId) {
396
+ prevHighlight = nodeId
397
+ this.conn.sendEvent('Overlay.nodeHighlightRequested', { nodeId })
398
+ }
399
+ }
400
+ }),
401
+ )
402
+ } else {
403
+ this.listOverlayComponents().forEach((x) => x.endNodeSelect())
404
+ }
405
+ })
406
+
407
+ this.conn.setRequestHandler('Overlay.highlightNode', async (args) => {
408
+ let node: glassEasel.Node
409
+ if ('nodeId' in args) {
410
+ node = this.queryActiveNode(args.nodeId).node
411
+ } else {
412
+ const n = this.getMaybeBackendNode(args.backendNodeId)
413
+ if (!n) throw new Error('no such node found')
414
+ node = n
415
+ }
416
+ this.listOverlayComponents().forEach((x) => {
417
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
418
+ x.highlight(null)
419
+ })
420
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
421
+ this.getOverlayComponent(node).highlight(node)
422
+ })
423
+
424
+ this.conn.setRequestHandler('Overlay.hideHighlight', async () => {
425
+ this.listOverlayComponents().forEach((x) => {
426
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
427
+ x.highlight(null)
428
+ })
429
+ })
430
+ }
431
+
432
+ attach(root: glassEasel.Element, env: glassEasel.MountPointEnv) {
433
+ const nodeMeta = this.activateNode(root)
434
+ const previousNode = this.mountPoints[this.mountPoints.length - 1]
435
+ const previousNodeId = previousNode ? previousNode.nodeMeta.nodeId : undefined
436
+ this.mountPoints.push({ nodeMeta, env })
437
+ this.conn.sendEvent('DOM.childNodeInserted', {
438
+ parentNodeId: this.documentNodeId,
439
+ previousNodeId: previousNodeId ?? 0,
440
+ node: this.collectNodeDetails(nodeMeta, 0, true),
441
+ })
442
+ this.conn.sendEvent('DOM.childNodeCountUpdated', {
443
+ nodeId: this.documentNodeId,
444
+ childNodeCount: this.mountPoints.length,
445
+ })
446
+ }
447
+
448
+ detach(root: glassEasel.Element) {
449
+ const index = this.mountPoints.findIndex((x) => x.nodeMeta.node === root)
450
+ if (index < 0) {
451
+ warn('no such mount point to remove')
452
+ return
453
+ }
454
+ this.mountPoints.splice(index, 1)
455
+ const nodeId = this.deactivateNodeTree(root)
456
+ if (!nodeId) return
457
+ this.conn.sendEvent('DOM.childNodeRemoved', {
458
+ parentNodeId: this.documentNodeId,
459
+ nodeId,
460
+ })
461
+ this.conn.sendEvent('DOM.childNodeCountUpdated', {
462
+ nodeId: this.documentNodeId,
463
+ childNodeCount: this.mountPoints.length,
464
+ })
465
+ }
466
+
467
+ getOverlayComponent(node: glassEasel.Node) {
468
+ const ctx = node.getBackendContext()
469
+ if (!ctx) {
470
+ throw new Error('backend context has been released')
471
+ }
472
+ return this.conn.getOverlayComponent(ctx)
473
+ }
474
+
475
+ listOverlayComponents() {
476
+ const ret: ReturnType<Connection['getOverlayComponent']>[] = []
477
+ this.mountPoints.forEach((mp) => {
478
+ const ctx = mp.nodeMeta.node.getBackendContext()
479
+ if (!ctx) return
480
+ const comp = this.conn.getOverlayComponent(ctx)
481
+ if (ret.includes(comp)) return
482
+ ret.push(comp)
483
+ })
484
+ return ret
485
+ }
486
+
487
+ // eslint-disable-next-line class-methods-use-this
488
+ private useInConsole(v: unknown): string {
489
+ let i = 0
490
+ while (i <= 0xffffffff) {
491
+ const varName = `temp${i}`
492
+ if (!Object.prototype.hasOwnProperty.call(globalThis, varName)) {
493
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
494
+ ;(globalThis as any)[varName] = v
495
+ // eslint-disable-next-line no-console
496
+ console.log(varName, v)
497
+ return varName
498
+ }
499
+ i += 1
500
+ }
501
+ return ''
502
+ }
503
+
504
+ generateNodeId(): NodeId {
505
+ const ret = this.nodeIdInc
506
+ this.nodeIdInc += 1
507
+ return ret
508
+ }
509
+
510
+ private getNodeId(node: glassEasel.Node): NodeId {
511
+ const nodeId = this.nodeIdMap.get(node)
512
+ if (nodeId !== undefined) {
513
+ return nodeId
514
+ }
515
+ const newNodeId = this.generateNodeId()
516
+ this.nodeIdMap.set(node, newNodeId)
517
+ return newNodeId
518
+ }
519
+
520
+ private queryActiveNode(nodeId: NodeId): NodeMeta {
521
+ const nodeMeta = this.activeNodes[nodeId]
522
+ if (!nodeMeta) throw new Error(`no active node found for node id ${nodeId}`)
523
+ return nodeMeta
524
+ }
525
+
526
+ private getMaybeBackendNode(backendNodeId: NodeId): glassEasel.Node | null {
527
+ const nodeMeta = this.activeNodes[backendNodeId]
528
+ if (nodeMeta) return nodeMeta?.node
529
+ return this.activeBackendNodes[backendNodeId]?.deref() ?? null
530
+ }
531
+
532
+ // eslint-disable-next-line class-methods-use-this
533
+ private startWatch(node: glassEasel.Node) {
534
+ const observer = glassEasel.MutationObserver.create((ev) => {
535
+ const node = ev.target
536
+ const nodeId = this.getNodeId(node)
537
+ const nodeMeta = this.activeNodes[nodeId]
538
+ if (!nodeMeta) return
539
+ if (ev.type === 'properties') {
540
+ const elem = node.asElement()!
541
+ const nameType = ev.nameType
542
+ if (nameType === 'attribute') {
543
+ const name = ev.attributeName ?? ''
544
+ const v = elem.getAttribute(name)
545
+ if (v === null || v === 'undefined') {
546
+ this.conn.sendEvent('DOM.attributeRemoved', { nodeId, name, nameType })
547
+ } else {
548
+ const detail = toGlassEaselVar(v)
549
+ const value = glassEaselVarToString(detail)
550
+ this.conn.sendEvent('DOM.attributeModified', {
551
+ nodeId,
552
+ name,
553
+ value,
554
+ detail,
555
+ nameType,
556
+ })
557
+ }
558
+ } else {
559
+ let name: string | undefined
560
+ let v: unknown
561
+ if (nameType === 'component-property') {
562
+ name = ev.propertyName ?? ''
563
+ v = elem.asGeneralComponent()?.data[name]
564
+ } else if (nameType === 'slot-value') {
565
+ name = ev.propertyName ?? ''
566
+ const maybeSlotValues = Reflect.get(elem, '_$slotValues') as unknown
567
+ if (typeof maybeSlotValues === 'object' && maybeSlotValues !== null) {
568
+ v = (maybeSlotValues as { [name: string]: unknown })[name]
569
+ }
570
+ } else if (nameType === 'dataset' && ev.attributeName?.startsWith('data:')) {
571
+ name = ev.attributeName ?? ''
572
+ v = elem.dataset[name.slice(5)]
573
+ } else if (nameType === 'mark' && ev.attributeName?.startsWith('mark:')) {
574
+ name = ev.attributeName ?? ''
575
+ const marks = Reflect.get(elem, '_$marks') as { [key: string]: unknown } | undefined
576
+ v = marks?.[name.slice(5)]
577
+ } else if (nameType === 'external-class') {
578
+ name = ev.attributeName ?? ''
579
+ v = elem.asGeneralComponent()?.getExternalClasses()?.[name]?.join(' ') ?? ''
580
+ } else if (ev.attributeName === 'slot') {
581
+ name = ev.attributeName
582
+ v = elem.slot
583
+ } else if (ev.attributeName === 'id') {
584
+ name = ev.attributeName
585
+ v = elem.id
586
+ } else if (ev.attributeName === 'class') {
587
+ name = ev.attributeName
588
+ v = elem.class
589
+ } else if (ev.attributeName === 'style') {
590
+ name = ev.attributeName
591
+ v = elem.style
592
+ } else if (ev.attributeName === 'name') {
593
+ name = ev.attributeName
594
+ v = Reflect.get(elem, '_$slotName')
595
+ }
596
+ if (name) {
597
+ const detail = toGlassEaselVar(v)
598
+ const value = glassEaselVarToString(detail)
599
+ this.conn.sendEvent('DOM.attributeModified', {
600
+ nodeId,
601
+ name,
602
+ value,
603
+ detail,
604
+ nameType,
605
+ })
606
+ } else {
607
+ warn('unknown mutation observer event')
608
+ }
609
+ }
610
+ return
611
+ }
612
+ if (ev.type === 'childList') {
613
+ if (!nodeMeta.childNodesSent) return
614
+ const parent = node.asElement()!
615
+ ev.addedNodes?.forEach((child) => {
616
+ const index = parent.childNodes.indexOf(child)
617
+ if (index < 0) return
618
+ const previousNodeId = index === 0 ? 0 : this.getNodeId(parent.childNodes[index - 1])
619
+ const childMeta = this.activateNode(child)
620
+ this.conn.sendEvent('DOM.childNodeInserted', {
621
+ parentNodeId: nodeId,
622
+ previousNodeId,
623
+ node: this.collectNodeDetails(childMeta, 0, false),
624
+ })
625
+ })
626
+ ev.removedNodes?.forEach((child) => {
627
+ this.deactivateNodeTree(child)
628
+ this.conn.sendEvent('DOM.childNodeRemoved', {
629
+ parentNodeId: nodeId,
630
+ nodeId: this.getNodeId(child),
631
+ })
632
+ })
633
+ return
634
+ }
635
+ if (ev.type === 'characterData') {
636
+ this.conn.sendEvent('DOM.characterDataModified', {
637
+ nodeId,
638
+ characterData: node.asTextNode()!.textContent,
639
+ })
640
+ return
641
+ }
642
+ warn('unknown mutation observer event')
643
+ })
644
+ observer.observe(node, { properties: 'all', characterData: true, childList: true })
645
+ return observer
646
+ }
647
+
648
+ // eslint-disable-next-line class-methods-use-this
649
+ private endWatch(observer: glassEasel.MutationObserver) {
650
+ observer.disconnect()
651
+ }
652
+
653
+ /**
654
+ * Start tracking a node.
655
+ *
656
+ * This will also activate its parent or host (for shadow-root).
657
+ */
658
+ private activateNode(node: glassEasel.Node): NodeMeta {
659
+ const nodeId = this.getNodeId(node)
660
+ delete this.activeBackendNodes[nodeId]
661
+ if (this.activeNodes[nodeId]) {
662
+ const nodeMeta = this.activeNodes[nodeId]
663
+ return nodeMeta
664
+ }
665
+ const isMountPoint = this.mountPoints.map((x) => x.nodeMeta.node).includes(node)
666
+ if (!isMountPoint) {
667
+ let p: glassEasel.Node | undefined
668
+ if (node.parentNode) p = node.parentNode
669
+ else if (node.asShadowRoot()) p = node.asShadowRoot()!.getHostNode()
670
+ else p = undefined
671
+ if (p) this.activateNode(p)
672
+ }
673
+ const observer = this.startWatch(node)
674
+ const nodeMeta = { node, nodeId, observer, childNodesSent: false }
675
+ this.activeNodes[nodeId] = nodeMeta
676
+ return nodeMeta
677
+ }
678
+
679
+ /** Release a node tree (to allow gabbage collection). */
680
+ private deactivateNodeTree(node: glassEasel.Node): NodeId | undefined {
681
+ const nodeId = this.nodeIdMap.get(node)
682
+ if (nodeId === undefined) {
683
+ return undefined
684
+ }
685
+ if (!this.activeNodes[nodeId]) {
686
+ return nodeId
687
+ }
688
+ const { observer } = this.activeNodes[nodeId]
689
+ this.endWatch(observer)
690
+ const shadowRoot = node.asGeneralComponent()?.getShadowRoot?.()
691
+ if (shadowRoot) this.deactivateNodeTree(shadowRoot)
692
+ const childNodes: glassEasel.Node[] | undefined = (node as glassEasel.Element).childNodes
693
+ if (childNodes) {
694
+ childNodes.forEach((node) => this.deactivateNodeTree(node))
695
+ }
696
+ delete this.activeNodes[nodeId]
697
+ return nodeId
698
+ }
699
+
700
+ private addBackendNode(node: glassEasel.Node): NodeId {
701
+ const nodeId = this.getNodeId(node)
702
+ this.activeBackendNodes[nodeId] = new WeakRef(node)
703
+ return nodeId
704
+ }
705
+
706
+ private activateBackendNodeIfNeeded(backendNodeId: NodeId): NodeMeta | null {
707
+ const node = this.activeBackendNodes[backendNodeId]?.deref()
708
+ if (node === undefined) {
709
+ const nodeMeta = this.activeNodes[backendNodeId]
710
+ return nodeMeta ?? null
711
+ }
712
+ return this.activateNode(node)
713
+ }
714
+
715
+ // eslint-disable-next-line class-methods-use-this
716
+ private collectNodeBasicInfomation(
717
+ backendNodeId: NodeId,
718
+ node: glassEasel.Node,
719
+ ): dom.BackendNode {
720
+ const ty = getNodeType(node)
721
+ const nodeName = getNodeName(node, ty, false)
722
+ const virtual = node.asElement()?.isVirtual() ?? false
723
+ const inheritSlots = node.asElement()?.isInheritSlots() ?? false
724
+ return {
725
+ backendNodeId,
726
+ nodeType: glassEaselNodeTypeToCDP(ty),
727
+ glassEaselNodeType: ty,
728
+ nodeName,
729
+ virtual,
730
+ inheritSlots,
731
+ }
732
+ }
733
+
734
+ collectNodeDetails(nodeMeta: NodeMeta, depth: number, isMountPoint: boolean): dom.Node {
735
+ const { nodeId, node } = nodeMeta
736
+ const tmplDevAttrs = (
737
+ node as glassEasel.Node & { _$wxTmplDevArgs?: glassEasel.template.TmplDevArgs }
738
+ )._$wxTmplDevArgs
739
+
740
+ // collect node information
741
+ const {
742
+ backendNodeId,
743
+ nodeType,
744
+ glassEaselNodeType: ty,
745
+ nodeName,
746
+ virtual,
747
+ inheritSlots,
748
+ } = this.collectNodeBasicInfomation(nodeId, node)
749
+ let parentId: NodeId | undefined
750
+ if (isMountPoint) parentId = this.documentNodeId
751
+ else if (node.parentNode) parentId = this.getNodeId(node.parentNode)
752
+ else if (node.asShadowRoot()) parentId = this.getNodeId(node.asShadowRoot()!.getHostNode())
753
+ else parentId = undefined
754
+ const localName = getNodeName(node, ty, true)
755
+ const nodeValue = node.asTextNode()?.textContent ?? ''
756
+
757
+ // collect attributes
758
+ const attributes: string[] = []
759
+ let glassEaselAttributeCount = 0
760
+ let slotName: string | undefined
761
+ if (ty !== GlassEaselNodeType.TextNode) {
762
+ const activeAttrs = tmplDevAttrs?.A
763
+ const elem = node.asElement()!
764
+ if (activeAttrs) {
765
+ // show active attributes based on template information
766
+ activeAttrs.forEach((name) => {
767
+ if (name[0] === ':') {
768
+ if (name === ':slot') attributes.push('slot', elem.slot)
769
+ if (name === ':id') attributes.push('id', elem.id)
770
+ if (name === ':class') attributes.push('class', elem.class)
771
+ if (name === ':style') attributes.push('style', elem.style)
772
+ if (name === ':name') attributes.push('style', Reflect.get(elem, '_$slotName'))
773
+ } else if (name.startsWith('data:')) {
774
+ const value = elem.dataset?.[name.slice(5)]
775
+ attributes.push(name, glassEaselVarToString(toGlassEaselVar(value)))
776
+ } else if (name.startsWith('mark:')) {
777
+ const marks = Reflect.get(elem, '_$marks') as { [key: string]: unknown } | undefined
778
+ const value = marks?.[name.slice(5)]
779
+ attributes.push(name, glassEaselVarToString(toGlassEaselVar(value)))
780
+ } else if (name.indexOf(':') < 0) {
781
+ if (elem.asNativeNode()) {
782
+ const value = elem.attributes.find(({ name: n }) => name === n)?.value
783
+ attributes.push(name, glassEaselVarToString(toGlassEaselVar(value)))
784
+ } else if (elem.asGeneralComponent()) {
785
+ const comp = elem.asGeneralComponent()!
786
+ if (comp.getComponentDefinition().behavior.getPropertyType(name) !== undefined) {
787
+ const value = comp.data[name] as unknown
788
+ attributes.push(name, glassEaselVarToString(toGlassEaselVar(value)))
789
+ } else if (comp.hasExternalClass(name)) {
790
+ const value = comp.getExternalClasses()[name]?.join(' ')
791
+ attributes.push(name, glassEaselVarToString(toGlassEaselVar(value)))
792
+ }
793
+ } else if (typeof Reflect.get(elem, '_$slotName') === 'string') {
794
+ const maybeSlotValues = Reflect.get(elem, '_$slotValues') as unknown
795
+ if (typeof maybeSlotValues === 'object' && maybeSlotValues !== null) {
796
+ const value = (maybeSlotValues as { [name: string]: unknown })[name]
797
+ attributes.push(name, glassEaselVarToString(toGlassEaselVar(value)))
798
+ }
799
+ }
800
+ }
801
+ })
802
+ } else {
803
+ // detect changed attributes
804
+ if (elem.slot) attributes.push('slot', elem.slot)
805
+ if (elem.id) attributes.push('id', elem.id)
806
+ if (elem.class) attributes.push('class', elem.class)
807
+ if (elem.style) attributes.push('style', elem.style)
808
+ const maybeSlotName = Reflect.get(elem, '_$slotName') as unknown
809
+ if (typeof maybeSlotName === 'string') {
810
+ slotName = maybeSlotName
811
+ attributes.push('name', slotName)
812
+ }
813
+ glassEaselAttributeCount = attributes.length / 2
814
+ if (elem.asNativeNode()) {
815
+ elem.attributes.forEach(({ name, value }) => {
816
+ attributes.push(name, glassEaselVarToString(toGlassEaselVar(value)))
817
+ })
818
+ }
819
+ Object.entries(elem.dataset ?? {}).forEach(([key, value]) => {
820
+ const name = `data:${key}`
821
+ attributes.push(name, glassEaselVarToString(toGlassEaselVar(value)))
822
+ })
823
+ const marks = Reflect.get(elem, '_$marks') as { [key: string]: unknown } | undefined
824
+ Object.entries(marks ?? {}).forEach(([key, value]) => {
825
+ const name = `mark:${key}`
826
+ attributes.push(name, glassEaselVarToString(toGlassEaselVar(value)))
827
+ })
828
+ }
829
+ }
830
+
831
+ // collect shadow-roots
832
+ const sr = node.asGeneralComponent()?.getShadowRoot()
833
+ const shadowRootType = sr ? 'open' : undefined
834
+ let shadowRoots: dom.Node[] | undefined
835
+ if (sr) {
836
+ const nodeMeta = this.activateNode(sr)
837
+ const n = this.collectNodeDetails(nodeMeta, depth - 1, false)
838
+ n.nodeName = 'shadow-root'
839
+ if (n) shadowRoots = [n]
840
+ }
841
+
842
+ // collect children
843
+ let children: dom.Node[] | undefined
844
+ if (depth > 1 && ty !== GlassEaselNodeType.TextNode) {
845
+ const elem = node.asElement()!
846
+ children = []
847
+ nodeMeta.childNodesSent = true
848
+ elem.childNodes.forEach((child) => {
849
+ const nodeMeta = this.activateNode(child)
850
+ const n = this.collectNodeDetails(nodeMeta, depth - 1, false)
851
+ if (n) children!.push(n)
852
+ })
853
+ }
854
+
855
+ // collect slot content
856
+ let distributedNodes: dom.BackendNode[] | undefined
857
+ if (typeof slotName === 'string') {
858
+ const elem = node.asElement()!
859
+ distributedNodes = []
860
+ elem.forEachComposedChild((child) => {
861
+ const nodeId = this.addBackendNode(child)
862
+ const n = this.collectNodeBasicInfomation(nodeId, child)
863
+ if (n) distributedNodes!.push(n)
864
+ })
865
+ }
866
+
867
+ return {
868
+ backendNodeId,
869
+ nodeType,
870
+ glassEaselNodeType: ty,
871
+ nodeName,
872
+ virtual,
873
+ inheritSlots,
874
+ nodeId,
875
+ parentId,
876
+ localName,
877
+ nodeValue,
878
+ attributes,
879
+ glassEaselAttributeCount,
880
+ shadowRootType,
881
+ shadowRoots,
882
+ children,
883
+ distributedNodes,
884
+ }
885
+ }
886
+
887
+ sendChildNodes(nodeMeta: NodeMeta) {
888
+ const { node, nodeId } = nodeMeta
889
+ const elem = node.asElement()
890
+ if (!elem) return
891
+ nodeMeta.childNodesSent = true
892
+ const nodes: dom.Node[] = []
893
+ elem.childNodes.forEach((child) => {
894
+ const nodeMeta = this.activateNode(child)
895
+ nodes.push(this.collectNodeDetails(nodeMeta, 0, false))
896
+ })
897
+ this.conn.sendEvent('DOM.setChildNodes', { parentId: nodeId, nodes })
898
+ }
899
+ }