add comments
This commit is contained in:
@@ -35,6 +35,27 @@ export interface VariablesResult {
|
||||
[scope: string]: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ScriptInfo {
|
||||
scriptId: string
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for debugging JavaScript code via Chrome DevTools Protocol.
|
||||
* Works with both Node.js (--inspect) and browser debugging.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cdp = await getCDPSessionForPage({ page, wsUrl })
|
||||
* const dbg = new Debugger({ cdp })
|
||||
*
|
||||
* await dbg.setBreakpoint({ file: '/path/to/file.js', line: 42 })
|
||||
* // trigger the code path, then:
|
||||
* const location = await dbg.getLocation()
|
||||
* const vars = await dbg.inspectVariables()
|
||||
* await dbg.resume()
|
||||
* ```
|
||||
*/
|
||||
export class Debugger {
|
||||
private cdp: CDPSession
|
||||
private debuggerEnabled = false
|
||||
@@ -42,7 +63,20 @@ export class Debugger {
|
||||
private currentCallFrames: Protocol.Debugger.CallFrame[] = []
|
||||
private breakpoints = new Map<string, BreakpointInfo>()
|
||||
private consoleOutput: ConsoleEntry[] = []
|
||||
private scripts = new Map<string, ScriptInfo>()
|
||||
|
||||
/**
|
||||
* Creates a new Debugger instance.
|
||||
*
|
||||
* @param options - Configuration options
|
||||
* @param options.cdp - A CDPSession instance for sending CDP commands
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const cdp = await getCDPSessionForPage({ page, wsUrl })
|
||||
* const dbg = new Debugger({ cdp })
|
||||
* ```
|
||||
*/
|
||||
constructor({ cdp }: { cdp: CDPSession }) {
|
||||
this.cdp = cdp
|
||||
this.setupEventListeners()
|
||||
@@ -59,6 +93,15 @@ export class Debugger {
|
||||
this.currentCallFrames = []
|
||||
})
|
||||
|
||||
this.cdp.on('Debugger.scriptParsed', (params) => {
|
||||
if (params.url && params.url.endsWith('.js')) {
|
||||
this.scripts.set(params.scriptId, {
|
||||
scriptId: params.scriptId,
|
||||
url: params.url,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.cdp.on('Runtime.consoleAPICalled', (params) => {
|
||||
const message = params.args
|
||||
.map((arg) => {
|
||||
@@ -87,6 +130,15 @@ export class Debugger {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the debugger and runtime domains. Called automatically by other methods.
|
||||
* Also resumes execution if the target was started with --inspect-brk.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* await dbg.enable()
|
||||
* ```
|
||||
*/
|
||||
async enable(): Promise<void> {
|
||||
if (this.debuggerEnabled) {
|
||||
return
|
||||
@@ -97,6 +149,21 @@ export class Debugger {
|
||||
this.debuggerEnabled = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes JavaScript code in the debugged process.
|
||||
* Code is wrapped in try-catch and promises are awaited.
|
||||
*
|
||||
* @param options - Execution options
|
||||
* @param options.code - JavaScript code to execute
|
||||
* @returns The result value and any console output generated
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const result = await dbg.executeCode({ code: 'Object.keys(require.cache).length' })
|
||||
* console.log(result.value) // 42
|
||||
* console.log(result.consoleOutput) // ['[log] some message']
|
||||
* ```
|
||||
*/
|
||||
async executeCode({ code }: { code: string }): Promise<EvaluateResult> {
|
||||
await this.enable()
|
||||
|
||||
@@ -133,6 +200,22 @@ export class Debugger {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a breakpoint at a specified file and line number.
|
||||
* The file path is automatically converted to a file:// URL if needed.
|
||||
*
|
||||
* @param options - Breakpoint options
|
||||
* @param options.file - Absolute file path or URL
|
||||
* @param options.line - Line number (1-based)
|
||||
* @returns The breakpoint ID for later removal
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const id = await dbg.setBreakpoint({ file: '/app/src/index.js', line: 42 })
|
||||
* // later:
|
||||
* await dbg.deleteBreakpoint({ breakpointId: id })
|
||||
* ```
|
||||
*/
|
||||
async setBreakpoint({ file, line }: { file: string; line: number }): Promise<string> {
|
||||
await this.enable()
|
||||
|
||||
@@ -151,16 +234,58 @@ export class Debugger {
|
||||
return response.breakpointId
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a breakpoint by its ID.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.breakpointId - The breakpoint ID returned by setBreakpoint
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* await dbg.deleteBreakpoint({ breakpointId: 'bp-123' })
|
||||
* ```
|
||||
*/
|
||||
async deleteBreakpoint({ breakpointId }: { breakpointId: string }): Promise<void> {
|
||||
await this.enable()
|
||||
await this.cdp.send('Debugger.removeBreakpoint', { breakpointId })
|
||||
this.breakpoints.delete(breakpointId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all active breakpoints set by this debugger instance.
|
||||
*
|
||||
* @returns Array of breakpoint info objects
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const breakpoints = dbg.listBreakpoints()
|
||||
* // [{ id: 'bp-123', file: '/app/index.js', line: 42 }]
|
||||
* ```
|
||||
*/
|
||||
listBreakpoints(): BreakpointInfo[] {
|
||||
return Array.from(this.breakpoints.values())
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects variables in the current scope.
|
||||
* When paused at a breakpoint with scope='local', returns variables from the call frame.
|
||||
* Otherwise returns global scope information.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.scope - 'local' for call frame variables, 'global' for global scope
|
||||
* @returns Variables grouped by scope type
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // When paused at a breakpoint:
|
||||
* const vars = await dbg.inspectVariables({ scope: 'local' })
|
||||
* // { local: { myVar: 'hello', count: 42 }, closure: { captured: true } }
|
||||
*
|
||||
* // Global scope:
|
||||
* const globals = await dbg.inspectVariables({ scope: 'global' })
|
||||
* // { lexicalNames: [...], globalThis: { ... } }
|
||||
* ```
|
||||
*/
|
||||
async inspectVariables({ scope = 'local' }: { scope?: 'local' | 'global' } = {}): Promise<VariablesResult> {
|
||||
await this.enable()
|
||||
|
||||
@@ -213,6 +338,25 @@ export class Debugger {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a JavaScript expression in the current context.
|
||||
* When paused at a breakpoint, evaluates in the call frame context (can access local variables).
|
||||
* Otherwise evaluates in the global context.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.expression - JavaScript expression to evaluate
|
||||
* @returns The result value and any console output generated
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // When paused, can access local variables:
|
||||
* const result = await dbg.evaluate({ expression: 'localVar + 1' })
|
||||
*
|
||||
* // Global context when not paused:
|
||||
* const result = await dbg.evaluate({ expression: 'process.env.NODE_ENV' })
|
||||
* console.log(result.value) // 'development'
|
||||
* ```
|
||||
*/
|
||||
async evaluate({ expression }: { expression: string }): Promise<EvaluateResult> {
|
||||
await this.enable()
|
||||
|
||||
@@ -264,6 +408,26 @@ export class Debugger {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current execution location when paused at a breakpoint.
|
||||
* Includes the call stack and surrounding source code for context.
|
||||
*
|
||||
* @returns Location info with URL, line number, call stack, and source context
|
||||
* @throws Error if debugger is not paused
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const location = await dbg.getLocation()
|
||||
* console.log(location.url) // '/app/src/index.js'
|
||||
* console.log(location.lineNumber) // 42
|
||||
* console.log(location.callstack) // [{ functionName: 'handleRequest', ... }]
|
||||
* console.log(location.sourceContext)
|
||||
* // ' 40: function handleRequest(req) {
|
||||
* // 41: const data = req.body
|
||||
* // > 42: processData(data)
|
||||
* // 43: }'
|
||||
* ```
|
||||
*/
|
||||
async getLocation(): Promise<LocationInfo> {
|
||||
await this.enable()
|
||||
|
||||
@@ -305,6 +469,17 @@ export class Debugger {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps over to the next line of code, not entering function calls.
|
||||
*
|
||||
* @throws Error if debugger is not paused
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* await dbg.stepOver()
|
||||
* const newLocation = await dbg.getLocation()
|
||||
* ```
|
||||
*/
|
||||
async stepOver(): Promise<void> {
|
||||
await this.enable()
|
||||
if (!this.paused) {
|
||||
@@ -313,6 +488,18 @@ export class Debugger {
|
||||
await this.cdp.send('Debugger.stepOver')
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps into a function call on the current line.
|
||||
*
|
||||
* @throws Error if debugger is not paused
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* await dbg.stepInto()
|
||||
* const location = await dbg.getLocation()
|
||||
* // now inside the called function
|
||||
* ```
|
||||
*/
|
||||
async stepInto(): Promise<void> {
|
||||
await this.enable()
|
||||
if (!this.paused) {
|
||||
@@ -321,6 +508,18 @@ export class Debugger {
|
||||
await this.cdp.send('Debugger.stepInto')
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps out of the current function, returning to the caller.
|
||||
*
|
||||
* @throws Error if debugger is not paused
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* await dbg.stepOut()
|
||||
* const location = await dbg.getLocation()
|
||||
* // back in the calling function
|
||||
* ```
|
||||
*/
|
||||
async stepOut(): Promise<void> {
|
||||
await this.enable()
|
||||
if (!this.paused) {
|
||||
@@ -329,6 +528,17 @@ export class Debugger {
|
||||
await this.cdp.send('Debugger.stepOut')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes code execution until the next breakpoint or completion.
|
||||
*
|
||||
* @throws Error if debugger is not paused
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* await dbg.resume()
|
||||
* // execution continues
|
||||
* ```
|
||||
*/
|
||||
async resume(): Promise<void> {
|
||||
await this.enable()
|
||||
if (!this.paused) {
|
||||
@@ -337,14 +547,67 @@ export class Debugger {
|
||||
await this.cdp.send('Debugger.resume')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most recent console output captured from the debugged process.
|
||||
* Console output is captured automatically via Runtime.consoleAPICalled events.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.limit - Maximum number of entries to return (default: 20)
|
||||
* @returns Array of console entries with type, message, and timestamp
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const logs = dbg.getConsoleOutput({ limit: 10 })
|
||||
* // [{ type: 'log', message: 'Hello world', timestamp: 1234567890 }]
|
||||
* ```
|
||||
*/
|
||||
getConsoleOutput({ limit = 20 }: { limit?: number } = {}): ConsoleEntry[] {
|
||||
return this.consoleOutput.slice(-limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the debugger is currently paused at a breakpoint.
|
||||
*
|
||||
* @returns true if paused, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* if (dbg.isPaused()) {
|
||||
* const vars = await dbg.inspectVariables()
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
isPaused(): boolean {
|
||||
return this.paused
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists available .js scripts where breakpoints can be set.
|
||||
* Scripts are collected from Debugger.scriptParsed events after enable() is called.
|
||||
*
|
||||
* @param options - Options
|
||||
* @param options.search - Optional string to filter scripts by URL (case-insensitive)
|
||||
* @returns Array of up to 20 matching scripts with scriptId and url
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // List all scripts
|
||||
* const scripts = dbg.listScripts()
|
||||
* // [{ scriptId: '1', url: 'file:///app/index.js' }, ...]
|
||||
*
|
||||
* // Search for specific files
|
||||
* const handlers = dbg.listScripts({ search: 'handler' })
|
||||
* // [{ scriptId: '5', url: 'file:///app/src/handlers.js' }]
|
||||
* ```
|
||||
*/
|
||||
listScripts({ search }: { search?: string } = {}): ScriptInfo[] {
|
||||
const scripts = Array.from(this.scripts.values())
|
||||
const filtered = search
|
||||
? scripts.filter((s) => s.url.toLowerCase().includes(search.toLowerCase()))
|
||||
: scripts
|
||||
return filtered.slice(0, 20)
|
||||
}
|
||||
|
||||
private formatPropertyValue(value: Protocol.Runtime.RemoteObject): unknown {
|
||||
if (value.type === 'object' && value.subtype !== 'null') {
|
||||
return `[${value.subtype || value.type}]`
|
||||
|
||||
Reference in New Issue
Block a user