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.
- package/.eslintignore +2 -0
- package/dist/backend.d.ts +21 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +2 -0
- package/dist/index.js.LICENSE.txt +1 -0
- package/dist/mount_point.d.ts +160 -0
- package/dist/overlay.d.ts +37 -0
- package/dist/protocol/css.d.ts +240 -0
- package/dist/protocol/dom.d.ts +463 -0
- package/dist/protocol/index.d.ts +48 -0
- package/dist/protocol/overlay.d.ts +64 -0
- package/dist/protocol/var.d.ts +15 -0
- package/dist/utils.d.ts +5 -0
- package/package.json +21 -0
- package/src/backend.ts +233 -0
- package/src/index.ts +115 -0
- package/src/mount_point.ts +899 -0
- package/src/overlay.ts +207 -0
- package/src/overlay.wxml +40 -0
- package/src/protocol/css.ts +222 -0
- package/src/protocol/dom.ts +392 -0
- package/src/protocol/index.ts +52 -0
- package/src/protocol/overlay.ts +62 -0
- package/src/protocol/var.ts +37 -0
- package/src/utils.ts +20 -0
- package/tsconfig.json +12 -0
- package/webpack.config.js +49 -0
- package/webpack.dev.config.js +12 -0
- package/wxml_loader.js +14 -0
package/src/backend.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/* eslint-disable arrow-body-style */
|
|
2
|
+
|
|
3
|
+
import * as glassEasel from 'glass-easel'
|
|
4
|
+
import parser from 'postcss-selector-parser'
|
|
5
|
+
import { selectorSpecificity, compare as selectorCompare } from '@csstools/selector-specificity'
|
|
6
|
+
import { tokenize, TokenType, stringify } from '@csstools/css-tokenizer'
|
|
7
|
+
import { backendUnsupported } from './utils'
|
|
8
|
+
|
|
9
|
+
export type BoundingClientRect = glassEasel.BoundingClientRect
|
|
10
|
+
|
|
11
|
+
export const getBoundingClientRect = (
|
|
12
|
+
ctx: glassEasel.GeneralBackendContext,
|
|
13
|
+
elem: glassEasel.GeneralBackendElement,
|
|
14
|
+
): Promise<BoundingClientRect> => {
|
|
15
|
+
return new Promise<BoundingClientRect>((resolve) => {
|
|
16
|
+
if (ctx.mode === glassEasel.BackendMode.Domlike) {
|
|
17
|
+
const { left, top, width, height } = (
|
|
18
|
+
elem.getBoundingClientRect as () => BoundingClientRect
|
|
19
|
+
)()
|
|
20
|
+
resolve({ left, top, width, height })
|
|
21
|
+
} else if (!elem.getBoundingClientRect) {
|
|
22
|
+
backendUnsupported('Element#getBoundingClientRect')
|
|
23
|
+
} else {
|
|
24
|
+
elem.getBoundingClientRect((rect) => {
|
|
25
|
+
resolve(rect)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const getAllComputedStyles = (
|
|
32
|
+
ctx: glassEasel.GeneralBackendContext,
|
|
33
|
+
elem: glassEasel.GeneralBackendElement,
|
|
34
|
+
): Promise<{ properties: { name: string; value: string }[] }> => {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
let properties: { name: string; value: string }[] = []
|
|
37
|
+
if (ctx.mode === glassEasel.BackendMode.Domlike) {
|
|
38
|
+
if (typeof ctx.getAllComputedStyles === 'function') {
|
|
39
|
+
ctx.getAllComputedStyles(elem as glassEasel.domlikeBackend.Element, (ret) => {
|
|
40
|
+
properties = ret.properties
|
|
41
|
+
resolve({ properties })
|
|
42
|
+
})
|
|
43
|
+
} else {
|
|
44
|
+
backendUnsupported('Context#getAllComputedStyles')
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
if ('getAllComputedStyles' in elem) {
|
|
48
|
+
;(elem as glassEasel.backend.Element).getAllComputedStyles!((ret) => {
|
|
49
|
+
properties = ret.properties
|
|
50
|
+
resolve({ properties })
|
|
51
|
+
})
|
|
52
|
+
} else {
|
|
53
|
+
backendUnsupported('Element#getAllComputedStyles')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const getBoxModel = (
|
|
60
|
+
ctx: glassEasel.GeneralBackendContext,
|
|
61
|
+
elem: glassEasel.GeneralBackendElement,
|
|
62
|
+
): Promise<{
|
|
63
|
+
margin: BoundingClientRect
|
|
64
|
+
border: BoundingClientRect
|
|
65
|
+
padding: BoundingClientRect
|
|
66
|
+
content: BoundingClientRect
|
|
67
|
+
}> => {
|
|
68
|
+
const parsePx = (v: string): number => {
|
|
69
|
+
if (!v.endsWith('px')) return NaN
|
|
70
|
+
return Number(v.slice(0, -2))
|
|
71
|
+
}
|
|
72
|
+
return new Promise<{
|
|
73
|
+
margin: BoundingClientRect
|
|
74
|
+
border: BoundingClientRect
|
|
75
|
+
padding: BoundingClientRect
|
|
76
|
+
content: BoundingClientRect
|
|
77
|
+
}>((resolve) => {
|
|
78
|
+
if ('getBoxModel' in elem) {
|
|
79
|
+
// if there is `getBoxModel` call, use it
|
|
80
|
+
elem.getBoxModel!((ret) => {
|
|
81
|
+
resolve(ret)
|
|
82
|
+
})
|
|
83
|
+
} else {
|
|
84
|
+
// otherwise, use `getAllComputedStyles` to emulate
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises, promise/catch-or-return, promise/always-return
|
|
86
|
+
Promise.resolve().then(async () => {
|
|
87
|
+
const border = await getBoundingClientRect(ctx, elem)
|
|
88
|
+
const cs = await getAllComputedStyles(ctx, elem)
|
|
89
|
+
let marginLeft = NaN
|
|
90
|
+
let marginTop = NaN
|
|
91
|
+
let marginRight = NaN
|
|
92
|
+
let marginBottom = NaN
|
|
93
|
+
let paddingLeft = NaN
|
|
94
|
+
let paddingTop = NaN
|
|
95
|
+
let paddingRight = NaN
|
|
96
|
+
let paddingBottom = NaN
|
|
97
|
+
let borderLeft = NaN
|
|
98
|
+
let borderTop = NaN
|
|
99
|
+
let borderRight = NaN
|
|
100
|
+
let borderBottom = NaN
|
|
101
|
+
cs.properties.forEach(({ name, value }) => {
|
|
102
|
+
if (name === 'margin-left') marginLeft = parsePx(value)
|
|
103
|
+
if (name === 'margin-top') marginTop = parsePx(value)
|
|
104
|
+
if (name === 'margin-right') marginRight = parsePx(value)
|
|
105
|
+
if (name === 'margin-bottom') marginBottom = parsePx(value)
|
|
106
|
+
if (name === 'padding-left') paddingLeft = parsePx(value)
|
|
107
|
+
if (name === 'padding-top') paddingTop = parsePx(value)
|
|
108
|
+
if (name === 'padding-right') paddingRight = parsePx(value)
|
|
109
|
+
if (name === 'padding-bottom') paddingBottom = parsePx(value)
|
|
110
|
+
if (name === 'border-left-width') borderLeft = parsePx(value)
|
|
111
|
+
if (name === 'border-top-width') borderTop = parsePx(value)
|
|
112
|
+
if (name === 'border-right-width') borderRight = parsePx(value)
|
|
113
|
+
if (name === 'border-bottom-width') borderBottom = parsePx(value)
|
|
114
|
+
})
|
|
115
|
+
const margin = {
|
|
116
|
+
left: border.left - marginLeft,
|
|
117
|
+
top: border.top - marginTop,
|
|
118
|
+
width: border.width + marginLeft + marginRight,
|
|
119
|
+
height: border.height + marginTop + marginBottom,
|
|
120
|
+
}
|
|
121
|
+
const padding = {
|
|
122
|
+
left: border.left + borderLeft,
|
|
123
|
+
top: border.top + borderTop,
|
|
124
|
+
width: border.width - borderLeft - borderRight,
|
|
125
|
+
height: border.height - borderTop - borderBottom,
|
|
126
|
+
}
|
|
127
|
+
const content = {
|
|
128
|
+
left: padding.left + paddingLeft,
|
|
129
|
+
top: padding.top + paddingTop,
|
|
130
|
+
width: padding.width - paddingLeft - paddingRight,
|
|
131
|
+
height: padding.height - paddingTop - paddingBottom,
|
|
132
|
+
}
|
|
133
|
+
resolve({
|
|
134
|
+
margin,
|
|
135
|
+
border,
|
|
136
|
+
padding,
|
|
137
|
+
content,
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const getMatchedRules = (
|
|
145
|
+
ctx: glassEasel.GeneralBackendContext,
|
|
146
|
+
elem: glassEasel.GeneralBackendElement,
|
|
147
|
+
): Promise<{
|
|
148
|
+
inline: glassEasel.CSSProperty[]
|
|
149
|
+
inlineText?: string
|
|
150
|
+
rules: glassEasel.CSSRule[]
|
|
151
|
+
crossOriginFailing?: boolean
|
|
152
|
+
}> => {
|
|
153
|
+
const parseNameValueStr = (cssText: string) => {
|
|
154
|
+
const ret: { name: string; value: string }[] = []
|
|
155
|
+
const tokens = tokenize({ css: cssText })
|
|
156
|
+
if (tokens.length === 0) return null
|
|
157
|
+
let nameStart = 0
|
|
158
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
159
|
+
const t = tokens[i]
|
|
160
|
+
if (t[0] === TokenType.Colon) {
|
|
161
|
+
const name = stringify(...tokens.slice(nameStart, i))
|
|
162
|
+
i += 1
|
|
163
|
+
const valueStart = i
|
|
164
|
+
for (; i < tokens.length; i += 1) {
|
|
165
|
+
const t = tokens[i]
|
|
166
|
+
if (t[0] === TokenType.Semicolon) break
|
|
167
|
+
}
|
|
168
|
+
const value = stringify(...tokens.slice(valueStart, i))
|
|
169
|
+
ret.push({ name, value })
|
|
170
|
+
nameStart = i + 1
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return ret
|
|
174
|
+
}
|
|
175
|
+
const calcRuleWeight = (rules: glassEasel.CSSRule[]): glassEasel.CSSRule[] => {
|
|
176
|
+
const rulesWithSelector = rules.map((rule) => {
|
|
177
|
+
if (rule.propertyText) {
|
|
178
|
+
rule.properties = parseNameValueStr(rule.propertyText) ?? rule.properties
|
|
179
|
+
}
|
|
180
|
+
const ps = parser().astSync(rule.selector)
|
|
181
|
+
const specificity = selectorSpecificity(ps)
|
|
182
|
+
return [specificity, rule] as const
|
|
183
|
+
})
|
|
184
|
+
rulesWithSelector.sort(([aSpec, aRule], [bSpec, bRule]) => {
|
|
185
|
+
const highBitsDiff = (aRule.weightHighBits || 0) - (bRule.weightHighBits || 0)
|
|
186
|
+
if (highBitsDiff !== 0) return highBitsDiff
|
|
187
|
+
if (aRule.weightLowBits !== undefined || bRule.weightLowBits !== undefined) {
|
|
188
|
+
const lowBitsDiff = (aRule.weightLowBits ?? -1) - (bRule.weightLowBits ?? -1)
|
|
189
|
+
if (lowBitsDiff !== 0) return lowBitsDiff
|
|
190
|
+
}
|
|
191
|
+
const selDiff = selectorCompare(aSpec, bSpec)
|
|
192
|
+
if (selDiff !== 0) return selDiff
|
|
193
|
+
const sheetDiff = aRule.sheetIndex - bRule.sheetIndex
|
|
194
|
+
if (sheetDiff !== 0) return sheetDiff
|
|
195
|
+
const ruleDiff = aRule.ruleIndex - bRule.ruleIndex
|
|
196
|
+
return ruleDiff
|
|
197
|
+
})
|
|
198
|
+
return rulesWithSelector.map(([_sel, rule]) => rule).reverse()
|
|
199
|
+
}
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
if (ctx.mode === glassEasel.BackendMode.Domlike) {
|
|
202
|
+
if (typeof ctx.getMatchedRules === 'function') {
|
|
203
|
+
try {
|
|
204
|
+
ctx.getMatchedRules(elem as glassEasel.domlikeBackend.Element, (ret) => {
|
|
205
|
+
ret.rules = calcRuleWeight(ret.rules)
|
|
206
|
+
if (ret.inlineText) {
|
|
207
|
+
ret.inline = parseNameValueStr(ret.inlineText) ?? ret.inline
|
|
208
|
+
}
|
|
209
|
+
resolve(ret)
|
|
210
|
+
})
|
|
211
|
+
} catch (err) {
|
|
212
|
+
// this may throw when reading cross-origin stylesheets
|
|
213
|
+
resolve({
|
|
214
|
+
inline: [],
|
|
215
|
+
rules: [],
|
|
216
|
+
crossOriginFailing: true,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
backendUnsupported('Context#getMatchedRules')
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
if ('getMatchedRules' in elem) {
|
|
224
|
+
;(elem as glassEasel.backend.Element).getMatchedRules!((ret) => {
|
|
225
|
+
ret.rules = calcRuleWeight(ret.rules)
|
|
226
|
+
resolve(ret)
|
|
227
|
+
})
|
|
228
|
+
} else {
|
|
229
|
+
backendUnsupported('Element#getMatchedRules')
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type * as glassEasel from 'glass-easel'
|
|
2
|
+
import type {
|
|
3
|
+
AgentEventKind,
|
|
4
|
+
AgentRequestKind,
|
|
5
|
+
AgentRecvMessage,
|
|
6
|
+
AgentSendMessage,
|
|
7
|
+
} from './protocol/index'
|
|
8
|
+
import { MountPointsManager } from './mount_point'
|
|
9
|
+
import { overlayCompDef, OverlayManager } from './overlay'
|
|
10
|
+
import { debug } from './utils'
|
|
11
|
+
|
|
12
|
+
export * as protocol from './protocol/index'
|
|
13
|
+
|
|
14
|
+
export interface MessageChannel {
|
|
15
|
+
send(data: AgentSendMessage): void
|
|
16
|
+
recv(listener: (data: AgentRecvMessage) => void): void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Connection {
|
|
20
|
+
private messageChannel: MessageChannel
|
|
21
|
+
private requestHandlers = Object.create(null) as Record<string, (detail: any) => Promise<any>>
|
|
22
|
+
readonly overlayManager: OverlayManager
|
|
23
|
+
|
|
24
|
+
constructor(messageChannel: MessageChannel) {
|
|
25
|
+
this.messageChannel = messageChannel
|
|
26
|
+
messageChannel.recv((data) => {
|
|
27
|
+
if (data.kind === 'request') {
|
|
28
|
+
debug(`recv request ${data.id}`, data.name, data.detail)
|
|
29
|
+
this.recvRequest(data.id, data.name, data.detail)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
this.overlayManager = new OverlayManager()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private recvRequest(id: number, name: string, detail: unknown) {
|
|
36
|
+
const handler = this.requestHandlers[name]
|
|
37
|
+
if (!handler) {
|
|
38
|
+
const data: AgentSendMessage = {
|
|
39
|
+
kind: 'error',
|
|
40
|
+
id,
|
|
41
|
+
message: 'invalid request name',
|
|
42
|
+
}
|
|
43
|
+
this.messageChannel.send(data)
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
handler
|
|
47
|
+
.call(this, detail)
|
|
48
|
+
.then((ret: unknown) => {
|
|
49
|
+
debug(`send response ${id}`, ret)
|
|
50
|
+
const data: AgentSendMessage = { kind: 'response', id, detail: ret }
|
|
51
|
+
this.messageChannel.send(data)
|
|
52
|
+
return undefined
|
|
53
|
+
})
|
|
54
|
+
.catch((err) => {
|
|
55
|
+
const data: AgentSendMessage = {
|
|
56
|
+
kind: 'error',
|
|
57
|
+
id,
|
|
58
|
+
message: err instanceof Error ? err.message : '',
|
|
59
|
+
stack: err instanceof Error ? err.stack : '',
|
|
60
|
+
}
|
|
61
|
+
this.messageChannel.send(data)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setRequestHandler<T extends keyof AgentRequestKind>(
|
|
66
|
+
name: T,
|
|
67
|
+
handler: (detail: AgentRequestKind[T]['request']) => Promise<AgentRequestKind[T]['response']>,
|
|
68
|
+
) {
|
|
69
|
+
this.requestHandlers[name] = handler
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
sendEvent<T extends keyof AgentEventKind>(name: T, detail: AgentEventKind[T]['detail']) {
|
|
73
|
+
debug('send event', name, detail)
|
|
74
|
+
const data: AgentSendMessage = { kind: 'event', name, detail }
|
|
75
|
+
this.messageChannel.send(data)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getOverlayComponent(mp: glassEasel.GeneralBackendContext) {
|
|
79
|
+
return this.overlayManager.get(mp).asInstanceOf(overlayCompDef)!
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class InspectorDevToolsImpl implements glassEasel.InspectorDevTools {
|
|
84
|
+
private conn: Connection
|
|
85
|
+
private mountPoints: MountPointsManager
|
|
86
|
+
private enabled = false
|
|
87
|
+
|
|
88
|
+
constructor(conn: Connection) {
|
|
89
|
+
this.conn = conn
|
|
90
|
+
this.mountPoints = new MountPointsManager(conn)
|
|
91
|
+
conn.setRequestHandler('DOM.enable', async () => {
|
|
92
|
+
if (this.enabled) return
|
|
93
|
+
this.enabled = true
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// eslint-disable-next-line class-methods-use-this
|
|
98
|
+
addMountPoint(root: glassEasel.Element, env: glassEasel.MountPointEnv): void {
|
|
99
|
+
debug('attach mount point', root)
|
|
100
|
+
this.mountPoints.attach(root, env)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// eslint-disable-next-line class-methods-use-this
|
|
104
|
+
removeMountPoint(root: glassEasel.GeneralComponent): void {
|
|
105
|
+
debug('detach mount point', root)
|
|
106
|
+
this.mountPoints.detach(root)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const getDevTools = (channel: MessageChannel): glassEasel.DevTools => {
|
|
111
|
+
const connection = new Connection(channel)
|
|
112
|
+
return {
|
|
113
|
+
inspector: new InspectorDevToolsImpl(connection),
|
|
114
|
+
}
|
|
115
|
+
}
|