Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419import type { OriginalMapping } from '@jridgewell/trace-mapping'import type { ParsedStack, TestError } from './types'import { originalPositionFor } from '@jridgewell/trace-mapping'import { resolve } from 'pathe'import { isPrimitive, notNullish } from './helpers'
export interface StackTraceParserOptions { ignoreStackEntries?: (RegExp | string)[] getSourceMap?: (file: string) => unknown getUrlId?: (id: string) => string frameFilter?: (error: TestError, frame: ParsedStack) => boolean | void}
const CHROME_IE_STACK_REGEXP = /^\s*at .*(?:\S:\d+|\(native\))/mconst SAFARI_NATIVE_CODE_REGEXP = /^(?:eval@)?(?:\[native code\])?$/
const stackIgnorePatterns: (string | RegExp)[] = [ 'node:internal', /\/packages\/\w+\/dist\//, /\/@vitest\/\w+\/dist\//, '/vitest/dist/', '/vitest/src/', '/packages/expect/src/', '/packages/snapshot/src/', '/node_modules/chai/', '/node_modules/tinyspy/', '/vite/dist/node/module-runner', '/rolldown-vite/dist/node/module-runner', // browser related deps '/deps/chunk-', '/deps/@vitest', '/deps/loupe', '/deps/chai', '/browser-playwright/dist/locators.js', '/browser-webdriverio/dist/locators.js', '/browser-preview/dist/locators.js', /node:\w+/, /__vitest_test__/, /__vitest_browser__/, '/@id/__x00__vitest/browser', /\/deps\/vitest_/, '/deps/vitest.js',]
export { stackIgnorePatterns as defaultStackIgnorePatterns }
const NOW_LENGTH = Date.now().toString().lengthconst REGEXP_VITEST = new RegExp(`vitest=\\d{${NOW_LENGTH}}`)
function extractLocation(urlLike: string) { // Fail-fast but return locations like "(native)" if (!urlLike.includes(':')) { return [urlLike] }
const regExp = /(.+?)(?::(\d+))?(?::(\d+))?$/ const parts = regExp.exec(urlLike.replace(/^\(|\)$/g, '')) if (!parts) { return [urlLike] } let url = parts[1] if (url.startsWith('async ')) { url = url.slice(6) } if (url.startsWith('http:') || url.startsWith('https:')) { const urlObj = new URL(url) urlObj.searchParams.delete('import') urlObj.searchParams.delete('browserv') url = urlObj.pathname + urlObj.hash + urlObj.search } if (url.startsWith('/@fs/')) { const isWindows = /^\/@fs\/[a-zA-Z]:\//.test(url) url = url.slice(isWindows ? 5 : 4) } if (url.includes('vitest=')) { url = url.replace(REGEXP_VITEST, '').replace(/[?&]$/, '') } return [url, parts[2] || undefined, parts[3] || undefined]}
export function parseSingleFFOrSafariStack(raw: string): ParsedStack | null { let line = raw.trim()
if (SAFARI_NATIVE_CODE_REGEXP.test(line)) { return null }
if (line.includes(' > eval')) { line = line.replace( / line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g, ':$1', ) }
// Early return for lines that don't look like Firefox/Safari stack traces // Firefox/Safari stack traces must contain '@' and should have location info after it if (!line.includes('@')) { return null }
// Find the correct @ that separates function name from location // For cases like '@https://@fs/path' or 'functionName@https://@fs/path' // we need to find the first @ that precedes a valid location (containing :) let atIndex = -1 let locationPart = '' let functionName: string | undefined
// Try each @ from left to right to find the one that gives us a valid location for (let i = 0; i < line.length; i++) { if (line[i] === '@') { const candidateLocation = line.slice(i + 1) // Minimum length 3 for valid location: 1 for filename + 1 for colon + 1 for line number (e.g., "a:1") if (candidateLocation.includes(':') && candidateLocation.length >= 3) { atIndex = i locationPart = candidateLocation functionName = i > 0 ? line.slice(0, i) : undefined break } } }
// Validate we found a valid location with minimum length (filename:line format) if (atIndex === -1 || !locationPart.includes(':') || locationPart.length < 3) { return null } const [url, lineNumber, columnNumber] = extractLocation(locationPart)
if (!url || !lineNumber || !columnNumber) { return null }
return { file: url, method: functionName || '', line: Number.parseInt(lineNumber), column: Number.parseInt(columnNumber), }}
export function parseSingleStack(raw: string): ParsedStack | null { const line = raw.trim() if (!CHROME_IE_STACK_REGEXP.test(line)) { return parseSingleFFOrSafariStack(line) } return parseSingleV8Stack(line)}
// Based on https://github.com/stacktracejs/error-stack-parser// Credit to stacktracejsexport function parseSingleV8Stack(raw: string): ParsedStack | null { let line = raw.trim()
if (!CHROME_IE_STACK_REGEXP.test(line)) { return null }
if (line.includes('(eval ')) { line = line .replace(/eval code/g, 'eval') .replace(/(\(eval at [^()]*)|(,.*$)/g, '') }
let sanitizedLine = line .replace(/^\s+/, '') .replace(/\(eval code/g, '(') .replace(/^.*?\s+/, '')
// capture and preserve the parenthesized location "(/foo/my bar.js:12:87)" in // case it has spaces in it, as the string is split on \s+ later on const location = sanitizedLine.match(/ (\(.+\)$)/)
// remove the parenthesized location from the line, if it was matched sanitizedLine = location ? sanitizedLine.replace(location[0], '') : sanitizedLine
// if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine // because this line doesn't have function name const [url, lineNumber, columnNumber] = extractLocation( location ? location[1] : sanitizedLine, ) let method = (location && sanitizedLine) || '' let file = url && ['eval', '<anonymous>'].includes(url) ? undefined : url
if (!file || !lineNumber || !columnNumber) { return null }
if (method.startsWith('async ')) { method = method.slice(6) }
if (file.startsWith('file://')) { file = file.slice(7) }
// normalize Windows path (\ -> /) file = file.startsWith('node:') || file.startsWith('internal:') ? file : resolve(file)
if (method) { method = method // vite 7+ .replace(/\(0\s?,\s?__vite_ssr_import_\d+__.(\w+)\)/g, '$1') // vite <7 .replace(/__(vite_ssr_import|vi_import)_\d+__\./g, '') .replace(/(Object\.)?__vite_ssr_export_default__\s?/g, '') }
return { method, file, line: Number.parseInt(lineNumber), column: Number.parseInt(columnNumber), }}
export function createStackString(stacks: ParsedStack[]): string { return stacks.map((stack) => { const line = `${stack.file}:${stack.line}:${stack.column}` if (stack.method) { return ` at ${stack.method}(${line})` } return ` at ${line}` }).join('\n')}
export function parseStacktrace( stack: string, options: StackTraceParserOptions = {},): ParsedStack[] { const { ignoreStackEntries = stackIgnorePatterns } = options let stacks = !CHROME_IE_STACK_REGEXP.test(stack) ? parseFFOrSafariStackTrace(stack) : parseV8Stacktrace(stack)
// remove vi.defineHelper's internal stacks const helperIndex = stacks.findLastIndex(s => // this covers cases such as // "__VITEST_HELPER__" // "__VITEST_HELPER__ [as <object method name>]" // "async*__VITEST_HELPER__" (firefox) // "async __VITEST_HELPER__" (webkit) s.method.includes('__VITEST_HELPER__'), ) if (helperIndex >= 0) { stacks = stacks.slice(helperIndex + 1) }
return stacks.map((stack) => { if (options.getUrlId) { stack.file = options.getUrlId(stack.file) }
const map = options.getSourceMap?.(stack.file) as | SourceMapLike | null | undefined if (!map || typeof map !== 'object' || !map.version) { return shouldFilter(ignoreStackEntries, stack.file) ? null : stack }
const traceMap = new DecodedMap(map, stack.file) if (stack.line <= 0 || stack.column <= 0) { return stack } const position = getOriginalPosition(traceMap, { line: stack.line, // stacktrace's column is 1-indexed, but sourcemap's one is 0-indexed column: stack.column - 1, }) if (!position) { return stack }
const { line, column, source, name } = position let file = source || stack.file if (/\/\w:\//.test(file)) { file = file.slice(1) }
if (shouldFilter(ignoreStackEntries, file)) { return null }
if (line != null && column != null) { return { line, column: column + 1, file, method: name || stack.method, } } return stack }).filter(s => s != null)}
function shouldFilter(ignoreStackEntries: (string | RegExp)[], file: string): boolean { return ignoreStackEntries.some(p => file.match(p))}
function parseFFOrSafariStackTrace(stack: string): ParsedStack[] { return stack .split('\n') .map(line => parseSingleFFOrSafariStack(line)) .filter(notNullish)}
function parseV8Stacktrace(stack: string): ParsedStack[] { return stack .split('\n') .map(line => parseSingleV8Stack(line)) .filter(notNullish)}
export function parseErrorStacktrace( e: TestError | Error, options: StackTraceParserOptions = {},): ParsedStack[] { if (!e || isPrimitive(e)) { return [] }
if ('stacks' in e && e.stacks) { return e.stacks }
const stackStr = e.stack || '' // if "stack" property was overwritten at runtime to be something else, // ignore the value because we don't know how to process it let stackFrames = typeof stackStr === 'string' ? parseStacktrace(stackStr, options) : []
if (!stackFrames.length) { const e_ = e as any if (e_.fileName != null && e_.lineNumber != null && e_.columnNumber != null) { stackFrames = parseStacktrace(`${e_.fileName}:${e_.lineNumber}:${e_.columnNumber}`, options) } if (e_.sourceURL != null && e_.line != null && e_._column != null) { stackFrames = parseStacktrace(`${e_.sourceURL}:${e_.line}:${e_.column}`, options) } }
if (options.frameFilter) { stackFrames = stackFrames.filter( f => options.frameFilter!(e as TestError, f) !== false, ) }
;(e as TestError).stacks = stackFrames return stackFrames}
interface SourceMapLike { version: number mappings?: string names?: string[] sources?: string[] sourcesContent?: string[] sourceRoot?: string}
interface Needle { line: number column: number}
export class DecodedMap { _encoded: string _decoded: undefined | number[][][] _decodedMemo: Stats url: string version: number names: string[] = [] resolvedSources: string[]
constructor( public map: SourceMapLike, from: string, ) { const { mappings, names, sources } = map this.version = map.version this.names = names || [] this._encoded = mappings || '' this._decodedMemo = memoizedState() this.url = from this.resolvedSources = (sources || []).map(s => resolve(from, '..', s || ''), ) }}
interface Stats { lastKey: number lastNeedle: number lastIndex: number}
function memoizedState(): Stats { return { lastKey: -1, lastNeedle: -1, lastIndex: -1, }}
export function getOriginalPosition( map: DecodedMap, needle: Needle,): OriginalMapping | null { const result = originalPositionFor(map as any, needle) if (result.column == null) { return null } return result}