-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
08cbe17
commit a78ba06
Showing
16 changed files
with
922 additions
and
12 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export const SOURCE_MAP_URL_COMMENT = "//# sourceMappingURL="; | ||
export const SOURCE_MAP_HEADER = "SourceMap"; | ||
export const SNIPPET_LINES_ABOVE = 2; | ||
export const SNIPPET_LINES_BELOW = 3; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import _ from "lodash"; | ||
import ErrorStackParser from "error-stack-parser"; | ||
import logger from "../utils/logger"; | ||
import { softFileURLToPath } from "./utils"; | ||
import type { ResolvedFrame, SufficientStackFrame } from "./types"; | ||
|
||
/** | ||
* @description | ||
* Rank values: | ||
* | ||
* 0: Can't extract code snippet; useless | ||
* | ||
* 1: WebdriverIO internals: Better than nothing | ||
* | ||
* 2: Project internals: Better than WebdriverIO internals, but worse, than user code part | ||
* | ||
* 3: User code: Best choice | ||
*/ | ||
const FRAME_REELVANCE: Record<string, { value: number; matcher: (fileName: string) => boolean }> = { | ||
repl: { value: 0, matcher: fileName => /^REPL\d+$/.test(fileName) }, | ||
nodeInternals: { value: 0, matcher: fileName => /^node:[a-zA-Z\-_]/.test(fileName) }, | ||
wdioInternals: { value: 1, matcher: fileName => fileName.includes("/node_modules/webdriverio/") }, | ||
projectInternals: { value: 2, matcher: fileName => fileName.includes("/node_modules/") }, | ||
userCode: { value: 3, matcher: () => true }, | ||
} as const; | ||
|
||
const getFrameRelevance = (frame: StackFrame): number => { | ||
if ([frame.fileName, frame.lineNumber, frame.columnNumber].some(_.isUndefined)) { | ||
return 0; | ||
} | ||
|
||
const fileName: string = softFileURLToPath(frame.fileName!); | ||
|
||
for (const factor in FRAME_REELVANCE) { | ||
if (FRAME_REELVANCE[factor].matcher(fileName)) { | ||
return FRAME_REELVANCE[factor].value; | ||
} | ||
} | ||
|
||
return 0; | ||
}; | ||
|
||
export const findRelevantStackFrame = (error: Error): SufficientStackFrame | null => { | ||
try { | ||
const parsedStackFrames = ErrorStackParser.parse(error); | ||
|
||
let relevantFrame: SufficientStackFrame | null = null; | ||
let relevantFrameRank = 0; | ||
|
||
for (const currentFrame of parsedStackFrames) { | ||
const currentFrameRank = getFrameRelevance(currentFrame); | ||
|
||
if (currentFrameRank > relevantFrameRank) { | ||
relevantFrame = currentFrame as SufficientStackFrame; | ||
relevantFrameRank = currentFrameRank; | ||
} | ||
} | ||
|
||
return relevantFrame; | ||
} catch (findError) { | ||
logger.warn("Unable to find relevant stack frame:", findError); | ||
|
||
return null; | ||
} | ||
}; | ||
|
||
export const resolveLocationWithStackFrame = ( | ||
stackFrame: SufficientStackFrame, | ||
fileContents: string, | ||
): ResolvedFrame => ({ | ||
file: softFileURLToPath(stackFrame.fileName), | ||
source: fileContents, | ||
location: { line: stackFrame.lineNumber, column: stackFrame.columnNumber }, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { findRelevantStackFrame, resolveLocationWithStackFrame } from "./frames"; | ||
import { extractSourceMaps, resolveLocationWithSourceMap } from "./source-maps"; | ||
import { getSourceCodeFile, formatErrorSnippet } from "./utils"; | ||
import logger from "../utils/logger"; | ||
import type { ResolvedFrame, SufficientStackFrame, WithSnippetError } from "./types"; | ||
|
||
const stackFrameLocationResolver = async (stackFrame: SufficientStackFrame): Promise<ResolvedFrame> => { | ||
const fileContents = await getSourceCodeFile(stackFrame.fileName); | ||
const sourceMaps = await extractSourceMaps(fileContents, stackFrame.fileName); | ||
|
||
return sourceMaps | ||
? resolveLocationWithSourceMap(stackFrame, sourceMaps) | ||
: resolveLocationWithStackFrame(stackFrame, fileContents); | ||
}; | ||
|
||
export const extendWithCodeSnippet = async (err: WithSnippetError): Promise<WithSnippetError> => { | ||
if (!err) { | ||
return err; | ||
} | ||
|
||
try { | ||
const relevantStackFrame = findRelevantStackFrame(err); | ||
|
||
if (!relevantStackFrame) { | ||
return err; | ||
} | ||
|
||
const { file, source, location } = await stackFrameLocationResolver(relevantStackFrame); | ||
|
||
err.snippet = formatErrorSnippet(err, { file, source, location }); | ||
|
||
return err; | ||
} catch (snippetError) { | ||
logger.warn("Unable to apply code snippet:", snippetError); | ||
|
||
return err; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { SourceMapConsumer, type BasicSourceMapConsumer } from "source-map"; | ||
import url from "url"; | ||
import { SOURCE_MAP_URL_COMMENT } from "./constants"; | ||
import { softFileURLToPath, getSourceCodeFile } from "./utils"; | ||
import type { SufficientStackFrame, ResolvedFrame } from "./types"; | ||
|
||
export const extractSourceMaps = async ( | ||
fileContents: string, | ||
fileName: string, | ||
): Promise<BasicSourceMapConsumer | null> => { | ||
const sourceMapsStartIndex = fileContents.indexOf(SOURCE_MAP_URL_COMMENT); | ||
const sourceMapsEndIndex = fileContents.indexOf("\n", sourceMapsStartIndex); | ||
|
||
if (sourceMapsStartIndex === -1) { | ||
return null; | ||
} | ||
|
||
const sourceMapUrl = | ||
sourceMapsEndIndex === -1 | ||
? fileContents.slice(sourceMapsStartIndex + SOURCE_MAP_URL_COMMENT.length) | ||
: fileContents.slice(sourceMapsStartIndex + SOURCE_MAP_URL_COMMENT.length, sourceMapsEndIndex); | ||
|
||
const sourceMaps = await getSourceCodeFile(url.resolve(fileName, sourceMapUrl)); | ||
|
||
return new SourceMapConsumer(sourceMaps) as Promise<BasicSourceMapConsumer>; | ||
}; | ||
|
||
export const resolveLocationWithSourceMap = ( | ||
stackFrame: SufficientStackFrame, | ||
sourceMaps: BasicSourceMapConsumer, | ||
): ResolvedFrame => { | ||
const positions = sourceMaps.originalPositionFor({ line: stackFrame.lineNumber, column: stackFrame.columnNumber }); | ||
const source = positions.source ? sourceMaps.sourceContentFor(positions.source) : null; | ||
const location = { line: positions.line!, column: positions.column! }; | ||
|
||
if (!source) { | ||
throw new Error("File source code could not be evaluated from the source map"); | ||
} | ||
|
||
if (!location.line || !location.column) { | ||
throw new Error("Line and column could not be evaluated from the source map"); | ||
} | ||
|
||
return { file: softFileURLToPath(sourceMaps.file), source, location }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import type { SetRequired } from "type-fest"; | ||
|
||
export type WithSnippetError = Error & { snippet?: string }; | ||
|
||
export type SufficientStackFrame = SetRequired<StackFrame, "fileName" | "lineNumber" | "columnNumber">; | ||
|
||
export type ResolvedFrame = { source: string; file: string; location: { line: number; column: number } }; |
Oops, something went wrong.