diff options
Diffstat (limited to 'packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts')
-rw-r--r-- | packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts | 193 |
1 files changed, 193 insertions, 0 deletions
diff --git a/packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts b/packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts new file mode 100644 index 000000000..3b3f05c6b --- /dev/null +++ b/packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts @@ -0,0 +1,193 @@ +import type { LineRange, MappedPosition } from "source-map-js"; +import { SourceMapConsumer } from "source-map-js"; + +export type LocationRequest = { + line?: number; + column?: number; + url?: string; +}; + +export type Location = { + line: number; // 0-based + column: number; // 0-based +} & ( + | { + verified: true; + } + | { + verified?: false; + message?: string; + } +); + +export interface SourceMap { + generatedLocation(request: LocationRequest): Location; + originalLocation(request: LocationRequest): Location; +} + +class ActualSourceMap implements SourceMap { + #sourceMap: SourceMapConsumer; + #sources: string[]; + + constructor(sourceMap: SourceMapConsumer) { + this.#sourceMap = sourceMap; + this.#sources = (sourceMap as any)._absoluteSources; + } + + #getSource(url?: string): string { + const sources = this.#sources; + if (!sources.length) { + return ""; + } + if (sources.length === 1 || !url) { + return sources[0]; + } + for (const source of sources) { + if (url.endsWith(source)) { + return source; + } + } + return ""; + } + + generatedLocation(request: LocationRequest): Location { + const { line, column, url } = request; + + let lineRange: LineRange; + try { + const source = this.#getSource(url); + lineRange = this.#sourceMap.generatedPositionFor({ + line: lineTo1BasedLine(line), + column: columnToColumn(column), + source, + }); + } catch (error) { + return { + line: lineToLine(line), + column: columnToColumn(column), + verified: false, + message: unknownToError(error), + }; + } + + if (!locationIsValid(lineRange)) { + return { + line: lineToLine(line), + column: columnToColumn(column), + verified: false, + }; + } + + const { line: gline, column: gcolumn } = lineRange; + return { + line: lineToLine(gline), + column: columnToColumn(gcolumn), + verified: true, + }; + } + + originalLocation(request: LocationRequest): Location { + const { line, column } = request; + + let mappedPosition: MappedPosition; + try { + mappedPosition = this.#sourceMap.originalPositionFor({ + line: lineTo1BasedLine(line), + column: columnToColumn(column), + }); + } catch (error) { + return { + line: lineToLine(line), + column: columnToColumn(column), + verified: false, + message: unknownToError(error), + }; + } + + if (!locationIsValid(mappedPosition)) { + return { + line: lineToLine(line), + column: columnToColumn(column), + verified: false, + }; + } + + const { line: oline, column: ocolumn } = mappedPosition; + return { + line: lineTo0BasedLine(oline), + column: columnToColumn(ocolumn), + verified: true, + }; + } +} + +class NoopSourceMap implements SourceMap { + generatedLocation(request: LocationRequest): Location { + const { line, column } = request; + return { + line: lineToLine(line), + column: columnToColumn(column), + verified: true, + }; + } + + originalLocation(request: LocationRequest): Location { + const { line, column } = request; + return { + line: lineToLine(line), + column: columnToColumn(column), + verified: true, + }; + } +} + +const defaultSourceMap = new NoopSourceMap(); + +export function SourceMap(url?: string): SourceMap { + if (!url || !url.startsWith("data:")) { + return defaultSourceMap; + } + try { + const [_, base64] = url.split(",", 2); + const decoded = Buffer.from(base64, "base64url").toString("utf8"); + const schema = JSON.parse(decoded); + const sourceMap = new SourceMapConsumer(schema); + return new ActualSourceMap(sourceMap); + } catch (error) { + console.warn("Failed to parse source map URL", url); + } + return defaultSourceMap; +} + +function lineTo1BasedLine(line?: number): number { + return numberIsValid(line) ? line + 1 : 1; +} + +function lineTo0BasedLine(line?: number): number { + return numberIsValid(line) ? line - 1 : 0; +} + +function lineToLine(line?: number): number { + return numberIsValid(line) ? line : 0; +} + +function columnToColumn(column?: number): number { + return numberIsValid(column) ? column : 0; +} + +function locationIsValid(location: Location): location is Location { + const { line, column } = location; + return numberIsValid(line) && numberIsValid(column); +} + +function numberIsValid(number?: number): number is number { + return typeof number === "number" && isFinite(number) && number >= 0; +} + +function unknownToError(error: unknown): string { + if (error instanceof Error) { + const { message } = error; + return message; + } + return String(error); +} |