-
Notifications
You must be signed in to change notification settings - Fork 323
/
Copy pathindex.js
173 lines (140 loc) · 4.8 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
'use strict'
const path = require('path')
const {
workerData: {
breakpointSetChannel,
breakpointHitChannel,
breakpointRemoveChannel
}
} = require('worker_threads')
const { randomUUID } = require('crypto')
const sourceMap = require('source-map')
// TODO: move debugger/devtools_client/session to common place
const session = require('../../../debugger/devtools_client/session')
// TODO: move debugger/devtools_client/snapshot to common place
const { getLocalStateForCallFrame } = require('../../../debugger/devtools_client/snapshot')
// TODO: move debugger/devtools_client/state to common place
const {
findScriptFromPartialPath,
getStackFromCallFrames
} = require('../../../debugger/devtools_client/state')
const log = require('../../../log')
let sessionStarted = false
const breakpointIdToProbe = new Map()
const probeIdToBreakpointId = new Map()
session.on('Debugger.paused', async ({ params: { hitBreakpoints: [hitBreakpoint], callFrames } }) => {
const probe = breakpointIdToProbe.get(hitBreakpoint)
if (!probe) {
log.warn(`No probe found for breakpoint ${hitBreakpoint}`)
return session.post('Debugger.resume')
}
const stack = getStackFromCallFrames(callFrames)
const getLocalState = await getLocalStateForCallFrame(callFrames[0])
await session.post('Debugger.resume')
const snapshot = {
id: randomUUID(),
timestamp: Date.now(),
probe: {
id: probe.id,
version: '0',
location: probe.location
},
stack,
language: 'javascript'
}
const state = getLocalState()
if (state) {
snapshot.captures = {
lines: { [probe.location.lines[0]]: { locals: state } }
}
}
breakpointHitChannel.postMessage({ snapshot })
})
breakpointRemoveChannel.on('message', async (probeId) => {
await removeBreakpoint(probeId)
breakpointRemoveChannel.postMessage(probeId)
})
breakpointSetChannel.on('message', async (probe) => {
await addBreakpoint(probe)
breakpointSetChannel.postMessage(probe.id)
})
async function removeBreakpoint (probeId) {
if (!sessionStarted) {
// We should not get in this state, but abort if we do, so the code doesn't fail unexpected
throw Error(`Cannot remove probe ${probeId}: Debugger not started`)
}
const breakpointId = probeIdToBreakpointId.get(probeId)
if (!breakpointId) {
throw Error(`Unknown probe id: ${probeId}`)
}
await session.post('Debugger.removeBreakpoint', { breakpointId })
probeIdToBreakpointId.delete(probeId)
breakpointIdToProbe.delete(breakpointId)
}
async function addBreakpoint (probe) {
if (!sessionStarted) await start()
const { file, line } = probe
probe.location = { file, lines: [String(line)] }
const script = findScriptFromPartialPath(file)
if (!script) {
log.error(`No loaded script found for ${file}`)
throw new Error(`No loaded script found for ${file}`)
}
const [path, scriptId, sourceMapURL] = script
log.warn(`Adding breakpoint at ${path}:${line}`)
let lineNumber = line
if (sourceMapURL && sourceMapURL.startsWith('data:')) {
try {
lineNumber = await processScriptWithInlineSourceMap({ file, line, sourceMapURL })
} catch (err) {
log.error('Error processing script with inline source map', err)
}
}
try {
const { breakpointId } = await session.post('Debugger.setBreakpoint', {
location: {
scriptId,
lineNumber: lineNumber - 1
}
})
breakpointIdToProbe.set(breakpointId, probe)
probeIdToBreakpointId.set(probe.id, breakpointId)
} catch (e) {
log.error(`Error setting breakpoint at ${path}:${line}:`, e)
}
}
function start () {
sessionStarted = true
return session.post('Debugger.enable') // return instead of await to reduce number of promises created
}
async function processScriptWithInlineSourceMap (params) {
const { file, line, sourceMapURL } = params
// Extract the base64-encoded source map
const base64SourceMap = sourceMapURL.split('base64,')[1]
// Decode the base64 source map
const decodedSourceMap = Buffer.from(base64SourceMap, 'base64').toString('utf8')
// Parse the source map
const consumer = await new sourceMap.SourceMapConsumer(decodedSourceMap)
let generatedPosition
// Map to the generated position. We'll attempt with the full file path first, then with the basename.
// TODO: figure out why sometimes the full path doesn't work
generatedPosition = consumer.generatedPositionFor({
source: file,
line,
column: 0
})
if (generatedPosition.line === null) {
generatedPosition = consumer.generatedPositionFor({
source: path.basename(file),
line,
column: 0
})
}
consumer.destroy()
// If we can't find the line, just return the original line
if (generatedPosition.line === null) {
log.error(`Could not find generated position for ${file}:${line}`)
return line
}
return generatedPosition.line
}