Skip to content

Commit e2c570b

Browse files
authored
fix(browser): improve source map handling for bundled files (#7534)
1 parent 5387a5b commit e2c570b

File tree

12 files changed

+124
-26
lines changed

12 files changed

+124
-26
lines changed

packages/browser/src/node/projectParent.ts

+49-7
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import type {
1010
Vitest,
1111
} from 'vitest/node'
1212
import type { BrowserServerState } from './state'
13+
import { readFileSync } from 'node:fs'
1314
import { readFile } from 'node:fs/promises'
1415
import { parseErrorStacktrace, parseStacktrace, type StackTraceParserOptions } from '@vitest/utils/source-map'
15-
import { join, resolve } from 'pathe'
16+
import { dirname, join, resolve } from 'pathe'
1617
import { BrowserServerCDPHandler } from './cdp'
1718
import builtinCommands from './commands/index'
1819
import { distRoot } from './constants'
@@ -41,6 +42,9 @@ export class ParentBrowserProject {
4142

4243
public config: ResolvedConfig
4344

45+
// cache for non-vite source maps
46+
private sourceMapCache = new Map<string, any>()
47+
4448
constructor(
4549
public project: TestProject,
4650
public base: string,
@@ -50,17 +54,39 @@ export class ParentBrowserProject {
5054
this.stackTraceOptions = {
5155
frameFilter: project.config.onStackTrace,
5256
getSourceMap: (id) => {
57+
if (this.sourceMapCache.has(id)) {
58+
return this.sourceMapCache.get(id)
59+
}
5360
const result = this.vite.moduleGraph.getModuleById(id)?.transformResult
61+
// this can happen for bundled dependencies in node_modules/.vite
62+
if (result && !result.map) {
63+
const sourceMapUrl = this.retrieveSourceMapURL(result.code)
64+
if (!sourceMapUrl) {
65+
return null
66+
}
67+
const filepathDir = dirname(id)
68+
const sourceMapPath = resolve(filepathDir, sourceMapUrl)
69+
const map = JSON.parse(readFileSync(sourceMapPath, 'utf-8'))
70+
this.sourceMapCache.set(id, map)
71+
return map
72+
}
5473
return result?.map
5574
},
56-
getFileName: (id) => {
75+
getUrlId: (id) => {
5776
const mod = this.vite.moduleGraph.getModuleById(id)
58-
if (mod?.file) {
59-
return mod.file
77+
if (mod) {
78+
return id
79+
}
80+
const resolvedPath = resolve(project.config.root, id.slice(1))
81+
const modUrl = this.vite.moduleGraph.getModuleById(resolvedPath)
82+
if (modUrl) {
83+
return resolvedPath
6084
}
61-
const modUrl = this.vite.moduleGraph.urlToModuleMap.get(id)
62-
if (modUrl?.file) {
63-
return modUrl.file
85+
// some browsers (looking at you, safari) don't report queries in stack traces
86+
// the next best thing is to try the first id that this file resolves to
87+
const files = this.vite.moduleGraph.getModulesByFile(resolvedPath)
88+
if (files && files.size) {
89+
return files.values().next().value!.id!
6490
}
6591
return id
6692
},
@@ -235,4 +261,20 @@ export class ParentBrowserProject {
235261
const decodedTestFile = decodeURIComponent(testFile)
236262
return { sessionId, testFile: decodedTestFile }
237263
}
264+
265+
private retrieveSourceMapURL(source: string): string | null {
266+
const re
267+
= /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm
268+
// Keep executing the search to find the *last* sourceMappingURL to avoid
269+
// picking up sourceMappingURLs from comments, strings, etc.
270+
let lastMatch, match
271+
// eslint-disable-next-line no-cond-assign
272+
while ((match = re.exec(source))) {
273+
lastMatch = match
274+
}
275+
if (!lastMatch) {
276+
return null
277+
}
278+
return lastMatch[1]
279+
}
238280
}

packages/utils/src/source-map.ts

+38-14
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type { SourceMapInput } from '@jridgewell/trace-mapping'
1616
export interface StackTraceParserOptions {
1717
ignoreStackEntries?: (RegExp | string)[]
1818
getSourceMap?: (file: string) => unknown
19-
getFileName?: (id: string) => string
19+
getUrlId?: (id: string) => string
2020
frameFilter?: (error: ErrorWithDiff, frame: ParsedStack) => boolean | void
2121
}
2222

@@ -62,7 +62,9 @@ function extractLocation(urlLike: string) {
6262
}
6363
if (url.startsWith('http:') || url.startsWith('https:')) {
6464
const urlObj = new URL(url)
65-
url = urlObj.pathname
65+
urlObj.searchParams.delete('import')
66+
urlObj.searchParams.delete('browserv')
67+
url = urlObj.pathname + urlObj.hash + urlObj.search
6668
}
6769
if (url.startsWith('/@fs/')) {
6870
const isWindows = /^\/@fs\/[a-zA-Z]:\//.test(url)
@@ -198,33 +200,55 @@ export function parseStacktrace(
198200
options: StackTraceParserOptions = {},
199201
): ParsedStack[] {
200202
const { ignoreStackEntries = stackIgnorePatterns } = options
201-
let stacks = !CHROME_IE_STACK_REGEXP.test(stack)
203+
const stacks = !CHROME_IE_STACK_REGEXP.test(stack)
202204
? parseFFOrSafariStackTrace(stack)
203205
: parseV8Stacktrace(stack)
204-
if (ignoreStackEntries.length) {
205-
stacks = stacks.filter(
206-
stack => !ignoreStackEntries.some(p => stack.file.match(p)),
207-
)
208-
}
206+
209207
return stacks.map((stack) => {
210-
if (options.getFileName) {
211-
stack.file = options.getFileName(stack.file)
208+
if (options.getUrlId) {
209+
stack.file = options.getUrlId(stack.file)
212210
}
213211

214212
const map = options.getSourceMap?.(stack.file) as
215213
| SourceMapInput
216214
| null
217215
| undefined
218216
if (!map || typeof map !== 'object' || !map.version) {
219-
return stack
217+
return shouldFilter(ignoreStackEntries, stack.file) ? null : stack
220218
}
219+
221220
const traceMap = new TraceMap(map)
222-
const { line, column } = originalPositionFor(traceMap, stack)
221+
const { line, column, source, name } = originalPositionFor(traceMap, stack)
222+
223+
let file: string = stack.file
224+
if (source) {
225+
const fileUrl = stack.file.startsWith('file://')
226+
? stack.file
227+
: `file://${stack.file}`
228+
const sourceRootUrl = map.sourceRoot
229+
? new URL(map.sourceRoot, fileUrl)
230+
: fileUrl
231+
file = new URL(source, sourceRootUrl).pathname
232+
}
233+
234+
if (shouldFilter(ignoreStackEntries, file)) {
235+
return null
236+
}
237+
223238
if (line != null && column != null) {
224-
return { ...stack, line, column }
239+
return {
240+
line,
241+
column,
242+
file,
243+
method: name || stack.method,
244+
}
225245
}
226246
return stack
227-
})
247+
}).filter(s => s != null)
248+
}
249+
250+
function shouldFilter(ignoreStackEntries: (string | RegExp)[], file: string): boolean {
251+
return ignoreStackEntries.some(p => file.match(p))
228252
}
229253

230254
function parseFFOrSafariStackTrace(stack: string): ParsedStack[] {

pnpm-lock.yaml

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/browser/bundled-lib/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "@vitest/bundled-lib",
3+
"type": "module",
4+
"private": true,
5+
"main": "./src/index.js"
6+
}

test/browser/bundled-lib/src/a.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function a() {
2+
return 'a'
3+
}

test/browser/bundled-lib/src/b.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function b() {
2+
throw new Error('error from b')
3+
}
+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export declare function index(): string

test/browser/bundled-lib/src/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { a } from './a.js'
2+
import { b } from './b.js'
3+
4+
export function index() {
5+
return a() + b()
6+
}

test/browser/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@types/react": "^18.2.79",
2727
"@vitejs/plugin-basic-ssl": "^1.0.2",
2828
"@vitest/browser": "workspace:*",
29+
"@vitest/bundled-lib": "link:./bundled-lib",
2930
"@vitest/cjs-lib": "link:./cjs-lib",
3031
"@vitest/injected-lib": "link:./injected-lib",
3132
"react": "^18.3.1",

test/browser/specs/runner.test.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,19 @@ error with a stack
163163

164164
test(`stack trace points to correct file in every browser`, () => {
165165
// depending on the browser it references either `.toBe()` or `expect()`
166-
expect(stderr).toMatch(/test\/failing.test.ts:10:(12|17)/)
166+
expect(stderr).toMatch(/test\/failing.test.ts:11:(12|17)/)
167167

168168
// column is 18 in safari, 8 in others
169169
expect(stderr).toMatch(/throwError src\/error.ts:8:(18|8)/)
170170

171171
expect(stderr).toContain('The call was not awaited. This method is asynchronous and must be awaited; otherwise, the call will not start to avoid unhandled rejections.')
172-
expect(stderr).toMatch(/test\/failing.test.ts:18:(27|36)/)
173-
expect(stderr).toMatch(/test\/failing.test.ts:19:(27|33)/)
174-
expect(stderr).toMatch(/test\/failing.test.ts:20:(27|39)/)
172+
expect(stderr).toMatch(/test\/failing.test.ts:19:(27|36)/)
173+
expect(stderr).toMatch(/test\/failing.test.ts:20:(27|33)/)
174+
expect(stderr).toMatch(/test\/failing.test.ts:21:(27|39)/)
175+
176+
expect(stderr).toMatch(/bundled-lib\/src\/b.js:2:(8|18)/)
177+
expect(stderr).toMatch(/bundled-lib\/src\/index.js:5:(15|17)/)
178+
expect(stderr).toMatch(/test\/failing.test.ts:25:(2|8)/)
175179
})
176180

177181
test('popup apis should log a warning', () => {

test/browser/test/failing.test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { page } from '@vitest/browser/context'
2+
import { index } from '@vitest/bundled-lib'
23
import { expect, it } from 'vitest'
34
import { throwError } from '../src/error'
45

@@ -19,3 +20,7 @@ it('several locator methods are not awaited', () => {
1920
page.getByRole('button').click()
2021
page.getByRole('button').tripleClick()
2122
})
23+
24+
it('correctly prints error from a bundled file', () => {
25+
index()
26+
})

test/browser/vitest.config.mts

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default defineConfig({
4242
},
4343
},
4444
optimizeDeps: {
45-
include: ['@vitest/cjs-lib', 'react/jsx-dev-runtime'],
45+
include: ['@vitest/cjs-lib', '@vitest/bundled-lib', 'react/jsx-dev-runtime'],
4646
},
4747
test: {
4848
include: ['test/**.test.{ts,js,tsx}'],

0 commit comments

Comments
 (0)